diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b881dd2b..ef59a843 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,47 +7,122 @@ on: - 'rm' paths-ignore: - 'README.md' - - 'LICENSE' + - 'LICENSE.txt' pull_request: branches-ignore: - 'v4' - 'rm' paths-ignore: - 'README.md' - - 'LICENSE' + - 'LICENSE.txt' jobs: build: name: Build - runs-on: windows-latest + strategy: + matrix: + os: [ubuntu-20.04, windows-latest] + fail-fast: false + runs-on: ${{ matrix.os }} + defaults: + run: + shell: bash steps: - uses: actions/checkout@v2 - name: Restore dependencies + if: matrix.os == 'windows-latest' run: dotnet restore - name: Build + if: matrix.os == 'windows-latest' run: dotnet build --no-restore - name: Test + if: matrix.os == 'windows-latest' run: dotnet test --no-build --verbosity normal - # Publish - - name: Publish framework-dependent + # Publish CLI + - name: Define MSBuild properties + run: echo "MSBUILD_PROPS=-p:PublishSingleFile=true -p:PublishTrimmed=true -p:TrimMode=link -p:DebuggerSupport=false -p:EnableUnsafeBinaryFormatterSerialization=false -p:EnableUnsafeUTF7Encoding=false -p:InvariantGlobalization=true" >> $GITHUB_ENV + - name: Publish CLI framework-dependent + run: | + dotnet publish Shadowsocks.CLI -c Release + - name: Publish CLI self-contained for Linux ARM64 + if: matrix.os == 'ubuntu-20.04' + run: | + dotnet publish Shadowsocks.CLI -c Release $MSBUILD_PROPS -r linux-arm64 --self-contained + - name: Publish CLI self-contained for Linux x64 + if: matrix.os == 'ubuntu-20.04' + run: | + dotnet publish Shadowsocks.CLI -c Release $MSBUILD_PROPS -r linux-x64 --self-contained + - name: Publish CLI self-contained for Windows ARM64 + if: matrix.os == 'windows-latest' + run: | + dotnet publish Shadowsocks.CLI -c Release $MSBUILD_PROPS -r win-arm64 --self-contained + - name: Publish CLI self-contained for Windows x64 + if: matrix.os == 'windows-latest' + run: | + dotnet publish Shadowsocks.CLI -c Release $MSBUILD_PROPS -r win-x64 --self-contained + # Publish WPF + - name: Publish WPF framework-dependent + if: matrix.os == 'windows-latest' run: dotnet publish Shadowsocks.WPF -c Release --no-restore - # - name: Publish self-contained for Windows ARM64 + # - name: Publish WPF self-contained for Windows ARM64 + # if: matrix.os == 'windows-latest' # run: dotnet publish Shadowsocks.WPF -c Release -r win-arm64 --self-contained - - name: Publish self-contained for Windows x64 + - name: Publish WPF self-contained for Windows x64 + if: matrix.os == 'windows-latest' run: dotnet publish Shadowsocks.WPF -c Release -r win-x64 --self-contained - # Upload - # - name: Upload artifacts for Windows ARM64 + # Upload CLI + - name: Upload CLI artifacts for Linux ARM64 + if: matrix.os == 'ubuntu-20.04' + uses: actions/upload-artifact@v2 + with: + name: shadowsocks-cli-${{ github.sha }}-linux-arm64 + path: Shadowsocks.CLI/bin/Release/net5.0/linux-arm64/publish/ + - name: Upload CLI artifacts for Linux x64 + if: matrix.os == 'ubuntu-20.04' + uses: actions/upload-artifact@v2 + with: + name: shadowsocks-cli-${{ github.sha }}-linux-x64 + path: Shadowsocks.CLI/bin/Release/net5.0/linux-x64/publish/ + - name: Upload CLI artifacts for Linux framework-dependent + if: matrix.os == 'ubuntu-20.04' + uses: actions/upload-artifact@v2 + with: + name: shadowsocks-cli-${{ github.sha }}-linux + path: Shadowsocks.CLI/bin/Release/net5.0/publish/ + - name: Upload CLI artifacts for Windows ARM64 + if: matrix.os == 'windows-latest' + uses: actions/upload-artifact@v2 + with: + name: shadowsocks-cli-${{ github.sha }}-windows-arm64 + path: Shadowsocks.CLI/bin/Release/net5.0/win-arm64/publish/ + - name: Upload CLI artifacts for Windows x64 + if: matrix.os == 'windows-latest' + uses: actions/upload-artifact@v2 + with: + name: shadowsocks-cli-${{ github.sha }}-windows-x64 + path: Shadowsocks.CLI/bin/Release/net5.0/win-x64/publish/ + - name: Upload CLI artifacts for Windows framework-dependent + if: matrix.os == 'windows-latest' + uses: actions/upload-artifact@v2 + with: + name: shadowsocks-cli-${{ github.sha }}-windows + path: Shadowsocks.CLI/bin/Release/net5.0/publish/ + # Upload WPF + # - name: Upload WPF artifacts for Windows ARM64 + # if: matrix.os == 'windows-latest' # uses: actions/upload-artifact@v2 # with: # name: shadowsocks-wpf-${{ github.sha }}-windows-arm64 # path: Shadowsocks.WPF/bin/Release/net5.0-windows10.0.19041.0/win-arm64/publish/ - - name: Upload artifacts for Windows x64 + - name: Upload WPF artifacts for Windows x64 + if: matrix.os == 'windows-latest' uses: actions/upload-artifact@v2 with: name: shadowsocks-wpf-${{ github.sha }}-windows-x64 path: Shadowsocks.WPF/bin/Release/net5.0-windows10.0.19041.0/win-x64/publish/ - - name: Upload artifacts for Windows framework-dependent + - name: Upload WPF artifacts for Windows framework-dependent + if: matrix.os == 'windows-latest' uses: actions/upload-artifact@v2 with: name: shadowsocks-wpf-${{ github.sha }}-windows diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c098fc2d..fd829e76 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,22 +8,57 @@ on: jobs: publish_upload: name: Publish and upload - runs-on: windows-latest + strategy: + matrix: + os: [ubuntu-20.04, windows-latest] + fail-fast: false + runs-on: ${{ matrix.os }} + defaults: + run: + shell: bash steps: - uses: actions/checkout@v2 - name: Restore dependencies + if: matrix.os == 'windows-latest' run: dotnet restore - name: Build + if: matrix.os == 'windows-latest' run: dotnet build --no-restore - name: Test + if: matrix.os == 'windows-latest' run: dotnet test --no-build --verbosity normal - # Publish - - name: Publish framework-dependent + # Publish CLI + - name: Define MSBuild properties + run: echo "MSBUILD_PROPS=-p:PublishSingleFile=true -p:PublishTrimmed=true -p:TrimMode=link -p:DebuggerSupport=false -p:EnableUnsafeBinaryFormatterSerialization=false -p:EnableUnsafeUTF7Encoding=false -p:InvariantGlobalization=true" >> $GITHUB_ENV + - name: Publish CLI framework-dependent + run: | + dotnet publish Shadowsocks.CLI -c Release + - name: Publish CLI self-contained for Linux ARM64 + if: matrix.os == 'ubuntu-20.04' + run: | + dotnet publish Shadowsocks.CLI -c Release $MSBUILD_PROPS -r linux-arm64 --self-contained + - name: Publish CLI self-contained for Linux x64 + if: matrix.os == 'ubuntu-20.04' + run: | + dotnet publish Shadowsocks.CLI -c Release $MSBUILD_PROPS -r linux-x64 --self-contained + - name: Publish CLI self-contained for Windows ARM64 + if: matrix.os == 'windows-latest' + run: | + dotnet publish Shadowsocks.CLI -c Release $MSBUILD_PROPS -r win-arm64 --self-contained + - name: Publish CLI self-contained for Windows x64 + if: matrix.os == 'windows-latest' + run: | + dotnet publish Shadowsocks.CLI -c Release $MSBUILD_PROPS -r win-x64 --self-contained + # Publish WPF + - name: Publish WPF framework-dependent + if: matrix.os == 'windows-latest' run: dotnet publish Shadowsocks.WPF -c Release --no-restore - # - name: Publish self-contained for Windows ARM64 + # - name: Publish WPF self-contained for Windows ARM64 + # if: matrix.os == 'windows-latest' # run: dotnet publish Shadowsocks.WPF -c Release -r win-arm64 --self-contained - - name: Publish self-contained for Windows x64 + - name: Publish WPF self-contained for Windows x64 + if: matrix.os == 'windows-latest' run: dotnet publish Shadowsocks.WPF -c Release -r win-x64 --self-contained # Get version - name: Get version @@ -31,8 +66,23 @@ jobs: shell: bash run: echo ::set-output name=VERSION::$(echo $GITHUB_REF | cut -d / -f 3) # Package + - name: Package for Linux + if: matrix.os == 'ubuntu-20.04' + env: + ZSTD_CLEVEL: 19 + ZSTD_NBTHREADS: 2 + run: | + # Shadowsocks.CLI + cd Shadowsocks.CLI/bin/Release/net5.0/publish + tar -acf ../shadowsocks-cli-${{ steps.get_version.outputs.VERSION }}-linux.tar.zst . + cd ../linux-arm64/publish + tar -acf ../../shadowsocks-cli-${{ steps.get_version.outputs.VERSION }}-linux-arm64.tar.zst . + cd ../../linux-x64/publish + tar -acf ../../shadowsocks-cli-${{ steps.get_version.outputs.VERSION }}-linux-x64.tar.zst . - name: Package for Windows + if: matrix.os == 'windows-latest' run: | + # WPF cd Shadowsocks.WPF/bin/Release/net5.0-windows10.0.19041.0/publish 7z a -tzip -mx=9 -mfb=128 ../shadowsocks-wpf-${{ steps.get_version.outputs.VERSION }}-windows.zip . 7z a -t7z -m0=lzma2 -mx=9 -mfb=64 -md=64m -ms=on ../shadowsocks-wpf-${{ steps.get_version.outputs.VERSION }}-windows.7z . @@ -42,8 +92,37 @@ jobs: cd ../../win-x64/publish 7z a -tzip -mx=9 -mfb=128 ../../shadowsocks-wpf-${{ steps.get_version.outputs.VERSION }}-windows-x64.zip . 7z a -t7z -m0=lzma2 -mx=9 -mfb=64 -md=64m -ms=on ../../shadowsocks-wpf-${{ steps.get_version.outputs.VERSION }}-windows-x64.7z . + # CLI + cd ../../../../../Shadowsocks.CLI/bin/Release/net5.0/publish + 7z a -tzip -mx=9 -mfb=128 ../shadowsocks-cli-${{ steps.get_version.outputs.VERSION }}-windows.zip . + 7z a -t7z -m0=lzma2 -mx=9 -mfb=64 -md=64m -ms=on ../shadowsocks-cli-${{ steps.get_version.outputs.VERSION }}-windows.7z . + cd ../win-arm64/publish + 7z a -tzip -mx=9 -mfb=128 ../../shadowsocks-cli-${{ steps.get_version.outputs.VERSION }}-windows-arm64.zip . + 7z a -t7z -m0=lzma2 -mx=9 -mfb=64 -md=64m -ms=on ../../shadowsocks-cli-${{ steps.get_version.outputs.VERSION }}-windows-arm64.7z . + cd ../../win-x64/publish + 7z a -tzip -mx=9 -mfb=128 ../../shadowsocks-cli-${{ steps.get_version.outputs.VERSION }}-windows-x64.zip . + 7z a -t7z -m0=lzma2 -mx=9 -mfb=64 -md=64m -ms=on ../../shadowsocks-cli-${{ steps.get_version.outputs.VERSION }}-windows-x64.7z . # Release - - name: Upload release assets for Windows + - name: Upload release assets for Linux + uses: svenstaro/upload-release-action@v2 + if: matrix.os == 'ubuntu-20.04' + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: Shadowsocks.CLI/bin/Release/net5.0/*.tar.zst + tag: ${{ github.ref }} + file_glob: true + prerelease: true + - name: Upload CLI release assets for Windows + if: matrix.os == 'windows-latest' + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: Shadowsocks.CLI/bin/Release/net5.0/shadowsocks-wpf-* + tag: ${{ github.ref }} + file_glob: true + prerelease: true + - name: Upload WPF release assets for Windows + if: matrix.os == 'windows-latest' uses: svenstaro/upload-release-action@v2 with: repo_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/Shadowsocks.CLI/Program.cs b/Shadowsocks.CLI/Program.cs new file mode 100644 index 00000000..eba20fd2 --- /dev/null +++ b/Shadowsocks.CLI/Program.cs @@ -0,0 +1,65 @@ +using Shadowsocks.Protocol; +using System; +using System.CommandLine; +using System.CommandLine.Invocation; +using System.Net; +using System.Text; +using System.Threading.Tasks; + +namespace Shadowsocks.CLI +{ + internal class Program + { + private static Task Main(string[] args) + { + var clientCommand = new Command("client", "Shadowsocks client."); + clientCommand.AddAlias("c"); + clientCommand.AddOption(new Option("--listen", "The address and port to listen on for both SOCKS5 and HTTP proxy.")); + clientCommand.AddOption(new Option("--listen-socks", "The address and port to listen on for SOCKS5 proxy.")); + clientCommand.AddOption(new Option("--listen-http", "The address and port to listen on for HTTP proxy.")); + clientCommand.AddOption(new Option("--server-address", "Address of the remote Shadowsocks server to connect to.")); + clientCommand.AddOption(new Option("--server-port", "Port of the remote Shadowsocks server to connect to.")); + clientCommand.AddOption(new Option("--method", "Encryption method to use for the remote Shadowsocks server.")); + clientCommand.AddOption(new Option("--password", "Password to use for the remote Shadowsocks server.")); + clientCommand.AddOption(new Option("--key", "Encryption key (NOT password!) to use for the remote Shadowsocks server.")); + clientCommand.AddOption(new Option("--plugin", "Plugin binary path.")); + clientCommand.AddOption(new Option("--plugin-opts", "Plugin options.")); + clientCommand.AddOption(new Option("--plugin-args", "Plugin startup arguments.")); + clientCommand.Handler = CommandHandler.Create( + async (string? listen, string? listenSocks, string? listenHttp, string serverAddress, int serverPort, string method, string? password, string? key, string? plugin, string? pluginOpts, string? pluginArgs) => + { + // TODO + var localEP = IPEndPoint.Parse(listenSocks); + var remoteEp = new DnsEndPoint(serverAddress, serverPort); + byte[]? mainKey = null; + if (!string.IsNullOrEmpty(key)) + mainKey = Encoding.UTF8.GetBytes(key); + var tcpPipeListener = new TcpPipeListener(localEP); + tcpPipeListener.Start(localEP, remoteEp, method, password, mainKey).Wait(); + }); + + var serverCommand = new Command("server", "Shadowsocks server."); + serverCommand.AddAlias("s"); + serverCommand.Handler = CommandHandler.Create( + () => + { + Console.WriteLine("Not implemented."); + }); + + var utilitiesCommand = new Command("utilities", "Shadowsocks-related utilities."); + utilitiesCommand.AddAlias("u"); + utilitiesCommand.AddAlias("util"); + utilitiesCommand.AddAlias("utils"); + + var rootCommand = new RootCommand("CLI for Shadowsocks server and client implementation in C#.") + { + clientCommand, + serverCommand, + utilitiesCommand, + }; + + Console.OutputEncoding = Encoding.UTF8; + return rootCommand.InvokeAsync(args); + } + } +} diff --git a/Shadowsocks.CLI/Shadowsocks.CLI.csproj b/Shadowsocks.CLI/Shadowsocks.CLI.csproj new file mode 100644 index 00000000..e40c8074 --- /dev/null +++ b/Shadowsocks.CLI/Shadowsocks.CLI.csproj @@ -0,0 +1,40 @@ + + + + Exe + net5.0 + enable + sscli + Shadowsocks.CLI + Clowwindy & The Community + Shadowsocks CLI + CLI for Shadowsocks server and client implementation in C#. + © 2021 Clowwindy & The Community + LICENSE.txt + https://github.com/shadowsocks/shadowsocks-windows + https://github.com/shadowsocks/shadowsocks-windows + Public + ssw128.png + + + + + True + + + + True + + + + + + + + + + + + + + diff --git a/Shadowsocks.Protocol/Direct/PortForwardService.cs b/Shadowsocks.Protocol/Direct/PortForwardService.cs new file mode 100644 index 00000000..24398538 --- /dev/null +++ b/Shadowsocks.Protocol/Direct/PortForwardService.cs @@ -0,0 +1,12 @@ +using System.IO.Pipelines; +using System.Threading.Tasks; + +namespace Shadowsocks.Protocol.Direct +{ + public class PortForwardService : IStreamService + { + public async Task Handle(IDuplexPipe pipe) => await Task.FromResult(null); + + public Task IsMyClient(IDuplexPipe pipe) => Task.FromResult(true); + } +} diff --git a/Shadowsocks.Protocol/DuplexPipe.cs b/Shadowsocks.Protocol/DuplexPipe.cs new file mode 100644 index 00000000..e10fb556 --- /dev/null +++ b/Shadowsocks.Protocol/DuplexPipe.cs @@ -0,0 +1,19 @@ +using System.IO.Pipelines; +using System.Threading.Tasks; + +namespace Shadowsocks.Protocol +{ + class DuplexPipe : IDuplexPipe + { + public PipeReader Input { get; set; } + public PipeWriter Output { get; set; } + + public static Task CopyDuplexPipe(IDuplexPipe p1, IDuplexPipe p2) + { + var t1 = p1.Input.CopyToAsync(p2.Output); + var t2 = p2.Input.CopyToAsync(p1.Output); + + return Task.WhenAll(t1, t2); + } + } +} diff --git a/Shadowsocks.Protocol/IProtocolMessage.cs b/Shadowsocks.Protocol/IProtocolMessage.cs new file mode 100644 index 00000000..7b3b7fa4 --- /dev/null +++ b/Shadowsocks.Protocol/IProtocolMessage.cs @@ -0,0 +1,18 @@ +using System; + +namespace Shadowsocks.Protocol +{ + public interface IProtocolMessage : IEquatable + { + public int Serialize(Memory buffer); + /// + /// + /// + /// + /// Tuple represent load state, + /// when success, length is how many byte has been taken + /// when fail, length is how many byte required, 0 is parse error + /// + public (bool success, int length) TryLoad(ReadOnlyMemory buffer); + } +} \ No newline at end of file diff --git a/Shadowsocks.Protocol/IStreamClient.cs b/Shadowsocks.Protocol/IStreamClient.cs new file mode 100644 index 00000000..e80d46ca --- /dev/null +++ b/Shadowsocks.Protocol/IStreamClient.cs @@ -0,0 +1,11 @@ +using System.IO.Pipelines; +using System.Net; +using System.Threading.Tasks; + +namespace Shadowsocks.Protocol +{ + interface IStreamClient + { + Task Connect(EndPoint destination, IDuplexPipe client, IDuplexPipe server); + } +} \ No newline at end of file diff --git a/Shadowsocks.Protocol/IStreamService.cs b/Shadowsocks.Protocol/IStreamService.cs new file mode 100644 index 00000000..c7cc63b8 --- /dev/null +++ b/Shadowsocks.Protocol/IStreamService.cs @@ -0,0 +1,11 @@ +using System.IO.Pipelines; +using System.Threading.Tasks; + +namespace Shadowsocks.Protocol +{ + public interface IStreamService + { + Task IsMyClient(IDuplexPipe pipe); + Task Handle(IDuplexPipe pipe); + } +} diff --git a/Shadowsocks.Protocol/PipePair.cs b/Shadowsocks.Protocol/PipePair.cs new file mode 100644 index 00000000..667f7794 --- /dev/null +++ b/Shadowsocks.Protocol/PipePair.cs @@ -0,0 +1,33 @@ +using System.IO.Pipelines; + +namespace Shadowsocks.Protocol +{ + internal class PipePair + { + + /* + * + * --> c ---w[ uplink ]r--> s + * <-- c <--r[ downlink ]w--- s + * down up down + */ + + private readonly Pipe uplink = new Pipe(); + private readonly Pipe downlink = new Pipe(); + public DuplexPipe UpSide { get; private set; } + public DuplexPipe DownSide { get; private set; } + public PipePair() + { + UpSide = new DuplexPipe + { + Input = downlink.Reader, + Output = uplink.Writer, + }; + DownSide = new DuplexPipe + { + Input = uplink.Reader, + Output = downlink.Writer, + }; + } + } +} diff --git a/Shadowsocks.Protocol/ProtocolMessagePipe.cs b/Shadowsocks.Protocol/ProtocolMessagePipe.cs new file mode 100644 index 00000000..6c11d553 --- /dev/null +++ b/Shadowsocks.Protocol/ProtocolMessagePipe.cs @@ -0,0 +1,150 @@ +using System; +using System.Buffers; +using System.Diagnostics; +using System.IO.Pipelines; +using System.Threading; +using System.Threading.Tasks; + +namespace Shadowsocks.Protocol +{ + public class ProtocolMessagePipe + { + private readonly PipeReader _reader; + private readonly PipeWriter _writer; + + public ProtocolMessagePipe(IDuplexPipe pipe) + { + _reader = pipe.Input; + _writer = pipe.Output; + } + + public async Task ReadAsync(int millisecond) where T : IProtocolMessage, new() + { + var delay = new CancellationTokenSource(); + delay.CancelAfter(millisecond); + + return await ReadAsync(delay.Token); + } + + public async Task ReadAsync(T ret, int millisecond) where T : IProtocolMessage + { + var delay = new CancellationTokenSource(); + delay.CancelAfter(millisecond); + + return await ReadAsync(ret, delay.Token); + } + + public async Task ReadAsync(CancellationToken token = default) where T : IProtocolMessage, new() => await ReadAsync(new T(), token); + + public async Task ReadAsync(T ret, CancellationToken token = default) where T : IProtocolMessage + { + Debug.WriteLine($"Reading protocol message {typeof(T).Name}"); + //var ret = new T(); + var required = 0; + do + { + var seq = ReadOnlySequence.Empty; + var eof = false; + var ctr = 0; + do + { + if (eof) + throw new FormatException( + $"Message {typeof(T)} parse error, required {required} byte, {seq.Length} byte remain"); + var result = await _reader.ReadAsync(token); + seq = result.Buffer; + eof = result.IsCompleted; + if (seq.Length == 0) + { + if (++ctr > 1000) + throw new FormatException($"Message {typeof(T)} parse error, maybe EOF"); + } + } while (seq.Length < required); + + var frame = MakeFrame(seq); + (var ok, var len) = ret.TryLoad(frame); + if (ok) + { + var ptr = seq.GetPosition(len, seq.Start); + _reader.AdvanceTo(ptr); + break; + } + + if (len == 0) + { + var arr = Util.GetArray(frame).Array; + if (arr == null) throw new FormatException($"Message {typeof(T)} parse error"); + throw new FormatException( + $"Message {typeof(T)} parse error, {Environment.NewLine}{BitConverter.ToString(arr)}"); + } + + required = len; + } while (true); + + return ret; + } + + public async Task WriteAsync(IProtocolMessage msg, CancellationToken token = default) + { + Debug.WriteLine($"Writing protocol message {msg}"); + + Memory buf; + var estSize = 4096; + int size; + do + { + buf = _writer.GetMemory(estSize); + try + { + size = msg.Serialize(buf); + } + catch (ArgumentException) + { + estSize *= 2; + continue; + } + if (estSize > 65536) throw new ArgumentException("Protocol message is too large"); + _writer.Advance(size); + await _writer.FlushAsync(token); + + return; + } while (true); + } + + private SequencePosition _lastFrameStart; + private SequencePosition _lastFrameEnd; + private ReadOnlyMemory _lastFrame; + + public ReadOnlyMemory MakeFrame(ReadOnlySequence seq) + { + // cached frame + if (_lastFrameStart.Equals(seq.Start) && _lastFrameEnd.Equals(seq.End)) + { + Debug.WriteLine("Hit cached frame"); + return _lastFrame; + } + + _lastFrameStart = seq.Start; + _lastFrameEnd = seq.End; + + if (seq.IsSingleSegment) + { + Debug.WriteLine("Frame is single segement"); + _lastFrame = seq.First; + return seq.First; + } + + Debug.WriteLine("Copy frame data into single Memory"); + Memory ret = new byte[seq.Length]; + var ptr = 0; + foreach (var mem in seq) + { + mem.CopyTo(ret.Slice(ptr)); + ptr += mem.Length; + } + + _lastFrame = ret; + return ret; + } + } +} \ No newline at end of file diff --git a/Shadowsocks.Protocol/Shadowsocks.Protocol.csproj b/Shadowsocks.Protocol/Shadowsocks.Protocol.csproj new file mode 100644 index 00000000..ea0f2c32 --- /dev/null +++ b/Shadowsocks.Protocol/Shadowsocks.Protocol.csproj @@ -0,0 +1,13 @@ + + + + net5.0 + enable + + + + + + + + diff --git a/Shadowsocks.Protocol/Shadowsocks/AeadBlockMessage.cs b/Shadowsocks.Protocol/Shadowsocks/AeadBlockMessage.cs new file mode 100644 index 00000000..ffe27bca --- /dev/null +++ b/Shadowsocks.Protocol/Shadowsocks/AeadBlockMessage.cs @@ -0,0 +1,69 @@ +using Shadowsocks.Protocol.Shadowsocks.Crypto; +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Shadowsocks.Protocol.Shadowsocks +{ + class AeadBlockMessage : IProtocolMessage + { + public Memory Data; + private readonly int tagLength; + private readonly ICrypto aead; + private Memory nonce; + + private int expectedDataLength; + + public AeadBlockMessage(ICrypto aead, Memory nonce, CryptoParameter parameter) + { + this.aead = aead; + this.nonce = nonce; + tagLength = parameter.TagSize; + } + + public bool Equals([AllowNull] IProtocolMessage other) => throw new NotImplementedException(); + + public int Serialize(Memory buffer) + { + var len = Data.Length + 2 * tagLength + 2; + if (buffer.Length < len) + throw Util.BufferTooSmall(len, buffer.Length, nameof(buffer)); + Memory m = new byte[2]; + m.Span[0] = (byte)(Data.Length / 256); + m.Span[1] = (byte)(Data.Length % 256); + var len1 = aead.Encrypt(nonce.Span, m.Span, buffer.Span); + Util.SodiumIncrement(nonce.Span); + buffer = buffer.Slice(len1); + aead.Encrypt(nonce.Span, Data.Span, buffer.Span); + Util.SodiumIncrement(nonce.Span); + return len; + } + + public (bool success, int length) TryLoad(ReadOnlyMemory buffer) + { + int len; + if (expectedDataLength == 0) + { + if (buffer.Length < tagLength + 2) return (false, tagLength + 2); + + // decrypt length + Memory m = new byte[2]; + len = aead.Decrypt(nonce.Span, m.Span, buffer.Span); + Util.SodiumIncrement(nonce.Span); + if (len != 2) return (false, 0); + + expectedDataLength = m.Span[0] * 256 + m.Span[1]; + if (expectedDataLength > 0x3fff) return (false, 0); + } + var totalLength = expectedDataLength + 2 * tagLength + 2; + if (buffer.Length < totalLength) return (false, totalLength); + + // decrypt data + var dataBuffer = buffer.Slice(tagLength + 2); + Data = new byte[expectedDataLength]; + len = aead.Decrypt(nonce.Span, Data.Span, dataBuffer.Span); + Util.SodiumIncrement(nonce.Span); + if (len != expectedDataLength) return (false, 0); + return (true, totalLength); + } + } +} diff --git a/Shadowsocks.Protocol/Shadowsocks/AeadClient.cs b/Shadowsocks.Protocol/Shadowsocks/AeadClient.cs new file mode 100644 index 00000000..97d6eaea --- /dev/null +++ b/Shadowsocks.Protocol/Shadowsocks/AeadClient.cs @@ -0,0 +1,110 @@ +using Shadowsocks.Protocol.Shadowsocks.Crypto; +using System; +using System.IO.Pipelines; +using System.Net; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; +using System.Collections.Generic; + +namespace Shadowsocks.Protocol.Shadowsocks +{ + public class AeadClient : IStreamClient + { + private CryptoParameter cryptoParameter; + private readonly byte[] mainKey; + private static readonly byte[] _ssSubKeyInfo = Encoding.ASCII.GetBytes("ss-subkey"); + + public AeadClient(CryptoParameter parameter, string password) + { + cryptoParameter = parameter; + mainKey = CryptoUtils.SSKDF(password, parameter.KeySize); + if (!parameter.IsAead) + throw new NotSupportedException($"Unsupported method."); + } + + public AeadClient(CryptoParameter parameter, byte[] key) + { + cryptoParameter = parameter; + mainKey = key; + } + + public Task Connect(EndPoint destination, IDuplexPipe client, IDuplexPipe server) => + // destination is ignored, this is just a converter + Task.WhenAll(ConvertUplink(client, server), ConvertDownlink(client, server)); + + public async Task ConvertUplink(IDuplexPipe client, IDuplexPipe server) + { + var up = cryptoParameter.GetCrypto(); + var pmp = new ProtocolMessagePipe(server); + var salt = new SaltMessage(16, true); + await pmp.WriteAsync(salt); + + var key = CryptoUtils.HKDF(cryptoParameter.KeySize, mainKey, salt.Salt.ToArray(), _ssSubKeyInfo); + up.Init(key, null); + Memory nonce = new byte[cryptoParameter.NonceSize]; + nonce.Span.Fill(0); + // TODO write salt with data + while (true) + { + var result = await client.Input.ReadAsync(); + if (result.IsCanceled || result.IsCompleted) return; + + // TODO compress into one chunk when possible + + foreach (var item in result.Buffer) + { + foreach (var i in SplitBigChunk(item)) + { + await pmp.WriteAsync(new AeadBlockMessage(up, nonce, cryptoParameter) + { + // in send routine, Data is readonly + Data = MemoryMarshal.AsMemory(i), + }); + } + } + client.Input.AdvanceTo(result.Buffer.End); + } + } + + public async Task ConvertDownlink(IDuplexPipe client, IDuplexPipe server) + { + var down = cryptoParameter.GetCrypto(); + + var pmp = new ProtocolMessagePipe(server); + var salt = await pmp.ReadAsync(new SaltMessage(cryptoParameter.KeySize)); + + var key = CryptoUtils.HKDF(cryptoParameter.KeySize, mainKey, salt.Salt.ToArray(), _ssSubKeyInfo); + down.Init(key, null); + Memory nonce = new byte[cryptoParameter.NonceSize]; + nonce.Span.Fill(0); + + while (true) + { + try + { + var block = await pmp.ReadAsync(new AeadBlockMessage(down, nonce, cryptoParameter)); + await client.Output.WriteAsync(block.Data); + client.Output.Advance(block.Data.Length); + } + catch (FormatException) + { + return; + } + } + } + + public List> SplitBigChunk(ReadOnlyMemory mem) + { + var l = new List>(mem.Length / 0x3fff + 1); + while (mem.Length > 0x3fff) + { + + l.Add(mem.Slice(0, 0x3fff)); + mem = mem.Slice(0x4000); + } + l.Add(mem); + return l; + } + } +} diff --git a/Shadowsocks.Protocol/Shadowsocks/Crypto/AeadAesGcmCrypto.cs b/Shadowsocks.Protocol/Shadowsocks/Crypto/AeadAesGcmCrypto.cs new file mode 100644 index 00000000..1fe5dac6 --- /dev/null +++ b/Shadowsocks.Protocol/Shadowsocks/Crypto/AeadAesGcmCrypto.cs @@ -0,0 +1,38 @@ +using System; +using System.Security.Cryptography; + +namespace Shadowsocks.Protocol.Shadowsocks.Crypto +{ + class AeadAesGcmCrypto : ICrypto + { + AesGcm aes; + + CryptoParameter parameter; + public AeadAesGcmCrypto(CryptoParameter parameter) + { + this.parameter = parameter; + } + + public void Init(byte[] key, byte[] iv) => aes = new AesGcm(key); + + public int Decrypt(ReadOnlySpan nonce, Span plain, ReadOnlySpan cipher) + { + aes.Decrypt( + nonce, + cipher[0..^parameter.TagSize], + cipher[^parameter.TagSize..], + plain[0..(cipher.Length - parameter.TagSize)]); + return cipher.Length - parameter.TagSize; + } + + public int Encrypt(ReadOnlySpan nonce, ReadOnlySpan plain, Span cipher) + { + aes.Encrypt( + nonce, + plain, + cipher[0..plain.Length], + cipher.Slice(plain.Length, parameter.TagSize)); + return plain.Length + parameter.TagSize; + } + } +} diff --git a/Shadowsocks.Protocol/Shadowsocks/Crypto/CryptoParameter.cs b/Shadowsocks.Protocol/Shadowsocks/Crypto/CryptoParameter.cs new file mode 100644 index 00000000..d8f887a6 --- /dev/null +++ b/Shadowsocks.Protocol/Shadowsocks/Crypto/CryptoParameter.cs @@ -0,0 +1,21 @@ +using System; + +namespace Shadowsocks.Protocol.Shadowsocks.Crypto +{ + public struct CryptoParameter + { + public Type Crypto; + public int KeySize; // key size = salt size + public int NonceSize; // reused as iv size + public int TagSize; + + public ICrypto GetCrypto() + { + var ctor = Crypto.GetConstructor(new[] { typeof(CryptoParameter) }) ?? + throw new TypeLoadException("can't load constructor"); + return (ICrypto)ctor.Invoke(new object[] { this }); + } + + public bool IsAead => TagSize > 0; + } +} diff --git a/Shadowsocks.Protocol/Shadowsocks/Crypto/CryptoProvider.cs b/Shadowsocks.Protocol/Shadowsocks/Crypto/CryptoProvider.cs new file mode 100644 index 00000000..637e562d --- /dev/null +++ b/Shadowsocks.Protocol/Shadowsocks/Crypto/CryptoProvider.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; + +namespace Shadowsocks.Protocol.Shadowsocks.Crypto +{ + static class CryptoProvider + { + static Dictionary parameters = new Dictionary + { + ["aes-256-gcm"] = new CryptoParameter { KeySize = 32, NonceSize = 12, TagSize = 16, Crypto = typeof(AeadAesGcmCrypto) }, + ["aes-192-gcm"] = new CryptoParameter { KeySize = 24, NonceSize = 12, TagSize = 16, Crypto = typeof(AeadAesGcmCrypto) }, + ["aes-128-gcm"] = new CryptoParameter { KeySize = 16, NonceSize = 12, TagSize = 16, Crypto = typeof(AeadAesGcmCrypto) }, + ["none"] = new CryptoParameter { KeySize = 0, NonceSize = 0, TagSize = 0, Crypto = typeof(UnsafeNoneCrypto) } + }; + + public static CryptoParameter GetCrypto(string method) + { + if (string.IsNullOrEmpty(method)) + { + // todo + //method = IoCManager.Container.Resolve().GetDefaultMethod(); + } + + method = method.ToLowerInvariant(); + var ok = parameters.TryGetValue(method, out var t); + if (!ok) + { + //t = parameters[DefaultCipher]; + throw new NotImplementedException(); + } + return t; + } + } +} diff --git a/Shadowsocks.Protocol/Shadowsocks/Crypto/CryptoUtils.cs b/Shadowsocks.Protocol/Shadowsocks/Crypto/CryptoUtils.cs new file mode 100644 index 00000000..d7fae32e --- /dev/null +++ b/Shadowsocks.Protocol/Shadowsocks/Crypto/CryptoUtils.cs @@ -0,0 +1,95 @@ +using Org.BouncyCastle.Crypto.Digests; +using Org.BouncyCastle.Crypto.Generators; +using Org.BouncyCastle.Crypto.Parameters; +using System; +using System.Security.Cryptography; +using System.Text; +using System.Threading; + +namespace Shadowsocks.Protocol.Shadowsocks.Crypto +{ + public static class CryptoUtils + { + private static readonly ThreadLocal Md5Hasher = new ThreadLocal(System.Security.Cryptography.MD5.Create); + + public static byte[] MD5(byte[] b) + { + var hash = new byte[16]; + Md5Hasher.Value?.TryComputeHash(b, hash, out _); + return hash; + } + // currently useless, just keep api same + public static Span MD5(Span span) + { + Span hash = new byte[16]; + Md5Hasher.Value?.TryComputeHash(span, hash, out _); + return hash; + } + + public static byte[] HKDF(int keylen, byte[] master, byte[] salt, byte[] info) + { + var ret = new byte[keylen]; + var degist = new Sha1Digest(); + var parameters = new HkdfParameters(master, salt, info); + var hkdf = new HkdfBytesGenerator(degist); + hkdf.Init(parameters); + hkdf.GenerateBytes(ret, 0, keylen); + return ret; + } + // currently useless, just keep api same, again + public static Span HKDF(int keylen, Span master, Span salt, Span info) + { + var ret = new byte[keylen]; + var degist = new Sha1Digest(); + var parameters = new HkdfParameters(master.ToArray(), salt.ToArray(), info.ToArray()); + var hkdf = new HkdfBytesGenerator(degist); + hkdf.Init(parameters); + hkdf.GenerateBytes(ret, 0, keylen); + return ret.AsSpan(); + } + + public static byte[] SSKDF(string password, int keylen) + { + var key = new byte[keylen]; + var pw = Encoding.UTF8.GetBytes(password); + var result = new byte[password.Length + 16]; + var i = 0; + var md5sum = Array.Empty(); + while (i < keylen) + { + if (i == 0) + { + md5sum = MD5(pw); + } + else + { + Array.Copy(md5sum, 0, result, 0, 16); + Array.Copy(pw, 0, result, 16, password.Length); + md5sum = MD5(result); + } + Array.Copy(md5sum, 0, key, i, Math.Min(16, keylen - i)); + i += 16; + } + return key; + } + + public static void SodiumIncrement(Span salt) + { + for (var i = 0; i < salt.Length; ++i) + { + if (++salt[i] != 0) + { + break; + } + } + } + + public static void RandomSpan(Span span) + { + using (var rng = RandomNumberGenerator.Create()) + { + rng.GetBytes(span); + } + } + } +} diff --git a/Shadowsocks.Protocol/Shadowsocks/Crypto/UnsafeNoneCrypto.cs b/Shadowsocks.Protocol/Shadowsocks/Crypto/UnsafeNoneCrypto.cs new file mode 100644 index 00000000..4c885248 --- /dev/null +++ b/Shadowsocks.Protocol/Shadowsocks/Crypto/UnsafeNoneCrypto.cs @@ -0,0 +1,28 @@ +using System; + +namespace Shadowsocks.Protocol.Shadowsocks.Crypto +{ + class UnsafeNoneCrypto : ICrypto + { + public UnsafeNoneCrypto(CryptoParameter parameter) + { + + } + + public int Decrypt(ReadOnlySpan nonce, Span plain, ReadOnlySpan cipher) + { + cipher.CopyTo(plain); + return plain.Length; + } + + public int Encrypt(ReadOnlySpan nonce, ReadOnlySpan plain, Span cipher) + { + plain.CopyTo(cipher); + return plain.Length; + } + + public void Init(byte[] key, byte[] iv) + { + } + } +} diff --git a/Shadowsocks.Protocol/Shadowsocks/ICrypto.cs b/Shadowsocks.Protocol/Shadowsocks/ICrypto.cs new file mode 100644 index 00000000..d23881cb --- /dev/null +++ b/Shadowsocks.Protocol/Shadowsocks/ICrypto.cs @@ -0,0 +1,12 @@ +using System; + +namespace Shadowsocks.Protocol.Shadowsocks +{ + // stream cipher simply ignore nonce + public interface ICrypto + { + void Init(byte[] key, byte[] iv); + int Encrypt(ReadOnlySpan nonce, ReadOnlySpan plain, Span cipher); + int Decrypt(ReadOnlySpan nonce, Span plain, ReadOnlySpan cipher); + } +} diff --git a/Shadowsocks.Protocol/Shadowsocks/PayloadProtocolClient.cs b/Shadowsocks.Protocol/Shadowsocks/PayloadProtocolClient.cs new file mode 100644 index 00000000..dd8630ff --- /dev/null +++ b/Shadowsocks.Protocol/Shadowsocks/PayloadProtocolClient.cs @@ -0,0 +1,21 @@ +using Shadowsocks.Protocol.Socks5; +using System.IO.Pipelines; +using System.Net; +using System.Threading.Tasks; + +namespace Shadowsocks.Protocol.Shadowsocks +{ + // shadowsocks payload protocol client + class PayloadProtocolClient : IStreamClient + { + public async Task Connect(EndPoint destination, IDuplexPipe client, IDuplexPipe server) + { + var addrMem = server.Output.GetMemory(512); + + var addrLen = Socks5Message.SerializeAddress(addrMem, destination); + server.Output.Advance(addrLen); + + await DuplexPipe.CopyDuplexPipe(client, server); + } + } +} diff --git a/Shadowsocks.Protocol/Shadowsocks/SaltMessage.cs b/Shadowsocks.Protocol/Shadowsocks/SaltMessage.cs new file mode 100644 index 00000000..4e3ffe97 --- /dev/null +++ b/Shadowsocks.Protocol/Shadowsocks/SaltMessage.cs @@ -0,0 +1,37 @@ +using Shadowsocks.Protocol.Shadowsocks.Crypto; +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Shadowsocks.Protocol.Shadowsocks +{ + public class SaltMessage : IProtocolMessage + { + private readonly int length; + public Memory Salt { get; private set; } + + public SaltMessage(int length, bool roll = false) + { + this.length = length; + if (roll) + { + Salt = new byte[length]; + CryptoUtils.RandomSpan(Salt.Span); + } + } + + public bool Equals([AllowNull] IProtocolMessage other) => throw new NotImplementedException(); + + public int Serialize(Memory buffer) + { + Salt.CopyTo(buffer); + return length; + } + + public (bool success, int length) TryLoad(ReadOnlyMemory buffer) + { + if (buffer.Length < length) return (false, length); + buffer.Slice(0, length).CopyTo(Salt); + return (true, length); + } + } +} diff --git a/Shadowsocks.Protocol/Shadowsocks/ShadowsocksClient.cs b/Shadowsocks.Protocol/Shadowsocks/ShadowsocksClient.cs new file mode 100644 index 00000000..681b1404 --- /dev/null +++ b/Shadowsocks.Protocol/Shadowsocks/ShadowsocksClient.cs @@ -0,0 +1,41 @@ +using Shadowsocks.Protocol.Shadowsocks.Crypto; +using System.IO.Pipelines; +using System.Net; +using System.Threading.Tasks; + +namespace Shadowsocks.Protocol.Shadowsocks +{ + internal class ShadowsocksClient : IStreamClient + { + private readonly IStreamClient shadow; + private readonly PayloadProtocolClient socks = new PayloadProtocolClient(); + private readonly PipePair p = new PipePair(); + + public ShadowsocksClient(string method, string password) + { + var param = CryptoProvider.GetCrypto(method); + if (param.IsAead) + { + shadow = new AeadClient(param, password); + } + else + { + shadow = new UnsafeClient(param, password); + } + } + + public ShadowsocksClient(string method, byte[] key) + { + var param = CryptoProvider.GetCrypto(method); + shadow = new AeadClient(param, key); + } + + public Task Connect(EndPoint destination, IDuplexPipe client, IDuplexPipe server) + { + var tShadow = shadow.Connect(null, p.UpSide, server); + var tSocks = socks.Connect(destination, client, p.UpSide); + + return Task.WhenAll(tShadow, tSocks); + } + } +} diff --git a/Shadowsocks.Protocol/Shadowsocks/UnsafeClient.cs b/Shadowsocks.Protocol/Shadowsocks/UnsafeClient.cs new file mode 100644 index 00000000..02a19819 --- /dev/null +++ b/Shadowsocks.Protocol/Shadowsocks/UnsafeClient.cs @@ -0,0 +1,86 @@ +using Shadowsocks.Protocol.Shadowsocks.Crypto; +using System; +using System.IO.Pipelines; +using System.Net; +using System.Threading.Tasks; + +namespace Shadowsocks.Protocol.Shadowsocks +{ + // 'original' shadowsocks encryption layer + internal class UnsafeClient : IStreamClient + { + CryptoParameter parameter; + string password; + + public UnsafeClient(CryptoParameter parameter, string password) + { + this.password = password; + this.parameter = parameter; + } + + public Task Connect(EndPoint destination, IDuplexPipe client, IDuplexPipe server) => + // destination is ignored, this is just a converter + Task.WhenAll(ConvertUplink(client, server), ConvertDownlink(client, server)); + + public async Task ConvertUplink(IDuplexPipe client, IDuplexPipe server) + { + var up = parameter.GetCrypto(); + var pmp = new ProtocolMessagePipe(server); + var key = CryptoUtils.SSKDF(password, parameter.KeySize); + + var salt = new SaltMessage(parameter.NonceSize, true); + await pmp.WriteAsync(salt); + + up.Init(key, salt.Salt.ToArray()); + Memory nonce = new byte[parameter.NonceSize]; + nonce.Span.Fill(0); + // TODO write salt with data + while (true) + { + var result = await client.Input.ReadAsync(); + if (result.IsCanceled || result.IsCompleted) return; + + // TODO compress into one chunk when possible + + foreach (var item in result.Buffer) + { + var mem = server.Output.GetMemory(item.Length); + var len = up.Encrypt(null, item.Span, mem.Span); + server.Output.Advance(len); + } + client.Input.AdvanceTo(result.Buffer.End); + } + } + + public async Task ConvertDownlink(IDuplexPipe client, IDuplexPipe server) + { + var down = parameter.GetCrypto(); + + var pmp = new ProtocolMessagePipe(server); + var salt = await pmp.ReadAsync(new SaltMessage(parameter.NonceSize)); + + var key = CryptoUtils.SSKDF(password, parameter.KeySize); + down.Init(key, salt.Salt.ToArray()); + + while (true) + { + while (true) + { + var result = await server.Input.ReadAsync(); + if (result.IsCanceled || result.IsCompleted) return; + + // TODO compress into one chunk when possible + + foreach (var item in result.Buffer) + { + var mem = client.Output.GetMemory(item.Length); + var len = down.Decrypt(null, mem.Span, item.Span); + client.Output.Advance(len); + } + server.Input.AdvanceTo(result.Buffer.End); + } + } + } + + } +} diff --git a/Shadowsocks.Protocol/Socks5/Socks5Client.cs b/Shadowsocks.Protocol/Socks5/Socks5Client.cs new file mode 100644 index 00000000..ed018e54 --- /dev/null +++ b/Shadowsocks.Protocol/Socks5/Socks5Client.cs @@ -0,0 +1,64 @@ +using System; +using System.Diagnostics; +using System.IO.Pipelines; +using System.Net; +using System.Text; +using System.Threading.Tasks; + +namespace Shadowsocks.Protocol.Socks5 +{ + class Socks5Client : IStreamClient + { + NetworkCredential _credential; + + public Socks5Client(NetworkCredential credential = null) + { + _credential = credential; + } + + public async Task Connect(EndPoint destination, IDuplexPipe client, IDuplexPipe server) + { + var pmp = new ProtocolMessagePipe(server); + + await pmp.WriteAsync(new Socks5VersionIdentifierMessage + { + Auth = _credential == null ? new [] { Socks5Message.AuthNone } : new [] { Socks5Message.AuthNone, Socks5Message.AuthUserPass } + }); + + var msm = await pmp.ReadAsync(); + switch (msm.SelectedAuth) + { + case Socks5Message.AuthNone: + break; + case Socks5Message.AuthUserPass: + Debug.Assert(_credential != null); + var name = _credential.UserName; + var password = _credential.Password; + + await pmp.WriteAsync(new Socks5UserPasswordRequestMessage + { + User = Encoding.UTF8.GetBytes(name), + Password = Encoding.UTF8.GetBytes(password), + }); + + var upResp = await pmp.ReadAsync(); + if (!upResp.Success) throw new UnauthorizedAccessException("Wrong username / password"); + + break; + default: + throw new NotSupportedException("Server not support our authencation method"); + } + await pmp.WriteAsync(new Socks5RequestMessage + { + Command = Socks5Message.CmdConnect, + EndPoint = destination, + }); + + var reply = await pmp.ReadAsync(); + + if (reply.Reply != Socks5Message.ReplySucceed) throw new Exception(); + + await DuplexPipe.CopyDuplexPipe(client, server); + } + } +} diff --git a/Shadowsocks.Protocol/Socks5/Socks5Message.cs b/Shadowsocks.Protocol/Socks5/Socks5Message.cs new file mode 100644 index 00000000..6c6720fa --- /dev/null +++ b/Shadowsocks.Protocol/Socks5/Socks5Message.cs @@ -0,0 +1,185 @@ +using System; +using System.Diagnostics; +using System.Net; +using System.Net.Sockets; +using System.Text; + +namespace Shadowsocks.Protocol.Socks5 +{ + public abstract class Socks5Message : IProtocolMessage + { + public abstract int Serialize(Memory buffer); + public abstract (bool success, int length) TryLoad(ReadOnlyMemory buffer); + + public abstract bool Equals(IProtocolMessage other); + + #region Socks5 constants + + public const byte AuthNone = 0; + public const byte AuthGssApi = 1; + public const byte AuthUserPass = 2; + public const byte AuthChallengeHandshake = 3; + public const byte AuthChallengeResponse = 5; + public const byte AuthSsl = 6; + public const byte AuthNds = 7; + public const byte AuthMultiAuthenticationFramework = 8; + public const byte AuthJsonParameterBlock = 9; + public const byte AuthNoAcceptable = 0xff; + + public const byte AddressIPv4 = 1; + public const byte AddressDomain = 3; + public const byte AddressIPv6 = 4; + + public const byte CmdConnect = 1; + public const byte CmdBind = 2; + public const byte CmdUdpAssociation = 3; + + public const byte ReplySucceed = 0; + public const byte ReplyFailure = 1; + public const byte ReplyNotAllowed = 2; + public const byte ReplyNetworkUnreachable = 3; + public const byte ReplyHostUnreachable = 4; + public const byte ReplyConnectionRefused = 5; + public const byte ReplyTtlExpired = 6; + public const byte ReplyCommandNotSupport = 7; + public const byte ReplyAddressNotSupport = 8; + + #endregion + + private static readonly NotSupportedException _addressNotSupport = + new NotSupportedException("Socks5 only support IPv4, IPv6, Domain name address"); + + #region Address convert + + private static (byte high, byte low) ExpandPort(int port) + { + Debug.Assert(port >= 0 && port <= 65535); + return ((byte) (port / 256), (byte) (port % 256)); + } + + private static int TransformPort(byte high, byte low) => high * 256 + low; + + protected static int NeededBytes(EndPoint endPoint) + { + switch (endPoint) + { + case IPEndPoint ipEndPoint when ipEndPoint.AddressFamily == AddressFamily.InterNetwork: + return 7; + + case IPEndPoint ipEndPoint when ipEndPoint.AddressFamily == AddressFamily.InterNetworkV6: + return 19; + + case DnsEndPoint dnsEndPoint: + var host = Util.EncodeHostName(dnsEndPoint.Host); + return host.Length + 4; + + default: + throw _addressNotSupport; + } + } + + public static int SerializeAddress(Memory buffer, EndPoint endPoint) + { + switch (endPoint) + { + case IPEndPoint ipEndPoint when ipEndPoint.AddressFamily == AddressFamily.InterNetwork: + { + if (buffer.Length < 7) throw Util.BufferTooSmall(7, buffer.Length, nameof(buffer)); + buffer.Span[0] = AddressIPv4; + Debug.Assert(ipEndPoint.Address.TryWriteBytes(buffer.Span[1..], out var l)); + Debug.Assert(l == 4); + + (var high, var low) = ExpandPort(ipEndPoint.Port); + buffer.Span[5] = high; + buffer.Span[6] = low; + return 7; + } + + case IPEndPoint ipEndPoint when ipEndPoint.AddressFamily == AddressFamily.InterNetworkV6: + { + if (buffer.Length < 19) throw Util.BufferTooSmall(19, buffer.Length, nameof(buffer)); + buffer.Span[0] = AddressIPv6; + Debug.Assert(ipEndPoint.Address.TryWriteBytes(buffer.Span[1..], out var l)); + Debug.Assert(l == 16); + + (var high, var low) = ExpandPort(ipEndPoint.Port); + buffer.Span[18] = low; + buffer.Span[17] = high; + return 19; + } + + case DnsEndPoint dnsEndPoint: + { + // 3 lHost [Host] port port + + var host = Util.EncodeHostName(dnsEndPoint.Host); + if (host.Length > 255) throw new NotSupportedException("Host name too long"); + if (buffer.Length < host.Length + 4) + throw Util.BufferTooSmall(host.Length + 4, buffer.Length, nameof(buffer)); + + buffer.Span[0] = AddressDomain; + buffer.Span[1] = (byte) host.Length; + Encoding.ASCII.GetBytes(host, buffer.Span[2..]); + + (var high, var low) = ExpandPort(dnsEndPoint.Port); + buffer.Span[host.Length + 2] = high; + buffer.Span[host.Length + 3] = low; + + return host.Length + 4; + } + + default: + throw _addressNotSupport; + } + } + + public static (bool success, int length) TryParseAddress(ReadOnlyMemory buffer, + out EndPoint result) + { + result = default; + if (buffer.Length < 1) return (false, 1); + var addrType = buffer.Span[0]; + int len; + switch (addrType) + { + case AddressIPv4: + if (buffer.Length < 7) return (false, 7); + var s = buffer[1..5]; + result = new IPEndPoint( + new IPAddress(Util.GetArray(s)), + TransformPort(buffer.Span[5], buffer.Span[6]) + ); + len = 7; + break; + + case AddressDomain: + if (buffer.Length < 2) return (false, 2); + var nameLength = buffer.Span[1]; + if (buffer.Length < nameLength + 4) return (false, nameLength + 4); + + result = new DnsEndPoint( + Encoding.ASCII.GetString(buffer.Span[2..(nameLength + 2)]), + TransformPort(buffer.Span[nameLength + 2], buffer.Span[nameLength + 3]) + ); + len = nameLength + 4; + + break; + + case AddressIPv6: + if (buffer.Length < 19) return (false, 19); + result = new IPEndPoint(new IPAddress(Util.GetArray(buffer[1..17])), + TransformPort(buffer.Span[17], buffer.Span[18])); + len = 19; + + break; + + default: + return (false, 0); + } + + return (true, len); + } + + #endregion + } +} \ No newline at end of file diff --git a/Shadowsocks.Protocol/Socks5/Socks5MethodSelectionMessage.cs b/Shadowsocks.Protocol/Socks5/Socks5MethodSelectionMessage.cs new file mode 100644 index 00000000..95f78039 --- /dev/null +++ b/Shadowsocks.Protocol/Socks5/Socks5MethodSelectionMessage.cs @@ -0,0 +1,37 @@ +using System; + +namespace Shadowsocks.Protocol.Socks5 +{ + public class Socks5MethodSelectionMessage : Socks5Message + { + // 5 auth + public byte SelectedAuth; + + public override int Serialize(Memory buffer) + { + if (buffer.Length < 2) throw Util.BufferTooSmall(2, buffer.Length, nameof(buffer)); + + buffer.Span[0] = 5; + buffer.Span[1] = SelectedAuth; + return 2; + } + + public override (bool success, int length) TryLoad(ReadOnlyMemory buffer) + { + // need 3 byte + if (buffer.Length < 2) return (false, 2); + if (buffer.Span[0] != 5) return (false, 0); + + SelectedAuth = buffer.Span[1]; + return (true, 2); + } + + public override bool Equals(IProtocolMessage other) + { + if (other is null) return false; + if (ReferenceEquals(this, other)) return true; + if (other.GetType() != GetType()) return false; + return SelectedAuth == ((Socks5MethodSelectionMessage) other).SelectedAuth; + } + } +} \ No newline at end of file diff --git a/Shadowsocks.Protocol/Socks5/Socks5RequestReplyMessage.cs b/Shadowsocks.Protocol/Socks5/Socks5RequestReplyMessage.cs new file mode 100644 index 00000000..35f8b34b --- /dev/null +++ b/Shadowsocks.Protocol/Socks5/Socks5RequestReplyMessage.cs @@ -0,0 +1,76 @@ +using System; +using System.Diagnostics; +using System.Net; + +namespace Shadowsocks.Protocol.Socks5 +{ + public class Socks5RequestReplyMessageBase : Socks5Message + { + // 5 cmdOrReply 0 aType [addr] port + + protected byte CmdByte; + public EndPoint EndPoint =new IPEndPoint(IPAddress.Any, 0); + + public override int Serialize(Memory buffer) + { + var addrLen = NeededBytes(EndPoint); + if (buffer.Length < addrLen + 3) throw Util.BufferTooSmall(addrLen + 3, buffer.Length, nameof(buffer)); + buffer.Span[0] = 5; + buffer.Span[1] = CmdByte; + buffer.Span[2] = 0; + + return SerializeAddress(buffer[3..], EndPoint) + 3; + } + + public override (bool success, int length) TryLoad(ReadOnlyMemory buffer) + { + if (buffer.Length < 4) return (false, 4); + if (buffer.Span[0] != 5 || buffer.Span[2] != 0) return (false, 0); + if (buffer.Span[3] == 3 && buffer.Length < 5) return (false, 5); + var req = buffer.Span[3] switch + { + AddressIPv4 => 10, + AddressDomain => buffer.Span[4] + 7, + AddressIPv6 => 22, + _ => 0, + }; + if (req == 0) return (false, 0); + if (buffer.Length < req) return (false, req); + + (var state, var len) = TryParseAddress(buffer[3..], out var ep); + Debug.Assert(state); + Debug.Assert(len == req - 3); + + CmdByte = buffer.Span[1]; + EndPoint = ep; + return (true, req); + } + + public override bool Equals(IProtocolMessage other) + { + if (other is null) return false; + if (ReferenceEquals(this, other)) return true; + if (other.GetType() != GetType()) return false; + var msg = (Socks5RequestReplyMessageBase) other; + return CmdByte == msg.CmdByte && EndPoint.Equals(msg.EndPoint); + } + } + + public class Socks5ReplyMessage : Socks5RequestReplyMessageBase + { + public byte Reply + { + get => CmdByte; + set => CmdByte = value; + } + } + + public class Socks5RequestMessage : Socks5RequestReplyMessageBase + { + public byte Command + { + get => CmdByte; + set => CmdByte = value; + } + } +} \ No newline at end of file diff --git a/Shadowsocks.Protocol/Socks5/Socks5Service.cs b/Shadowsocks.Protocol/Socks5/Socks5Service.cs new file mode 100644 index 00000000..38f10449 --- /dev/null +++ b/Shadowsocks.Protocol/Socks5/Socks5Service.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.IO.Pipelines; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Shadowsocks.Protocol.Socks5 +{ + public class Socks5Service : IStreamService + { + public Socks5Service() + { + + } + + public Socks5Service(Dictionary passwords) + { + enablePassword = true; + this.passwords = passwords; + } + + private readonly bool enablePassword; + private readonly Dictionary passwords = new Dictionary(); + public static int ReadTimeout = 120000; + + public async Task IsMyClient(IDuplexPipe pipe) + { + var result = await pipe.Input.ReadAsync(); + pipe.Input.AdvanceTo(result.Buffer.Start); + var buffer = result.Buffer; + if (buffer.Length < 3) return false; + if (buffer.First.Span[0] != 5) return false; + if (buffer.First.Span[1] == 0) return false; + // ver 5, has auth method + return true; + } + + public async Task Handle(IDuplexPipe pipe) + { + var pmp = new ProtocolMessagePipe(pipe); + var hs = await pmp.ReadAsync(); + + var selected = Socks5Message.AuthNoAcceptable; + if (enablePassword) + { + foreach (var a in Util.GetArray(hs.Auth)) + { + if (a == Socks5Message.AuthUserPass) + { + selected = Socks5Message.AuthUserPass; + break; + } + + if (a == Socks5Message.AuthNone) + { + selected = Socks5Message.AuthNone; + } + } + } + else + { + if (Util.GetArray(hs.Auth).Any(a => a == Socks5Message.AuthNone)) + { + selected = Socks5Message.AuthNone; + } + } + + await pmp.WriteAsync(new Socks5MethodSelectionMessage() + { + SelectedAuth = selected, + }); + switch (selected) + { + case Socks5Message.AuthNoAcceptable: + default: + await pipe.Output.CompleteAsync(); + return null; + + case Socks5Message.AuthNone: + break; + + case Socks5Message.AuthUserPass: + var token = await pmp.ReadAsync(); + var user = Encoding.UTF8.GetString(token.User.Span); + var password = Encoding.UTF8.GetString(token.Password.Span); + var ar = new Socks5UserPasswordResponseMessage(); + var success = + passwords.TryGetValue(user, out var expectPassword) + && expectPassword == password; + ar.Success = success; + await pmp.WriteAsync(ar); + if (!success) + { + await pipe.Output.CompleteAsync(); + return null; + } + + break; + } + + var req = await pmp.ReadAsync(); + var resp = new Socks5ReplyMessage(); + switch (req.Command) + { + case Socks5Message.CmdBind: + case Socks5Message.CmdUdpAssociation: // not support yet + resp.Reply = Socks5Message.ReplyCommandNotSupport; + break; + + case Socks5Message.CmdConnect: + Console.WriteLine(req.EndPoint); + // TODO: route and dial outbound + + + resp.Reply = Socks5Message.ReplySucceed; + break; + } + // TODO: write response, hand out connection + await pmp.WriteAsync(resp); + if (req.Command != Socks5Message.CmdConnect) return null; + + return pipe; + } + } +} diff --git a/Shadowsocks.Protocol/Socks5/Socks5UdpMessage.cs b/Shadowsocks.Protocol/Socks5/Socks5UdpMessage.cs new file mode 100644 index 00000000..a5a1736b --- /dev/null +++ b/Shadowsocks.Protocol/Socks5/Socks5UdpMessage.cs @@ -0,0 +1,56 @@ +using System; +using System.Diagnostics; +using System.Net; + +namespace Shadowsocks.Protocol.Socks5 +{ + public class Socks5UdpMessage : Socks5Message + { + public byte Fragment; + public EndPoint EndPoint = new IPEndPoint(IPAddress.Any, 0); + + public override int Serialize(Memory buffer) + { + var addrLen = NeededBytes(EndPoint); + if (buffer.Length < addrLen + 3) throw Util.BufferTooSmall(addrLen + 3, buffer.Length, nameof(buffer)); + buffer.Span[0] = 0; + buffer.Span[1] = 0; + buffer.Span[2] = Fragment; + + return SerializeAddress(buffer[3..], EndPoint) + 3; + } + + public override (bool success, int length) TryLoad(ReadOnlyMemory buffer) + { + if (buffer.Length < 4) return (false, 4); + if (buffer.Span[0] != 0 || buffer.Span[1] != 0) return (false, 0); + if (buffer.Span[3] == 3 && buffer.Length < 5) return (false, 5); + var req = buffer.Span[3] switch + { + AddressIPv4 => 10, + AddressDomain => buffer.Span[4] + 7, + AddressIPv6 => 22, + _ => 0, + }; + if (req == 0) return (false, 0); + if (buffer.Length < req) return (false, req); + + (var state, var len) = TryParseAddress(buffer[3..], out var ep); + Debug.Assert(state); + Debug.Assert(len == req - 3); + + Fragment = buffer.Span[2]; + EndPoint = ep; + return (true, req); + } + + public override bool Equals(IProtocolMessage other) + { + if (other is null) return false; + if (ReferenceEquals(this, other)) return true; + if (other.GetType() != GetType()) return false; + var msg = (Socks5UdpMessage) other; + return Fragment == msg.Fragment && Equals(EndPoint, msg.EndPoint); + } + } +} \ No newline at end of file diff --git a/Shadowsocks.Protocol/Socks5/Socks5UserPasswordRequestMessage.cs b/Shadowsocks.Protocol/Socks5/Socks5UserPasswordRequestMessage.cs new file mode 100644 index 00000000..22050bb3 --- /dev/null +++ b/Shadowsocks.Protocol/Socks5/Socks5UserPasswordRequestMessage.cs @@ -0,0 +1,47 @@ +using System; + +namespace Shadowsocks.Protocol.Socks5 +{ + public class Socks5UserPasswordRequestMessage : Socks5Message + { + // 1 lUser [User] lPassword [Password] + + public Memory User; + public Memory Password; + + public override int Serialize(Memory buffer) + { + var required = User.Length + Password.Length + 3; + if (buffer.Length < required) throw Util.BufferTooSmall(required, buffer.Length, nameof(buffer)); + buffer.Span[0] = 1; + buffer.Span[1] = (byte) User.Length; + User.CopyTo(buffer.Slice(2)); + buffer.Span[User.Length + 2] = (byte) Password.Length; + Password.CopyTo(buffer.Slice(User.Length + 3)); + return required; + } + + public override (bool success, int length) TryLoad(ReadOnlyMemory buffer) + { + if (buffer.Length < 2) return (false, 2); + if (buffer.Span[0] != 1) return (false, 0); + int userLength = buffer.Span[1]; + if (buffer.Length < userLength + 3) return (false, userLength + 3); + int passLength = buffer.Span[userLength + 2]; + if (buffer.Length < userLength + passLength + 3) return (false, userLength + passLength + 3); + + User = Util.GetArray(buffer[2..(2 + userLength)]); + Password = Util.GetArray(buffer[(3 + userLength)..(3 + userLength + passLength)]); + return (true, userLength + passLength + 3); + } + + public override bool Equals(IProtocolMessage other) + { + if (other is null) return false; + if (ReferenceEquals(this, other)) return true; + if (other.GetType() != GetType()) return false; + var msg = (Socks5UserPasswordRequestMessage) other; + return Util.MemEqual(User, msg.User) && Util.MemEqual(Password, msg.Password); + } + } +} \ No newline at end of file diff --git a/Shadowsocks.Protocol/Socks5/Socks5UserPasswordResponseMessage.cs b/Shadowsocks.Protocol/Socks5/Socks5UserPasswordResponseMessage.cs new file mode 100644 index 00000000..fffadedc --- /dev/null +++ b/Shadowsocks.Protocol/Socks5/Socks5UserPasswordResponseMessage.cs @@ -0,0 +1,37 @@ +using System; + +namespace Shadowsocks.Protocol.Socks5 +{ + public class Socks5UserPasswordResponseMessage : Socks5Message + { + // 1 success + + public bool Success; + + public override int Serialize(Memory buffer) + { + if (buffer.Length < 2) throw Util.BufferTooSmall(2, buffer.Length, nameof(buffer)); + + buffer.Span[0] = 1; + buffer.Span[1] = (byte) (Success ? 0 : 1); + return 2; + } + + public override (bool success, int length) TryLoad(ReadOnlyMemory buffer) + { + if (buffer.Length < 2) return (false, 2); + if (buffer.Span[0] != 1) return (false, 0); + + Success = buffer.Span[1] == 0; + return (true, 2); + } + + public override bool Equals(IProtocolMessage other) + { + if (other is null) return false; + if (ReferenceEquals(this, other)) return true; + if (other.GetType() != GetType()) return false; + return Success == ((Socks5UserPasswordResponseMessage)other).Success; + } + } +} \ No newline at end of file diff --git a/Shadowsocks.Protocol/Socks5/Socks5VersionIdentifierMessage.cs b/Shadowsocks.Protocol/Socks5/Socks5VersionIdentifierMessage.cs new file mode 100644 index 00000000..8d1805fe --- /dev/null +++ b/Shadowsocks.Protocol/Socks5/Socks5VersionIdentifierMessage.cs @@ -0,0 +1,42 @@ +using System; + +namespace Shadowsocks.Protocol.Socks5 +{ + public class Socks5VersionIdentifierMessage : Socks5Message + { + // 5 lAuth [Auth] + + public Memory Auth; + + public override int Serialize(Memory buffer) + { + var required = Auth.Length + 2; + if (buffer.Length < required) throw Util.BufferTooSmall(required, buffer.Length, nameof(buffer)); + + buffer.Span[0] = 5; + buffer.Span[1] = (byte) Auth.Length; + Auth.CopyTo(buffer.Slice(2)); + return Auth.Length + 2; + } + + public override (bool success, int length) TryLoad(ReadOnlyMemory buffer) + { + // need 3 byte + if (buffer.Length < 3) return (false, 3); + if (buffer.Span[0] != 5) return (false, 0); + if (buffer.Span[1] == 0) return (false, 0); + if (buffer.Length < buffer.Span[1] + 2) return (false, buffer.Span[1] + 2); + + Auth = Util.GetArray(buffer[2..(2 + buffer.Span[1])]); + return (true, buffer.Span[1] + 2); + } + + public override bool Equals(IProtocolMessage other) + { + if (other is null) return false; + if (ReferenceEquals(this, other)) return true; + if (other.GetType() != GetType()) return false; + return Util.MemEqual(Auth, ((Socks5VersionIdentifierMessage) other).Auth); + } + } +} \ No newline at end of file diff --git a/Shadowsocks.Protocol/TcpPipeClient.cs b/Shadowsocks.Protocol/TcpPipeClient.cs new file mode 100644 index 00000000..ecaec790 --- /dev/null +++ b/Shadowsocks.Protocol/TcpPipeClient.cs @@ -0,0 +1,16 @@ +using Pipelines.Sockets.Unofficial; +using System.IO.Pipelines; +using System.Net; +using System.Threading.Tasks; + +namespace Shadowsocks.Protocol +{ + public class TcpPipeClient : IStreamClient + { + public async Task Connect(EndPoint destination, IDuplexPipe client, IDuplexPipe server) + { + var sc = await SocketConnection.ConnectAsync(destination); + await DuplexPipe.CopyDuplexPipe(client, sc); + } + } +} diff --git a/Shadowsocks.Protocol/TcpPipeListener.cs b/Shadowsocks.Protocol/TcpPipeListener.cs new file mode 100644 index 00000000..b8f9ddd6 --- /dev/null +++ b/Shadowsocks.Protocol/TcpPipeListener.cs @@ -0,0 +1,69 @@ +using Pipelines.Sockets.Unofficial; +using Shadowsocks.Protocol.Shadowsocks; +using Shadowsocks.Protocol.Socks5; +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; +using System.Threading.Tasks; + +namespace Shadowsocks.Protocol +{ + public class TcpPipeListener + { + private readonly TcpListener _listener; + private readonly IEnumerable _services; + + public TcpPipeListener(IPEndPoint localEP) + { + _listener = new TcpListener(localEP); + _services = new[] { new Socks5Service(), }; + } + + public TcpPipeListener(IPEndPoint endPoint, IEnumerable services) + { + _listener = new TcpListener(endPoint); + _services = services; + } + + public async Task Start(IPEndPoint localEP, DnsEndPoint remoteEP, string method, string? password, byte[]? key) + { + _listener.Start(); + + while (true) + { + var socket = await _listener.AcceptSocketAsync(); + var conn = SocketConnection.Create(socket); + + foreach (var svc in _services) + { + if (await svc.IsMyClient(conn)) + { + // todo: save to list, so we can optionally close them + _ = RunService(svc, conn, localEP, remoteEP, method, password, key); + } + } + } + } + + private async Task RunService(IStreamService svc, SocketConnection conn, IPEndPoint localEP, DnsEndPoint remoteEP, string method, string? password, byte[]? key) + { + var s5tcp = new PipePair(); + + var raw = await svc.Handle(conn); + ShadowsocksClient s5c; + if (!string.IsNullOrEmpty(password)) + s5c = new ShadowsocksClient(method, password); + else if (key != null) + s5c = new ShadowsocksClient(method, key); + else + throw new ArgumentException("Either a password or a key must be provided."); + var tpc = new TcpPipeClient(); + var t2 = tpc.Connect(remoteEP, s5tcp.DownSide, null); + var t1 = s5c.Connect(localEP, raw, s5tcp.UpSide); + await Task.WhenAll(t1, t2); + } + + public void Stop() => _listener.Stop(); + } +} diff --git a/Shadowsocks.Protocol/Util.cs b/Shadowsocks.Protocol/Util.cs new file mode 100644 index 00000000..89956696 --- /dev/null +++ b/Shadowsocks.Protocol/Util.cs @@ -0,0 +1,53 @@ +using System; +using System.Globalization; +using System.Runtime.InteropServices; +using System.Text; + +namespace Shadowsocks.Protocol +{ + internal static class Util + { + private static readonly IdnMapping _idnMapping = new IdnMapping(); + + public static string RestoreHostName(string punycode) => Encoding.UTF8.GetByteCount(punycode) != punycode.Length + ? punycode.ToLowerInvariant() + : _idnMapping.GetUnicode(punycode).ToLowerInvariant(); + + public static string EncodeHostName(string unicode) => Encoding.UTF8.GetByteCount(unicode) != unicode.Length + ? _idnMapping.GetAscii(unicode).ToLowerInvariant() + : unicode.ToLowerInvariant(); + + public static ArraySegment GetArray(ReadOnlyMemory m) + { + if (!MemoryMarshal.TryGetArray(m, out var arr)) + { + throw new InvalidOperationException("Can't get base array"); + } + return arr; + } + + public static ArgumentException BufferTooSmall(int expected, int actual, string name) => new ArgumentException($"Require {expected} byte buffer, received {actual} byte", name); + + public static bool MemEqual(Memory m1, Memory m2) + { + if (m1.Length != m2.Length) return false; + for (var i = 0; i < m1.Length; i++) + { + if (m1.Span[i] != m2.Span[i]) return false; + } + + return true; + } + + public static void SodiumIncrement(Span salt) + { + for (var i = 0; i < salt.Length; ++i) + { + if (++salt[i] != 0) + { + break; + } + } + } + } +} diff --git a/shadowsocks-windows.sln b/shadowsocks-windows.sln index 4bff9697..b66c3070 100644 --- a/shadowsocks-windows.sln +++ b/shadowsocks-windows.sln @@ -33,9 +33,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shadowsocks.WPF", "Shadowso EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shadowsocks.Interop", "Shadowsocks.Interop\Shadowsocks.Interop.csproj", "{1CC6E8A9-1875-430C-B2BB-F227ACD711B1}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shadowsocks.Tests", "Shadowsocks.Tests\Shadowsocks.Tests.csproj", "{8923E1ED-2594-4668-A4FA-DC2CFF7EA1CA}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shadowsocks.Tests", "Shadowsocks.Tests\Shadowsocks.Tests.csproj", "{8923E1ED-2594-4668-A4FA-DC2CFF7EA1CA}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shadowsocks.WPF.Tests", "Shadowsocks.WPF.Tests\Shadowsocks.WPF.Tests.csproj", "{97C056B0-2AEB-4467-AAC9-E0FE0639BA9E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shadowsocks.WPF.Tests", "Shadowsocks.WPF.Tests\Shadowsocks.WPF.Tests.csproj", "{97C056B0-2AEB-4467-AAC9-E0FE0639BA9E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shadowsocks.CLI", "Shadowsocks.CLI\Shadowsocks.CLI.csproj", "{78EB3006-81B0-4C13-9B80-E91766874A57}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shadowsocks.Protocol", "Shadowsocks.Protocol\Shadowsocks.Protocol.csproj", "{94DE5045-4D09-437B-BDE3-679FCAF07A2D}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -79,6 +83,14 @@ Global {97C056B0-2AEB-4467-AAC9-E0FE0639BA9E}.Debug|Any CPU.Build.0 = Debug|Any CPU {97C056B0-2AEB-4467-AAC9-E0FE0639BA9E}.Release|Any CPU.ActiveCfg = Release|Any CPU {97C056B0-2AEB-4467-AAC9-E0FE0639BA9E}.Release|Any CPU.Build.0 = Release|Any CPU + {78EB3006-81B0-4C13-9B80-E91766874A57}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {78EB3006-81B0-4C13-9B80-E91766874A57}.Debug|Any CPU.Build.0 = Debug|Any CPU + {78EB3006-81B0-4C13-9B80-E91766874A57}.Release|Any CPU.ActiveCfg = Release|Any CPU + {78EB3006-81B0-4C13-9B80-E91766874A57}.Release|Any CPU.Build.0 = Release|Any CPU + {94DE5045-4D09-437B-BDE3-679FCAF07A2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {94DE5045-4D09-437B-BDE3-679FCAF07A2D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {94DE5045-4D09-437B-BDE3-679FCAF07A2D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {94DE5045-4D09-437B-BDE3-679FCAF07A2D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE