mirror of
https://github.com/2dust/v2rayN.git
synced 2026-05-30 01:34:08 +00:00
Some checks failed
release Linux / build (push) Has been cancelled
release Linux / build and release deb x64 & arm64 (push) Has been cancelled
release Linux / build and release rpm x64 & arm64 (push) Has been cancelled
release Linux / build and release rpm riscv64 (push) Has been cancelled
release macOS / build (push) Has been cancelled
release Windows desktop (Avalonia UI) / build (push) Has been cancelled
release Windows / build (push) Has been cancelled
release Linux / release-zip (push) Has been cancelled
release macOS / release-zip (push) Has been cancelled
release macOS / package and release macOS dmg (push) Has been cancelled
release Windows desktop (Avalonia UI) / release-zip (push) Has been cancelled
release Windows / release-zip (push) Has been cancelled
420 lines
14 KiB
C#
420 lines
14 KiB
C#
namespace ServiceLib.UdpTest;
|
|
|
|
public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposable
|
|
{
|
|
private TcpClient _tcpClient;
|
|
private UdpClient _udpClient;
|
|
private IPEndPoint _relayEndPoint;
|
|
|
|
private bool _initialized = false;
|
|
|
|
/// <summary>
|
|
/// Send UDP data to a remote endpoint (IP address)
|
|
/// </summary>
|
|
public async Task SendAsync(IPEndPoint remote, byte[] data)
|
|
{
|
|
var addrData = new Socks5AddressData
|
|
{
|
|
AddressType = remote.Address.AddressFamily == AddressFamily.InterNetwork
|
|
? Socks5AddressData.AddrTypeIPv4
|
|
: Socks5AddressData.AddrTypeIPv6,
|
|
Host = remote.Address.ToString(),
|
|
Port = (ushort)remote.Port
|
|
};
|
|
var packet = BuildSocks5UdpPacket(addrData, data);
|
|
await _udpClient.SendAsync(packet, packet.Length, _relayEndPoint);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Send UDP data to a remote endpoint (domain name or IP address)
|
|
/// </summary>
|
|
/// <param name="host">Domain name or IP address</param>
|
|
/// <param name="port">Port number</param>
|
|
/// <param name="data">Data to send</param>
|
|
public async Task SendAsync(string host, ushort port, byte[] data)
|
|
{
|
|
var addrData = new Socks5AddressData();
|
|
|
|
// Try to parse as IP address first
|
|
if (IPAddress.TryParse(host, out var ipAddr))
|
|
{
|
|
addrData.AddressType = ipAddr.AddressFamily == AddressFamily.InterNetwork
|
|
? Socks5AddressData.AddrTypeIPv4
|
|
: Socks5AddressData.AddrTypeIPv6;
|
|
addrData.Host = ipAddr.ToString();
|
|
}
|
|
else
|
|
{
|
|
// Treat as domain name
|
|
addrData.AddressType = Socks5AddressData.AddrTypeDomain;
|
|
addrData.Host = host;
|
|
}
|
|
|
|
addrData.Port = port;
|
|
|
|
var packet = BuildSocks5UdpPacket(addrData, data);
|
|
await _udpClient.SendAsync(packet, packet.Length, _relayEndPoint);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Receive UDP data from remote endpoint
|
|
/// </summary>
|
|
/// <param name="cancellationToken">Cancellation token to cancel the receive operation</param>
|
|
/// <returns>Remote endpoint information and received data</returns>
|
|
public async Task<(Socks5RemoteEndpoint Remote, byte[] Data)> ReceiveAsync(
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var result = await _udpClient.ReceiveAsync(cancellationToken).ConfigureAwait(false);
|
|
var (remote, payload) = ParseSocks5UdpPacket(result.Buffer);
|
|
return (remote, payload);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Represents a remote endpoint that can be either an IP address or a domain name
|
|
/// </summary>
|
|
public class Socks5RemoteEndpoint(string host, ushort port, bool isDomain)
|
|
{
|
|
public string Host { get; set; } = host;
|
|
public ushort Port { get; set; } = port;
|
|
public bool IsDomain { get; set; } = isDomain;
|
|
}
|
|
|
|
private static byte[] BuildSocks5UdpPacket(Socks5AddressData addressData, byte[] data)
|
|
{
|
|
using var ms = new MemoryStream();
|
|
|
|
// RSV (2 bytes) + FRAG (1 byte) - Reserved and Fragment fields
|
|
ms.WriteByte(0x00);
|
|
ms.WriteByte(0x00);
|
|
ms.WriteByte(0x00);
|
|
|
|
// Write address (ATYP + address + port)
|
|
ms.Write(addressData.ToBytes());
|
|
|
|
// User data payload
|
|
ms.Write(data);
|
|
|
|
return ms.ToArray();
|
|
}
|
|
|
|
private static (Socks5RemoteEndpoint Remote, byte[] Data) ParseSocks5UdpPacket(byte[] packet)
|
|
{
|
|
if (packet.Length < 10) // Minimum length: RSV(2) + FRAG(1) + ATYP(1) + IPv4(4) + Port(2) = 10
|
|
{
|
|
throw new ArgumentException("Invalid SOCKS5 UDP packet: too short");
|
|
}
|
|
|
|
var offset = 0;
|
|
|
|
// RSV (2 bytes) - Reserved field, skip
|
|
offset += 2;
|
|
|
|
// FRAG (1 byte) - Fragment number, currently only support 0 (no fragmentation)
|
|
var frag = packet[offset++];
|
|
if (frag != 0x00)
|
|
{
|
|
throw new NotSupportedException("SOCKS5 UDP fragmentation is not supported");
|
|
}
|
|
|
|
// ATYP (1 byte) - Address type
|
|
var addressType = packet[offset++];
|
|
|
|
string host;
|
|
int addressLength;
|
|
bool isDomain;
|
|
|
|
switch (addressType)
|
|
{
|
|
case Socks5AddressData.AddrTypeIPv4:
|
|
if (packet.Length < offset + 4)
|
|
{
|
|
throw new ArgumentException("Invalid SOCKS5 UDP packet: IPv4 address incomplete");
|
|
}
|
|
|
|
var ipv4Bytes = new byte[4];
|
|
Array.Copy(packet, offset, ipv4Bytes, 0, 4);
|
|
host = new IPAddress(ipv4Bytes).ToString();
|
|
addressLength = 4;
|
|
isDomain = false;
|
|
break;
|
|
|
|
case Socks5AddressData.AddrTypeIPv6:
|
|
if (packet.Length < offset + 16)
|
|
{
|
|
throw new ArgumentException("Invalid SOCKS5 UDP packet: IPv6 address incomplete");
|
|
}
|
|
|
|
var ipv6Bytes = new byte[16];
|
|
Array.Copy(packet, offset, ipv6Bytes, 0, 16);
|
|
host = new IPAddress(ipv6Bytes).ToString();
|
|
addressLength = 16;
|
|
isDomain = false;
|
|
break;
|
|
|
|
case Socks5AddressData.AddrTypeDomain:
|
|
if (packet.Length < offset + 1)
|
|
{
|
|
throw new ArgumentException("Invalid SOCKS5 UDP packet: domain length missing");
|
|
}
|
|
|
|
var domainLength = packet[offset++];
|
|
if (packet.Length < offset + domainLength)
|
|
{
|
|
throw new ArgumentException("Invalid SOCKS5 UDP packet: domain incomplete");
|
|
}
|
|
|
|
host = Encoding.ASCII.GetString(packet, offset, domainLength);
|
|
addressLength = domainLength;
|
|
isDomain = true;
|
|
break;
|
|
|
|
default:
|
|
throw new NotSupportedException($"Unsupported SOCKS5 address type: {addressType}");
|
|
}
|
|
|
|
offset += addressLength;
|
|
|
|
// Port (2 bytes, big-endian)
|
|
if (packet.Length < offset + 2)
|
|
{
|
|
throw new ArgumentException("Invalid SOCKS5 UDP packet: port incomplete");
|
|
}
|
|
|
|
var port = BinaryPrimitives.ReadUInt16BigEndian(packet.AsSpan(offset, 2));
|
|
offset += 2;
|
|
|
|
// Data (remaining bytes)
|
|
var dataLength = packet.Length - offset;
|
|
var data = new byte[dataLength];
|
|
if (dataLength > 0)
|
|
{
|
|
Array.Copy(packet, offset, data, 0, dataLength);
|
|
}
|
|
|
|
// Create remote endpoint without DNS resolution
|
|
var remote = new Socks5RemoteEndpoint(host, port, isDomain);
|
|
return (remote, data);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_tcpClient.Dispose();
|
|
_udpClient.Dispose();
|
|
}
|
|
|
|
#region SOCKS5 Connection Handling
|
|
|
|
private const byte Socks5Version = 0x05;
|
|
private const byte SocksCmdUdpAssociate = 0x03;
|
|
|
|
public async Task<bool> EstablishUdpAssociationAsync(CancellationToken cancellationToken)
|
|
{
|
|
if (_initialized)
|
|
{
|
|
Dispose();
|
|
_initialized = false;
|
|
}
|
|
|
|
_udpClient = new UdpClient(new IPEndPoint(IPAddress.Any, 0));
|
|
_tcpClient = new TcpClient();
|
|
try
|
|
{
|
|
await _tcpClient.ConnectAsync(socks5Host, socks5TcpPort, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
catch (SocketException)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var tcpControlStream = _tcpClient.GetStream();
|
|
|
|
byte[] handshakeRequest = [Socks5Version, 0x01, 0x00];
|
|
await tcpControlStream.WriteAsync(handshakeRequest, cancellationToken).ConfigureAwait(false);
|
|
var handshakeResponse = new byte[2];
|
|
if (await tcpControlStream.ReadAsync(handshakeResponse, cancellationToken).ConfigureAwait(false) < 2 ||
|
|
handshakeResponse[0] != Socks5Version || handshakeResponse[1] != 0x00)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var clientAddrForSocks = new Socks5AddressData
|
|
{
|
|
AddressType = Socks5AddressData.AddrTypeIPv4,
|
|
Host = "0.0.0.0",
|
|
Port = 0
|
|
};
|
|
using var udpAssociateReqMs = new MemoryStream();
|
|
udpAssociateReqMs.WriteByte(Socks5Version);
|
|
udpAssociateReqMs.WriteByte(SocksCmdUdpAssociate);
|
|
udpAssociateReqMs.WriteByte(0x00);
|
|
udpAssociateReqMs.Write(clientAddrForSocks.ToBytes());
|
|
await tcpControlStream.WriteAsync(udpAssociateReqMs.ToArray(), cancellationToken).ConfigureAwait(false);
|
|
|
|
var verRepRsv = new byte[3];
|
|
if (await tcpControlStream.ReadAsync(verRepRsv, cancellationToken).ConfigureAwait(false) < 3 ||
|
|
verRepRsv[0] != Socks5Version || verRepRsv[1] != 0x00)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var proxyRelaySocksAddr =
|
|
await Socks5AddressData.ParseAsync(tcpControlStream, cancellationToken).ConfigureAwait(false);
|
|
if (proxyRelaySocksAddr == null || !IPAddress.TryParse(proxyRelaySocksAddr.Host, out var proxyRelayIp))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
_relayEndPoint = new IPEndPoint(proxyRelayIp, proxyRelaySocksAddr.Port);
|
|
_initialized = true;
|
|
return true;
|
|
}
|
|
|
|
#endregion SOCKS5 Connection Handling
|
|
|
|
#region SOCKS5 Address Handling
|
|
|
|
private class Socks5AddressData
|
|
{
|
|
public const byte AddrTypeIPv4 = 0x01;
|
|
public const byte AddrTypeDomain = 0x03;
|
|
public const byte AddrTypeIPv6 = 0x04;
|
|
|
|
public byte AddressType { get; set; }
|
|
public string Host { get; set; } = string.Empty;
|
|
public ushort Port { get; set; }
|
|
|
|
public byte[] ToBytes()
|
|
{
|
|
using var ms = new MemoryStream();
|
|
ms.WriteByte(AddressType);
|
|
switch (AddressType)
|
|
{
|
|
case AddrTypeIPv4:
|
|
if (IPAddress.TryParse(Host, out var ip) && ip.AddressFamily == AddressFamily.InterNetwork)
|
|
{
|
|
ms.Write(ip.GetAddressBytes(), 0, 4);
|
|
}
|
|
else
|
|
{
|
|
ms.Write([0, 0, 0, 0]);
|
|
}
|
|
|
|
break;
|
|
|
|
case AddrTypeDomain:
|
|
if (string.IsNullOrEmpty(Host))
|
|
{
|
|
ms.WriteByte(0);
|
|
}
|
|
else
|
|
{
|
|
var domainBytes = Encoding.ASCII.GetBytes(Host);
|
|
ms.WriteByte((byte)domainBytes.Length);
|
|
ms.Write(domainBytes);
|
|
}
|
|
|
|
break;
|
|
|
|
case AddrTypeIPv6:
|
|
if (IPAddress.TryParse(Host, out var ip6) && ip6.AddressFamily == AddressFamily.InterNetworkV6)
|
|
{
|
|
ms.Write(ip6.GetAddressBytes(), 0, 16);
|
|
}
|
|
else
|
|
{
|
|
ms.Write(new byte[16]);
|
|
}
|
|
|
|
break;
|
|
|
|
default:
|
|
throw new NotSupportedException($"SOCKS5 address type {AddressType} not supported.");
|
|
}
|
|
|
|
var portBytes = new byte[2];
|
|
BinaryPrimitives.WriteUInt16BigEndian(portBytes, Port);
|
|
ms.Write(portBytes);
|
|
return ms.ToArray();
|
|
}
|
|
|
|
public static async Task<Socks5AddressData?> ParseAsync(Stream stream, CancellationToken ct)
|
|
{
|
|
var addr = new Socks5AddressData();
|
|
var typeByte = new byte[1];
|
|
try
|
|
{
|
|
if (await stream.ReadAsync(typeByte.AsMemory(0, 1), ct).ConfigureAwait(false) < 1)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
addr.AddressType = typeByte[0];
|
|
switch (addr.AddressType)
|
|
{
|
|
case AddrTypeIPv4:
|
|
var ipv4Bytes = new byte[4];
|
|
if (await stream.ReadAsync(ipv4Bytes.AsMemory(0, 4), ct).ConfigureAwait(false) < 4)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
addr.Host = new IPAddress(ipv4Bytes).ToString();
|
|
break;
|
|
|
|
case AddrTypeDomain:
|
|
var lenByte = new byte[1];
|
|
if (await stream.ReadAsync(lenByte.AsMemory(0, 1), ct).ConfigureAwait(false) < 1)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (lenByte[0] == 0)
|
|
{
|
|
addr.Host = string.Empty;
|
|
}
|
|
else
|
|
{
|
|
var domainBytes = new byte[lenByte[0]];
|
|
if (await stream.ReadAsync(domainBytes.AsMemory(0, domainBytes.Length), ct)
|
|
.ConfigureAwait(false) < domainBytes.Length)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
addr.Host = Encoding.ASCII.GetString(domainBytes);
|
|
}
|
|
|
|
break;
|
|
|
|
case AddrTypeIPv6:
|
|
var ipv6Bytes = new byte[16];
|
|
if (await stream.ReadAsync(ipv6Bytes.AsMemory(0, 16), ct).ConfigureAwait(false) < 16)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
addr.Host = new IPAddress(ipv6Bytes).ToString();
|
|
break;
|
|
|
|
default:
|
|
return null;
|
|
}
|
|
|
|
var portBytes = new byte[2];
|
|
if (await stream.ReadAsync(portBytes.AsMemory(0, 2), ct).ConfigureAwait(false) < 2)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
addr.Port = BinaryPrimitives.ReadUInt16BigEndian(portBytes);
|
|
return addr;
|
|
}
|
|
catch (Exception ex) when (ex is IOException or ObjectDisposedException)
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion SOCKS5 Address Handling
|
|
}
|