From de1b3049016cc8456904cb81ce23f103dea3d40b Mon Sep 17 00:00:00 2001 From: Bruce Wayne Date: Thu, 12 Aug 2021 14:36:44 +0800 Subject: [PATCH] test: Add SOCKS5 UDP test --- .github/workflows/CI.yml | 2 +- .../ListenServices/UdpListenService.cs | 1 + .../UdpClients/ShadowsocksUdpClient.cs | 1 + Socks5/Clients/Socks5Client.cs | 28 +++-- Socks5/Models/Socks5UdpReceivePacket.cs | 7 +- ...{Socks5Server.cs => SimpleSocks5Server.cs} | 18 ++- Socks5/Servers/SimpleSocks5UdpServer.cs | 107 ++++++++++++++++++ Socks5/Utils/Socks5TestUtils.cs | 4 +- Socks5/Utils/Unpack.cs | 21 ++-- UnitTest/Socks5Test.cs | 38 ++++++- 10 files changed, 195 insertions(+), 32 deletions(-) rename Socks5/Servers/{Socks5Server.cs => SimpleSocks5Server.cs} (85%) create mode 100644 Socks5/Servers/SimpleSocks5UdpServer.cs diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 2e3b4f5..5f51aeb 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -38,7 +38,7 @@ jobs: - name: Test shell: pwsh - run: dotnet test -c Release --filter ClassName!~Socks5 + run: dotnet test -c Release build: name: Build diff --git a/Shadowsocks.Protocol/ListenServices/UdpListenService.cs b/Shadowsocks.Protocol/ListenServices/UdpListenService.cs index 6eb2953..1f34ea1 100644 --- a/Shadowsocks.Protocol/ListenServices/UdpListenService.cs +++ b/Shadowsocks.Protocol/ListenServices/UdpListenService.cs @@ -9,6 +9,7 @@ namespace Shadowsocks.Protocol.ListenServices { + //TODO Remake public class UdpListenService : IListenService { private readonly ILogger _logger; diff --git a/Shadowsocks.Protocol/UdpClients/ShadowsocksUdpClient.cs b/Shadowsocks.Protocol/UdpClients/ShadowsocksUdpClient.cs index 3b08381..3fcf827 100644 --- a/Shadowsocks.Protocol/UdpClients/ShadowsocksUdpClient.cs +++ b/Shadowsocks.Protocol/UdpClients/ShadowsocksUdpClient.cs @@ -9,6 +9,7 @@ namespace Shadowsocks.Protocol.UdpClients { + //TODO Remake public class ShadowsocksUdpClient : IUdpClient { private readonly ILogger _logger; diff --git a/Socks5/Clients/Socks5Client.cs b/Socks5/Clients/Socks5Client.cs index b3aea36..55d1109 100644 --- a/Socks5/Clients/Socks5Client.cs +++ b/Socks5/Clients/Socks5Client.cs @@ -126,29 +126,37 @@ public async ValueTask UdpAssociateAsync(IPAddress address, ushort return bound; } - //TODO .NET6.0 - public async Task ReceiveAsync() + public async Task ReceiveAsync(CancellationToken token = default) { Verify.Operation(Status is Status.Established && _udpClient is not null, @"Socks5 is not established."); - var res = await _udpClient.ReceiveAsync(); + var buffer = ArrayPool.Shared.Rent(0x10000); + try + { + var length = await _udpClient.Client.ReceiveAsync(buffer, SocketFlags.None, token); - return Unpack.Udp(res.Buffer); + return Unpack.Udp(buffer.AsMemory(0, length)); + } + finally + { + ArrayPool.Shared.Return(buffer); + } } - public Task SendUdpAsync(ReadOnlyMemory data, string dst, ushort dstPort) + public Task SendUdpAsync(ReadOnlyMemory data, string dst, ushort dstPort, CancellationToken token = default) { - return SendUdpAsync(data, dst, default, dstPort); + return SendUdpAsync(data, dst, default, dstPort, token); } - public Task SendUdpAsync(ReadOnlyMemory data, IPAddress dstAddress, ushort dstPort) + public Task SendUdpAsync(ReadOnlyMemory data, IPAddress dstAddress, ushort dstPort, CancellationToken token = default) { - return SendUdpAsync(data, default, dstAddress, dstPort); + return SendUdpAsync(data, default, dstAddress, dstPort, token); } private async Task SendUdpAsync( ReadOnlyMemory data, - string? dst, IPAddress? dstAddress, ushort dstPort) + string? dst, IPAddress? dstAddress, ushort dstPort, + CancellationToken token = default) { Verify.Operation(Status is Status.Established && _udpClient is not null, @"Socks5 is not established."); @@ -157,7 +165,7 @@ private async Task SendUdpAsync( { var length = Pack.Udp(buffer, dst, dstAddress, dstPort, data.Span); - return await _udpClient.SendAsync(buffer, length); + return await _udpClient.Client.SendAsync(buffer.AsMemory(0, length), SocketFlags.None, token); } finally { diff --git a/Socks5/Models/Socks5UdpReceivePacket.cs b/Socks5/Models/Socks5UdpReceivePacket.cs index da74842..f7c16df 100644 --- a/Socks5/Models/Socks5UdpReceivePacket.cs +++ b/Socks5/Models/Socks5UdpReceivePacket.cs @@ -4,13 +4,18 @@ namespace Socks5.Models { + // +----+------+------+----------+----------+----------+ + // |RSV | FRAG | ATYP | DST.ADDR | DST.PORT | DATA | + // +----+------+------+----------+----------+----------+ + // | 2 | 1 | 1 | Variable | 2 | Variable | + // +----+------+------+----------+----------+----------+ public struct Socks5UdpReceivePacket { - public Memory Data; public byte Fragment; public AddressType Type; public IPAddress? Address; public string? Domain; public ushort Port; + public Memory Data; } } diff --git a/Socks5/Servers/Socks5Server.cs b/Socks5/Servers/SimpleSocks5Server.cs similarity index 85% rename from Socks5/Servers/Socks5Server.cs rename to Socks5/Servers/SimpleSocks5Server.cs index 8d6308a..ece0d01 100644 --- a/Socks5/Servers/Socks5Server.cs +++ b/Socks5/Servers/SimpleSocks5Server.cs @@ -11,7 +11,10 @@ namespace Socks5.Servers { - public class Socks5Server + /// + /// A simple SOCKS5 server for test use only + /// + public class SimpleSocks5Server { public TcpListener TcpListener { get; } @@ -23,10 +26,18 @@ public class Socks5Server Port = IPEndPoint.MinPort, }; + public ServerBound ReplyUdpBound { get; set; } = new() + { + Type = AddressType.IPv4, + Address = IPAddress.Any, + Domain = default, + Port = IPEndPoint.MinPort, + }; + private readonly UsernamePassword? _credential; private readonly CancellationTokenSource _cts; - public Socks5Server(IPEndPoint bindEndPoint, UsernamePassword? credential = null) + public SimpleSocks5Server(IPEndPoint bindEndPoint, UsernamePassword? credential = null) { _credential = credential; TcpListener = new TcpListener(bindEndPoint); @@ -93,8 +104,7 @@ private async ValueTask HandleAsync(TcpClient rec, CancellationToken token) } case Command.UdpAssociate: { - //TODO - await service.SendReplyAsync(Socks5Reply.CommandNotSupported, ReplyTcpBound, token); + await service.SendReplyAsync(Socks5Reply.Succeeded, ReplyUdpBound, token); break; } default: diff --git a/Socks5/Servers/SimpleSocks5UdpServer.cs b/Socks5/Servers/SimpleSocks5UdpServer.cs new file mode 100644 index 0000000..5d2d472 --- /dev/null +++ b/Socks5/Servers/SimpleSocks5UdpServer.cs @@ -0,0 +1,107 @@ +using Microsoft; +using Microsoft.VisualStudio.Threading; +using Socks5.Enums; +using Socks5.Utils; +using System; +using System.Buffers; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +namespace Socks5.Servers +{ + /// + /// Just for test use only + /// + public class SimpleSocks5UdpServer + { + public UdpClient UdpListener { get; } + + private readonly CancellationTokenSource _cts; + + public SimpleSocks5UdpServer(IPEndPoint bindEndPoint) + { + UdpListener = new UdpClient(bindEndPoint); + + _cts = new CancellationTokenSource(); + } + + public async ValueTask StartAsync() + { + try + { + while (!_cts.IsCancellationRequested) + { + //TODO .NET6.0 + var message = await UdpListener.ReceiveAsync(); + + HandleAsync(message, _cts.Token).Forget(); + } + } + catch (Exception) + { + Stop(); + } + } + + private async ValueTask HandleAsync(UdpReceiveResult result, CancellationToken token) + { + if (token.IsCancellationRequested) + { + return; + } + + var socks5UdpPacket = Unpack.Udp(result.Buffer); + if (socks5UdpPacket.Fragment is not 0x00) + { + return; // Ignore + } + + UdpClient client; + if (socks5UdpPacket.Type is AddressType.Domain) + { + Assumes.NotNull(socks5UdpPacket.Domain); + client = new UdpClient(socks5UdpPacket.Domain, socks5UdpPacket.Port); + } + else + { + Assumes.NotNull(socks5UdpPacket.Address); + client = new UdpClient(socks5UdpPacket.Address.AddressFamily); + client.Connect(socks5UdpPacket.Address, socks5UdpPacket.Port); + } + + await client.Client.SendAsync(socks5UdpPacket.Data, SocketFlags.None, token); + + var headerLength = result.Buffer.Length - socks5UdpPacket.Data.Length; + var receiveBuffer = ArrayPool.Shared.Rent(0x10000); + try + { + var receiveLength = await client.Client.ReceiveAsync(receiveBuffer.AsMemory(headerLength), SocketFlags.None, token); + result.Buffer.AsSpan(0, headerLength).CopyTo(receiveBuffer); + + //TODO .NET6.0 + await UdpListener.Client.SendToAsync( + new ArraySegment(receiveBuffer, 0, headerLength + receiveLength), + SocketFlags.None, + result.RemoteEndPoint); + } + finally + { + ArrayPool.Shared.Return(receiveBuffer); + } + } + + public void Stop() + { + try + { + UdpListener.Dispose(); + } + finally + { + _cts.Cancel(); + } + } + } +} diff --git a/Socks5/Utils/Socks5TestUtils.cs b/Socks5/Utils/Socks5TestUtils.cs index 3d36367..f219955 100644 --- a/Socks5/Utils/Socks5TestUtils.cs +++ b/Socks5/Utils/Socks5TestUtils.cs @@ -124,9 +124,9 @@ public static async ValueTask Socks5UdpAssociateAsync( buffer[offset++] = 0x00; buffer[offset++] = 0x01; - await client.SendUdpAsync(buffer.AsMemory(0, offset), targetHost, targetPort); + await client.SendUdpAsync(buffer.AsMemory(0, offset), targetHost, targetPort, token); - var res = await client.ReceiveAsync(); + var res = await client.ReceiveAsync(token); return res.Data.Span[..2].SequenceEqual(buffer.AsSpan(0, 2)); } diff --git a/Socks5/Utils/Unpack.cs b/Socks5/Utils/Unpack.cs index b461f55..ae3ff8a 100644 --- a/Socks5/Utils/Unpack.cs +++ b/Socks5/Utils/Unpack.cs @@ -7,6 +7,7 @@ using System.Buffers.Binary; using System.Collections.Generic; using System.Net; +using System.Runtime.InteropServices; using System.Text; namespace Socks5.Utils @@ -117,7 +118,7 @@ public static int DestinationAddress(AddressType type, ReadOnlySpan bytes, return offset; } - public static Socks5UdpReceivePacket Udp(byte[] buffer) + public static Socks5UdpReceivePacket Udp(ReadOnlyMemory buffer) { // +----+------+------+----------+----------+----------+ // |RSV | FRAG | ATYP | DST.ADDR | DST.PORT | DATA | @@ -125,24 +126,26 @@ public static Socks5UdpReceivePacket Udp(byte[] buffer) // | 2 | 1 | 1 | Variable | 2 | Variable | // +----+------+------+----------+----------+----------+ - Requires.Range(buffer.LongLength >= 7, nameof(buffer)); + var span = buffer.Span; + Requires.Range(buffer.Length >= 7, nameof(buffer)); var res = new Socks5UdpReceivePacket(); - if (buffer[0] != Constants.Rsv || buffer[1] != Constants.Rsv) + if (span[0] is not Constants.Rsv || span[1] is not Constants.Rsv) { - throw new ProtocolErrorException($@"Protocol failed, RESERVED is not 0x0000: 0x{buffer[0]:X2}{buffer[1]:X2}."); + throw new ProtocolErrorException($@"Protocol failed, RESERVED is not 0x0000: 0x{span[0]:X2}{span[1]:X2}."); } - res.Fragment = buffer[2]; + res.Fragment = span[2]; - res.Type = (AddressType)buffer[3]; + res.Type = (AddressType)span[3]; + Requires.Defined(res.Type, nameof(res.Type)); var offset = 4; - offset += DestinationAddress(res.Type, buffer.AsSpan(offset), out res.Address, out res.Domain); + offset += DestinationAddress(res.Type, span[offset..], out res.Address, out res.Domain); - res.Port = BinaryPrimitives.ReadUInt16BigEndian(buffer.AsSpan(offset)); - res.Data = buffer.AsMemory(offset + 2); + res.Port = BinaryPrimitives.ReadUInt16BigEndian(span[offset..]); + res.Data = MemoryMarshal.AsMemory(buffer[(offset + 2)..]); return res; } diff --git a/UnitTest/Socks5Test.cs b/UnitTest/Socks5Test.cs index d9fb934..bde6840 100644 --- a/UnitTest/Socks5Test.cs +++ b/UnitTest/Socks5Test.cs @@ -1,5 +1,6 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.Threading; +using Socks5.Enums; using Socks5.Models; using Socks5.Servers; using Socks5.Utils; @@ -20,7 +21,7 @@ public async Task ConnectTestAsync() UserName = @"114514!", Password = @"1919810¥" }; - var server = new Socks5Server(serverEndpoint, userPass); + var server = new SimpleSocks5Server(serverEndpoint, userPass); server.StartAsync().Forget(); try { @@ -42,12 +43,39 @@ public async Task ConnectTestAsync() [TestMethod] public async Task UdpAssociateTestAsync() { - var option = new Socks5CreateOption + var serverEndpoint = new IPEndPoint(IPAddress.Loopback, 0); + var userPass = new UsernamePassword + { + UserName = @"114514!", + Password = @"1919810¥" + }; + var server = new SimpleSocks5Server(serverEndpoint, userPass); + var udpServer = new SimpleSocks5UdpServer(serverEndpoint); + udpServer.StartAsync().Forget(); + server.ReplyUdpBound = new ServerBound { - Address = IPAddress.Loopback, - Port = 23333 + Type = AddressType.IPv4, + Address = ((IPEndPoint)udpServer.UdpListener.Client.LocalEndPoint!).Address, + Domain = default, + Port = (ushort)((IPEndPoint)udpServer.UdpListener.Client.LocalEndPoint!).Port, }; - Assert.IsTrue(await Socks5TestUtils.Socks5UdpAssociateAsync(option)); + server.StartAsync().Forget(); + try + { + var port = (ushort)((IPEndPoint)server.TcpListener.LocalEndpoint).Port; + var option = new Socks5CreateOption + { + Address = IPAddress.Loopback, + Port = port, + UsernamePassword = userPass + }; + Assert.IsTrue(await Socks5TestUtils.Socks5UdpAssociateAsync(option)); + } + finally + { + server.Stop(); + udpServer.Stop(); + } } } }