namespace ServiceLib.UdpTest; public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposable { private TcpClient _tcpClient; private UdpClient _udpClient; private IPEndPoint _relayEndPoint; private bool _initialized = false; /// /// Send UDP data to a remote endpoint (IP address) /// 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); } /// /// Send UDP data to a remote endpoint (domain name or IP address) /// /// Domain name or IP address /// Port number /// Data to send 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); } /// /// Receive UDP data from remote endpoint /// /// Cancellation token to cancel the receive operation /// Remote endpoint information and received data 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); } /// /// Represents a remote endpoint that can be either an IP address or a domain name /// 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 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 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 }