Fix udptest (#10199)

* Fix

* Reformat

* Fix tcp read

* Fix verify
This commit is contained in:
DHR60
2026-09-19 06:42:51 +00:00
committed by GitHub
parent 2fbe81041b
commit e6e95e00d7
5 changed files with 142 additions and 120 deletions
+115 -96
View File
@@ -2,14 +2,19 @@ namespace ServiceLib.UdpTest;
public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposable public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposable
{ {
private bool _initialized;
private IPEndPoint _relayEndPoint;
private TcpClient _tcpClient; private TcpClient _tcpClient;
private UdpClient _udpClient; private UdpClient _udpClient;
private IPEndPoint _relayEndPoint;
private bool _initialized = false; public void Dispose()
{
_tcpClient?.Dispose();
_udpClient?.Dispose();
}
/// <summary> /// <summary>
/// Send UDP data to a remote endpoint (IP address) /// Send UDP data to a remote endpoint (IP address)
/// </summary> /// </summary>
public async Task SendAsync(IPEndPoint remote, byte[] data, CancellationToken ct = default) 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.AddrTypeIPv4
: Socks5AddressData.AddrTypeIPv6, : Socks5AddressData.AddrTypeIPv6,
Host = remote.Address.ToString(), Host = remote.Address.ToString(),
Port = (ushort)remote.Port Port = (ushort)remote.Port,
}; };
var packet = BuildSocks5UdpPacket(addrData, data); var packet = BuildSocks5UdpPacket(addrData, data);
await _udpClient.SendAsync(packet.AsMemory(), _relayEndPoint, ct); await _udpClient.SendAsync(packet.AsMemory(), _relayEndPoint, ct);
} }
/// <summary> /// <summary>
/// Send UDP data to a remote endpoint (domain name or IP address) /// Send UDP data to a remote endpoint (domain name or IP address)
/// </summary> /// </summary>
/// <param name="host">Domain name or IP address</param> /// <param name="host">Domain name or IP address</param>
/// <param name="port">Port number</param> /// <param name="port">Port number</param>
@@ -54,12 +59,12 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
addrData.Port = port; addrData.Port = port;
var packet = BuildSocks5UdpPacket(addrData, data); 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); await _udpClient.SendAsync(packet.AsMemory(), _relayEndPoint, ct);
} }
/// <summary> /// <summary>
/// Receive UDP data from remote endpoint /// Receive UDP data from remote endpoint
/// </summary> /// </summary>
/// <param name="cancellationToken">Cancellation token to cancel the receive operation</param> /// <param name="cancellationToken">Cancellation token to cancel the receive operation</param>
/// <returns>Remote endpoint information and received data</returns> /// <returns>Remote endpoint information and received data</returns>
@@ -71,16 +76,6 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
return (remote, payload); 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) private static byte[] BuildSocks5UdpPacket(Socks5AddressData addressData, byte[] data)
{ {
using var ms = new MemoryStream(); 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"); 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; var offset = 0;
// RSV (2 bytes) - Reserved field, skip // RSV (2 bytes) - Reserved field, skip
@@ -198,83 +198,34 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
return (remote, data); return (remote, data);
} }
public void Dispose() /// <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)
{ {
_tcpClient.Dispose(); public string Host { get; set; } = host;
_udpClient.Dispose(); 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<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 #region SOCKS5 Address Handling
private static async Task<bool> TryReadExactlyAsync(
Stream stream,
Memory<byte> buffer,
CancellationToken cancellationToken)
{
try
{
await stream.ReadExactlyAsync(buffer, cancellationToken).ConfigureAwait(false);
return true;
}
catch (EndOfStreamException)
{
return false;
}
}
private class Socks5AddressData private class Socks5AddressData
{ {
public const byte AddrTypeIPv4 = 0x01; public const byte AddrTypeIPv4 = 0x01;
@@ -345,7 +296,7 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
var typeByte = new byte[1]; var typeByte = new byte[1];
try try
{ {
if (await stream.ReadAsync(typeByte.AsMemory(0, 1), ct).ConfigureAwait(false) < 1) if (!await TryReadExactlyAsync(stream, typeByte, ct).ConfigureAwait(false))
{ {
return null; return null;
} }
@@ -355,7 +306,7 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
{ {
case AddrTypeIPv4: case AddrTypeIPv4:
var ipv4Bytes = new byte[4]; 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; return null;
} }
@@ -365,7 +316,7 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
case AddrTypeDomain: case AddrTypeDomain:
var lenByte = new byte[1]; 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; return null;
} }
@@ -377,8 +328,7 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
else else
{ {
var domainBytes = new byte[lenByte[0]]; var domainBytes = new byte[lenByte[0]];
if (await stream.ReadAsync(domainBytes.AsMemory(0, domainBytes.Length), ct) if (!await TryReadExactlyAsync(stream, domainBytes, ct).ConfigureAwait(false))
.ConfigureAwait(false) < domainBytes.Length)
{ {
return null; return null;
} }
@@ -390,7 +340,7 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
case AddrTypeIPv6: case AddrTypeIPv6:
var ipv6Bytes = new byte[16]; 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; return null;
} }
@@ -403,7 +353,7 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
} }
var portBytes = new byte[2]; 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; return null;
} }
@@ -419,4 +369,73 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
} }
#endregion SOCKS5 Address Handling #endregion SOCKS5 Address Handling
#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 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
} }
@@ -13,7 +13,7 @@ public class DnsService : IUdpTest
// Question: www.google.com, Type A, Class IN // Question: www.google.com, Type A, Class IN
0x03, 0x77, 0x77, 0x77, 0x06, 0x67, 0x6F, 0x6F, 0x03, 0x77, 0x77, 0x77, 0x06, 0x67, 0x6F, 0x6F,
0x67, 0x6C, 0x65, 0x03, 0x63, 0x6F, 0x6D, 0x00, 0x67, 0x6C, 0x65, 0x03, 0x63, 0x6F, 0x6D, 0x00,
0x00, 0x01, 0x00, 0x01 0x00, 0x01, 0x00, 0x01,
]; ];
public byte[] BuildUdpRequestPacket() public byte[] BuildUdpRequestPacket()
@@ -17,13 +17,13 @@ public class McBeService : IUdpTest
0xFD, 0xFD, 0xFD, 0xFD, 0x12, 0x34, 0x56, 0x78, 0xFD, 0xFD, 0xFD, 0xFD, 0x12, 0x34, 0x56, 0x78,
// Client GUID (random 16 bytes) // Client GUID (random 16 bytes)
0x66, 0x0E, 0xAB, 0xBC, 0x61, 0x0D, 0x1F, 0x4E, 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 = private static readonly byte[] McBeMagicBytes =
[ [
0x00, 0xFF, 0xFF, 0x00, 0xFE, 0xFE, 0xFE, 0xFE, 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<string> ValidGameModes = private static readonly List<string> ValidGameModes =
@@ -31,7 +31,7 @@ public class McBeService : IUdpTest
"Survival", "Survival",
"Creative", "Creative",
"Adventure", "Adventure",
"Spectator" "Spectator",
]; ];
public byte[] BuildUdpRequestPacket() public byte[] BuildUdpRequestPacket()
@@ -61,6 +61,10 @@ public class McBeService : IUdpTest
return false; // Magic bytes do not match return false; // Magic bytes do not match
} }
var stringLength = (ushort)((mcbeResponseBytes[33] << 8) | mcbeResponseBytes[34]); 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 stringData = Encoding.UTF8.GetString(mcbeResponseBytes.Skip(35).Take(stringLength).ToArray());
var stringParts = stringData.Split(';'); var stringParts = stringData.Split(';');
// check Game Mode str // check Game Mode str
@@ -31,10 +31,7 @@ public class StunService : IUdpTest
if (stunResponseBytes.Length >= 2) if (stunResponseBytes.Length >= 2)
{ {
var messageType = (stunResponseBytes[0] << 8) | stunResponseBytes[1]; var messageType = (stunResponseBytes[0] << 8) | stunResponseBytes[1];
if (messageType is 0x0101 or 0x0111) return messageType is 0x0101 or 0x0111;
{
return true;
}
} }
return true; return true;
+16 -14
View File
@@ -5,7 +5,6 @@ namespace ServiceLib.UdpTest;
public class UdpTestService public class UdpTestService
{ {
private const string DefaultUdpTestType = "ntp"; private const string DefaultUdpTestType = "ntp";
private readonly IUdpTest _udpTest;
private static readonly IReadOnlyDictionary<string, Func<IUdpTest>> UdpTestFactories = private static readonly IReadOnlyDictionary<string, Func<IUdpTest>> UdpTestFactories =
new Dictionary<string, Func<IUdpTest>>(StringComparer.OrdinalIgnoreCase) new Dictionary<string, Func<IUdpTest>>(StringComparer.OrdinalIgnoreCase)
@@ -16,6 +15,8 @@ public class UdpTestService
["mcbe"] = () => new McBeService(), ["mcbe"] = () => new McBeService(),
}; };
private readonly IUdpTest _udpTest;
private UdpTestService(IUdpTest udpTest) private UdpTestService(IUdpTest udpTest)
{ {
_udpTest = udpTest; _udpTest = udpTest;
@@ -88,7 +89,8 @@ public class UdpTestService
return (targetServerHost, _udpTest.GetDefaultTargetPort()); return (targetServerHost, _udpTest.GetDefaultTargetPort());
} }
public async Task<TimeSpan> SendUdpRequestAsync(string targetServerHost, int socks5Port, CancellationToken ct = default) public async Task<TimeSpan> SendUdpRequestAsync(string targetServerHost, int socks5Port,
CancellationToken ct = default)
{ {
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ct, timeoutCts.Token); using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ct, timeoutCts.Token);
@@ -106,7 +108,7 @@ public class UdpTestService
var (targetHost, targetPort) = ParseHostAndPort(targetServerHost); var (targetHost, targetPort) = ParseHostAndPort(targetServerHost);
byte[] udpReceiveResult = null; byte[]? validUdpReceiveResult = null;
// Get minimum round trip time from two attempts // Get minimum round trip time from two attempts
var roundTripTime = TimeSpan.MaxValue; var roundTripTime = TimeSpan.MaxValue;
@@ -121,7 +123,11 @@ public class UdpTestService
var (_, receiveResult) = await channel.ReceiveAsync(linkedCt).ConfigureAwait(false); var (_, receiveResult) = await channel.ReceiveAsync(linkedCt).ConfigureAwait(false);
stopwatch.Stop(); stopwatch.Stop();
udpReceiveResult = receiveResult; if (!_udpTest.VerifyAndExtractUdpResponse(receiveResult))
{
continue;
}
validUdpReceiveResult = receiveResult;
var currentRoundTripTime = stopwatch.Elapsed; var currentRoundTripTime = stopwatch.Elapsed;
if (currentRoundTripTime < roundTripTime) if (currentRoundTripTime < roundTripTime)
@@ -129,6 +135,10 @@ public class UdpTestService
roundTripTime = currentRoundTripTime; roundTripTime = currentRoundTripTime;
} }
} }
catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested)
{
throw;
}
catch catch
{ {
if (attempt == 1 && roundTripTime == TimeSpan.MaxValue) if (attempt == 1 && roundTripTime == TimeSpan.MaxValue)
@@ -138,18 +148,10 @@ public class UdpTestService
} }
} }
if ((udpReceiveResult?.Length ?? 0) < 4 + 1 + 4 + 2) if (validUdpReceiveResult != null)
{
throw new Exception("Received NTP response is too short.");
}
if (udpReceiveResult != null && _udpTest.VerifyAndExtractUdpResponse(udpReceiveResult))
{ {
return roundTripTime; return roundTripTime;
} }
else throw new Exception("Failed to verify and extract UDP response.");
{
throw new Exception("Failed to verify and extract UDP response.");
}
} }
} }