diff --git a/v2rayN/ServiceLib.UdpTest/Socks5UdpChannel.cs b/v2rayN/ServiceLib.UdpTest/Socks5UdpChannel.cs index 45f93c22..d6c8786b 100644 --- a/v2rayN/ServiceLib.UdpTest/Socks5UdpChannel.cs +++ b/v2rayN/ServiceLib.UdpTest/Socks5UdpChannel.cs @@ -2,14 +2,19 @@ namespace ServiceLib.UdpTest; public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposable { + private bool _initialized; + private IPEndPoint _relayEndPoint; private TcpClient _tcpClient; private UdpClient _udpClient; - private IPEndPoint _relayEndPoint; - private bool _initialized = false; + public void Dispose() + { + _tcpClient?.Dispose(); + _udpClient?.Dispose(); + } /// - /// Send UDP data to a remote endpoint (IP address) + /// Send UDP data to a remote endpoint (IP address) /// public async Task SendAsync(IPEndPoint remote, byte[] data, CancellationToken ct = default) { @@ -19,14 +24,14 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl ? Socks5AddressData.AddrTypeIPv4 : Socks5AddressData.AddrTypeIPv6, Host = remote.Address.ToString(), - Port = (ushort)remote.Port + Port = (ushort)remote.Port, }; var packet = BuildSocks5UdpPacket(addrData, data); await _udpClient.SendAsync(packet.AsMemory(), _relayEndPoint, ct); } /// - /// Send UDP data to a remote endpoint (domain name or IP address) + /// Send UDP data to a remote endpoint (domain name or IP address) /// /// Domain name or IP address /// Port number @@ -54,12 +59,12 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl addrData.Port = port; var packet = BuildSocks5UdpPacket(addrData, data); - //await _udpClient.SendAsync(packet, packet.Length, _relayEndPoint); + // await _udpClient.SendAsync(packet, packet.Length, _relayEndPoint); await _udpClient.SendAsync(packet.AsMemory(), _relayEndPoint, ct); } /// - /// Receive UDP data from remote endpoint + /// Receive UDP data from remote endpoint /// /// Cancellation token to cancel the receive operation /// Remote endpoint information and received data @@ -71,16 +76,6 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl 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(); @@ -106,6 +101,11 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl throw new ArgumentException("Invalid SOCKS5 UDP packet: too short"); } + if (packet[0] != 0x00 || packet[1] != 0x00) + { + throw new ArgumentException("Invalid SOCKS5 UDP packet: RSV field must be 0"); + } + var offset = 0; // RSV (2 bytes) - Reserved field, skip @@ -198,83 +198,34 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl return (remote, data); } - public void Dispose() + /// + /// Represents a remote endpoint that can be either an IP address or a domain name + /// + public class Socks5RemoteEndpoint(string host, ushort port, bool isDomain) { - _tcpClient.Dispose(); - _udpClient.Dispose(); + public string Host { get; set; } = host; + public ushort Port { get; set; } = port; + public bool IsDomain { get; set; } = isDomain; } - #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 static async Task TryReadExactlyAsync( + Stream stream, + Memory buffer, + CancellationToken cancellationToken) + { + try + { + await stream.ReadExactlyAsync(buffer, cancellationToken).ConfigureAwait(false); + return true; + } + catch (EndOfStreamException) + { + return false; + } + } + private class Socks5AddressData { public const byte AddrTypeIPv4 = 0x01; @@ -345,7 +296,7 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl var typeByte = new byte[1]; try { - if (await stream.ReadAsync(typeByte.AsMemory(0, 1), ct).ConfigureAwait(false) < 1) + if (!await TryReadExactlyAsync(stream, typeByte, ct).ConfigureAwait(false)) { return null; } @@ -355,7 +306,7 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl { case AddrTypeIPv4: var ipv4Bytes = new byte[4]; - if (await stream.ReadAsync(ipv4Bytes.AsMemory(0, 4), ct).ConfigureAwait(false) < 4) + if (!await TryReadExactlyAsync(stream, ipv4Bytes, ct).ConfigureAwait(false)) { return null; } @@ -365,7 +316,7 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl case AddrTypeDomain: var lenByte = new byte[1]; - if (await stream.ReadAsync(lenByte.AsMemory(0, 1), ct).ConfigureAwait(false) < 1) + if (!await TryReadExactlyAsync(stream, lenByte, ct).ConfigureAwait(false)) { return null; } @@ -377,8 +328,7 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl else { var domainBytes = new byte[lenByte[0]]; - if (await stream.ReadAsync(domainBytes.AsMemory(0, domainBytes.Length), ct) - .ConfigureAwait(false) < domainBytes.Length) + if (!await TryReadExactlyAsync(stream, domainBytes, ct).ConfigureAwait(false)) { return null; } @@ -390,7 +340,7 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl case AddrTypeIPv6: var ipv6Bytes = new byte[16]; - if (await stream.ReadAsync(ipv6Bytes.AsMemory(0, 16), ct).ConfigureAwait(false) < 16) + if (!await TryReadExactlyAsync(stream, ipv6Bytes, ct).ConfigureAwait(false)) { return null; } @@ -403,7 +353,7 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl } var portBytes = new byte[2]; - if (await stream.ReadAsync(portBytes.AsMemory(0, 2), ct).ConfigureAwait(false) < 2) + if (!await TryReadExactlyAsync(stream, portBytes, ct).ConfigureAwait(false)) { return null; } @@ -419,4 +369,73 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl } #endregion SOCKS5 Address Handling + + #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 TryReadExactlyAsync(tcpControlStream, handshakeResponse, cancellationToken).ConfigureAwait(false) || + 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 TryReadExactlyAsync(tcpControlStream, verRepRsv, cancellationToken).ConfigureAwait(false) || + 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 } diff --git a/v2rayN/ServiceLib.UdpTest/Tester/DnsService.cs b/v2rayN/ServiceLib.UdpTest/Tester/DnsService.cs index 81bea2b8..be5d05f3 100644 --- a/v2rayN/ServiceLib.UdpTest/Tester/DnsService.cs +++ b/v2rayN/ServiceLib.UdpTest/Tester/DnsService.cs @@ -13,7 +13,7 @@ public class DnsService : IUdpTest // Question: www.google.com, Type A, Class IN 0x03, 0x77, 0x77, 0x77, 0x06, 0x67, 0x6F, 0x6F, 0x67, 0x6C, 0x65, 0x03, 0x63, 0x6F, 0x6D, 0x00, - 0x00, 0x01, 0x00, 0x01 + 0x00, 0x01, 0x00, 0x01, ]; public byte[] BuildUdpRequestPacket() diff --git a/v2rayN/ServiceLib.UdpTest/Tester/McBeService.cs b/v2rayN/ServiceLib.UdpTest/Tester/McBeService.cs index b4ec0221..4efcaae9 100644 --- a/v2rayN/ServiceLib.UdpTest/Tester/McBeService.cs +++ b/v2rayN/ServiceLib.UdpTest/Tester/McBeService.cs @@ -17,13 +17,13 @@ public class McBeService : IUdpTest 0xFD, 0xFD, 0xFD, 0xFD, 0x12, 0x34, 0x56, 0x78, // Client GUID (random 16 bytes) 0x66, 0x0E, 0xAB, 0xBC, 0x61, 0x0D, 0x1F, 0x4E, - 0xA4, 0x40, 0x8C, 0x65, 0xC1, 0xBE, 0xF5, 0x4B + 0xA4, 0x40, 0x8C, 0x65, 0xC1, 0xBE, 0xF5, 0x4B, ]; private static readonly byte[] McBeMagicBytes = [ 0x00, 0xFF, 0xFF, 0x00, 0xFE, 0xFE, 0xFE, 0xFE, - 0xFD, 0xFD, 0xFD, 0xFD, 0x12, 0x34, 0x56, 0x78 + 0xFD, 0xFD, 0xFD, 0xFD, 0x12, 0x34, 0x56, 0x78, ]; private static readonly List ValidGameModes = @@ -31,7 +31,7 @@ public class McBeService : IUdpTest "Survival", "Creative", "Adventure", - "Spectator" + "Spectator", ]; public byte[] BuildUdpRequestPacket() @@ -43,9 +43,9 @@ public class McBeService : IUdpTest { // 0x1c | client alive time in ms (recorded from previous ping) | // server GUID | Magic | string length | Edition - // + // // Edition Example: - // + // // MCPE;Dedicated Server;527;1.19.1;0;10;13253860892328930865;Bedrock level;Survival;1;19132;19133; if (mcbeResponseBytes.Length < 48) { @@ -61,6 +61,10 @@ public class McBeService : IUdpTest return false; // Magic bytes do not match } var stringLength = (ushort)((mcbeResponseBytes[33] << 8) | mcbeResponseBytes[34]); + if (mcbeResponseBytes.Length < 35 + stringLength) + { + return false; // Not enough data for the string + } var stringData = Encoding.UTF8.GetString(mcbeResponseBytes.Skip(35).Take(stringLength).ToArray()); var stringParts = stringData.Split(';'); // check Game Mode str diff --git a/v2rayN/ServiceLib.UdpTest/Tester/StunService.cs b/v2rayN/ServiceLib.UdpTest/Tester/StunService.cs index c6b925a3..01ae7f27 100644 --- a/v2rayN/ServiceLib.UdpTest/Tester/StunService.cs +++ b/v2rayN/ServiceLib.UdpTest/Tester/StunService.cs @@ -31,10 +31,7 @@ public class StunService : IUdpTest if (stunResponseBytes.Length >= 2) { var messageType = (stunResponseBytes[0] << 8) | stunResponseBytes[1]; - if (messageType is 0x0101 or 0x0111) - { - return true; - } + return messageType is 0x0101 or 0x0111; } return true; diff --git a/v2rayN/ServiceLib.UdpTest/UdpTestService.cs b/v2rayN/ServiceLib.UdpTest/UdpTestService.cs index 27daeca1..b27aa542 100644 --- a/v2rayN/ServiceLib.UdpTest/UdpTestService.cs +++ b/v2rayN/ServiceLib.UdpTest/UdpTestService.cs @@ -5,7 +5,6 @@ namespace ServiceLib.UdpTest; public class UdpTestService { private const string DefaultUdpTestType = "ntp"; - private readonly IUdpTest _udpTest; private static readonly IReadOnlyDictionary> UdpTestFactories = new Dictionary>(StringComparer.OrdinalIgnoreCase) @@ -16,6 +15,8 @@ public class UdpTestService ["mcbe"] = () => new McBeService(), }; + private readonly IUdpTest _udpTest; + private UdpTestService(IUdpTest udpTest) { _udpTest = udpTest; @@ -88,7 +89,8 @@ public class UdpTestService return (targetServerHost, _udpTest.GetDefaultTargetPort()); } - public async Task SendUdpRequestAsync(string targetServerHost, int socks5Port, CancellationToken ct = default) + public async Task SendUdpRequestAsync(string targetServerHost, int socks5Port, + CancellationToken ct = default) { using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ct, timeoutCts.Token); @@ -106,7 +108,7 @@ public class UdpTestService var (targetHost, targetPort) = ParseHostAndPort(targetServerHost); - byte[] udpReceiveResult = null; + byte[]? validUdpReceiveResult = null; // Get minimum round trip time from two attempts var roundTripTime = TimeSpan.MaxValue; @@ -121,7 +123,11 @@ public class UdpTestService var (_, receiveResult) = await channel.ReceiveAsync(linkedCt).ConfigureAwait(false); stopwatch.Stop(); - udpReceiveResult = receiveResult; + if (!_udpTest.VerifyAndExtractUdpResponse(receiveResult)) + { + continue; + } + validUdpReceiveResult = receiveResult; var currentRoundTripTime = stopwatch.Elapsed; if (currentRoundTripTime < roundTripTime) @@ -129,6 +135,10 @@ public class UdpTestService roundTripTime = currentRoundTripTime; } } + catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested) + { + throw; + } catch { if (attempt == 1 && roundTripTime == TimeSpan.MaxValue) @@ -138,18 +148,10 @@ public class UdpTestService } } - if ((udpReceiveResult?.Length ?? 0) < 4 + 1 + 4 + 2) - { - throw new Exception("Received NTP response is too short."); - } - - if (udpReceiveResult != null && _udpTest.VerifyAndExtractUdpResponse(udpReceiveResult)) + if (validUdpReceiveResult != null) { return roundTripTime; } - else - { - throw new Exception("Failed to verify and extract UDP response."); - } + throw new Exception("Failed to verify and extract UDP response."); } }