- CLI: client, server, utilities (unfinished) - Merge v5/pipelines by @studentmain into main (unfinished)pull/3089/head
| @@ -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 | |||
| @@ -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 }} | |||
| @@ -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<int> Main(string[] args) | |||
| { | |||
| var clientCommand = new Command("client", "Shadowsocks client."); | |||
| clientCommand.AddAlias("c"); | |||
| clientCommand.AddOption(new Option<string?>("--listen", "The address and port to listen on for both SOCKS5 and HTTP proxy.")); | |||
| clientCommand.AddOption(new Option<string?>("--listen-socks", "The address and port to listen on for SOCKS5 proxy.")); | |||
| clientCommand.AddOption(new Option<string?>("--listen-http", "The address and port to listen on for HTTP proxy.")); | |||
| clientCommand.AddOption(new Option<string>("--server-address", "Address of the remote Shadowsocks server to connect to.")); | |||
| clientCommand.AddOption(new Option<int>("--server-port", "Port of the remote Shadowsocks server to connect to.")); | |||
| clientCommand.AddOption(new Option<string>("--method", "Encryption method to use for the remote Shadowsocks server.")); | |||
| clientCommand.AddOption(new Option<string?>("--password", "Password to use for the remote Shadowsocks server.")); | |||
| clientCommand.AddOption(new Option<string?>("--key", "Encryption key (NOT password!) to use for the remote Shadowsocks server.")); | |||
| clientCommand.AddOption(new Option<string?>("--plugin", "Plugin binary path.")); | |||
| clientCommand.AddOption(new Option<string?>("--plugin-opts", "Plugin options.")); | |||
| clientCommand.AddOption(new Option<string?>("--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); | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,40 @@ | |||
| <Project Sdk="Microsoft.NET.Sdk"> | |||
| <PropertyGroup> | |||
| <OutputType>Exe</OutputType> | |||
| <TargetFramework>net5.0</TargetFramework> | |||
| <Nullable>enable</Nullable> | |||
| <AssemblyName>sscli</AssemblyName> | |||
| <PackageId>Shadowsocks.CLI</PackageId> | |||
| <Authors>Clowwindy & The Community</Authors> | |||
| <Product>Shadowsocks CLI</Product> | |||
| <Description>CLI for Shadowsocks server and client implementation in C#.</Description> | |||
| <Copyright>© 2021 Clowwindy & The Community</Copyright> | |||
| <PackageLicenseFile>LICENSE.txt</PackageLicenseFile> | |||
| <PackageProjectUrl>https://github.com/shadowsocks/shadowsocks-windows</PackageProjectUrl> | |||
| <RepositoryUrl>https://github.com/shadowsocks/shadowsocks-windows</RepositoryUrl> | |||
| <RepositoryType>Public</RepositoryType> | |||
| <PackageIcon>ssw128.png</PackageIcon> | |||
| </PropertyGroup> | |||
| <ItemGroup> | |||
| <None Include="..\LICENSE.txt"> | |||
| <Pack>True</Pack> | |||
| <PackagePath></PackagePath> | |||
| </None> | |||
| <None Include="..\Shadowsocks.WPF\Resources\ssw128.png"> | |||
| <Pack>True</Pack> | |||
| <PackagePath></PackagePath> | |||
| </None> | |||
| </ItemGroup> | |||
| <ItemGroup> | |||
| <PackageReference Include="System.CommandLine" Version="2.0.0-beta1.20574.7" /> | |||
| </ItemGroup> | |||
| <ItemGroup> | |||
| <ProjectReference Include="..\Shadowsocks.Interop\Shadowsocks.Interop.csproj" /> | |||
| <ProjectReference Include="..\Shadowsocks.Protocol\Shadowsocks.Protocol.csproj" /> | |||
| </ItemGroup> | |||
| </Project> | |||
| @@ -0,0 +1,12 @@ | |||
| using System.IO.Pipelines; | |||
| using System.Threading.Tasks; | |||
| namespace Shadowsocks.Protocol.Direct | |||
| { | |||
| public class PortForwardService : IStreamService | |||
| { | |||
| public async Task<IDuplexPipe> Handle(IDuplexPipe pipe) => await Task.FromResult<IDuplexPipe>(null); | |||
| public Task<bool> IsMyClient(IDuplexPipe pipe) => Task.FromResult(true); | |||
| } | |||
| } | |||
| @@ -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); | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,18 @@ | |||
| using System; | |||
| namespace Shadowsocks.Protocol | |||
| { | |||
| public interface IProtocolMessage : IEquatable<IProtocolMessage> | |||
| { | |||
| public int Serialize(Memory<byte> buffer); | |||
| /// <summary> | |||
| /// | |||
| /// </summary> | |||
| /// <param name="buffer"></param> | |||
| /// <returns>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 | |||
| /// </returns> | |||
| public (bool success, int length) TryLoad(ReadOnlyMemory<byte> buffer); | |||
| } | |||
| } | |||
| @@ -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); | |||
| } | |||
| } | |||
| @@ -0,0 +1,11 @@ | |||
| using System.IO.Pipelines; | |||
| using System.Threading.Tasks; | |||
| namespace Shadowsocks.Protocol | |||
| { | |||
| public interface IStreamService | |||
| { | |||
| Task<bool> IsMyClient(IDuplexPipe pipe); | |||
| Task<IDuplexPipe> Handle(IDuplexPipe pipe); | |||
| } | |||
| } | |||
| @@ -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, | |||
| }; | |||
| } | |||
| } | |||
| } | |||
| @@ -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<T> ReadAsync<T>(int millisecond) where T : IProtocolMessage, new() | |||
| { | |||
| var delay = new CancellationTokenSource(); | |||
| delay.CancelAfter(millisecond); | |||
| return await ReadAsync<T>(delay.Token); | |||
| } | |||
| public async Task<T> ReadAsync<T>(T ret, int millisecond) where T : IProtocolMessage | |||
| { | |||
| var delay = new CancellationTokenSource(); | |||
| delay.CancelAfter(millisecond); | |||
| return await ReadAsync(ret, delay.Token); | |||
| } | |||
| public async Task<T> ReadAsync<T>(CancellationToken token = default) where T : IProtocolMessage, new() => await ReadAsync(new T(), token); | |||
| public async Task<T> ReadAsync<T>(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<byte>.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<byte> 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<byte> _lastFrame; | |||
| public ReadOnlyMemory<byte> MakeFrame(ReadOnlySequence<byte> 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<byte> 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; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,13 @@ | |||
| <Project Sdk="Microsoft.NET.Sdk"> | |||
| <PropertyGroup> | |||
| <TargetFramework>net5.0</TargetFramework> | |||
| <Nullable>enable</Nullable> | |||
| </PropertyGroup> | |||
| <ItemGroup> | |||
| <PackageReference Include="BouncyCastle.NetCore" Version="1.8.8" /> | |||
| <PackageReference Include="Pipelines.Sockets.Unofficial" Version="2.2.0" /> | |||
| </ItemGroup> | |||
| </Project> | |||
| @@ -0,0 +1,69 @@ | |||
| using Shadowsocks.Protocol.Shadowsocks.Crypto; | |||
| using System; | |||
| using System.Diagnostics.CodeAnalysis; | |||
| namespace Shadowsocks.Protocol.Shadowsocks | |||
| { | |||
| class AeadBlockMessage : IProtocolMessage | |||
| { | |||
| public Memory<byte> Data; | |||
| private readonly int tagLength; | |||
| private readonly ICrypto aead; | |||
| private Memory<byte> nonce; | |||
| private int expectedDataLength; | |||
| public AeadBlockMessage(ICrypto aead, Memory<byte> 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<byte> buffer) | |||
| { | |||
| var len = Data.Length + 2 * tagLength + 2; | |||
| if (buffer.Length < len) | |||
| throw Util.BufferTooSmall(len, buffer.Length, nameof(buffer)); | |||
| Memory<byte> 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<byte> buffer) | |||
| { | |||
| int len; | |||
| if (expectedDataLength == 0) | |||
| { | |||
| if (buffer.Length < tagLength + 2) return (false, tagLength + 2); | |||
| // decrypt length | |||
| Memory<byte> 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); | |||
| } | |||
| } | |||
| } | |||
| @@ -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<byte> 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<byte> 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<ReadOnlyMemory<byte>> SplitBigChunk(ReadOnlyMemory<byte> mem) | |||
| { | |||
| var l = new List<ReadOnlyMemory<byte>>(mem.Length / 0x3fff + 1); | |||
| while (mem.Length > 0x3fff) | |||
| { | |||
| l.Add(mem.Slice(0, 0x3fff)); | |||
| mem = mem.Slice(0x4000); | |||
| } | |||
| l.Add(mem); | |||
| return l; | |||
| } | |||
| } | |||
| } | |||
| @@ -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<byte> nonce, Span<byte> plain, ReadOnlySpan<byte> 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<byte> nonce, ReadOnlySpan<byte> plain, Span<byte> cipher) | |||
| { | |||
| aes.Encrypt( | |||
| nonce, | |||
| plain, | |||
| cipher[0..plain.Length], | |||
| cipher.Slice(plain.Length, parameter.TagSize)); | |||
| return plain.Length + parameter.TagSize; | |||
| } | |||
| } | |||
| } | |||
| @@ -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; | |||
| } | |||
| } | |||
| @@ -0,0 +1,34 @@ | |||
| using System; | |||
| using System.Collections.Generic; | |||
| namespace Shadowsocks.Protocol.Shadowsocks.Crypto | |||
| { | |||
| static class CryptoProvider | |||
| { | |||
| static Dictionary<string, CryptoParameter> parameters = new Dictionary<string, CryptoParameter> | |||
| { | |||
| ["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<IDefaultCrypto>().GetDefaultMethod(); | |||
| } | |||
| method = method.ToLowerInvariant(); | |||
| var ok = parameters.TryGetValue(method, out var t); | |||
| if (!ok) | |||
| { | |||
| //t = parameters[DefaultCipher]; | |||
| throw new NotImplementedException(); | |||
| } | |||
| return t; | |||
| } | |||
| } | |||
| } | |||
| @@ -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<MD5> Md5Hasher = new ThreadLocal<MD5>(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<byte> MD5(Span<byte> span) | |||
| { | |||
| Span<byte> 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<byte> HKDF(int keylen, Span<byte> master, Span<byte> salt, Span<byte> 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<byte>(); | |||
| 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<byte> salt) | |||
| { | |||
| for (var i = 0; i < salt.Length; ++i) | |||
| { | |||
| if (++salt[i] != 0) | |||
| { | |||
| break; | |||
| } | |||
| } | |||
| } | |||
| public static void RandomSpan(Span<byte> span) | |||
| { | |||
| using (var rng = RandomNumberGenerator.Create()) | |||
| { | |||
| rng.GetBytes(span); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,28 @@ | |||
| using System; | |||
| namespace Shadowsocks.Protocol.Shadowsocks.Crypto | |||
| { | |||
| class UnsafeNoneCrypto : ICrypto | |||
| { | |||
| public UnsafeNoneCrypto(CryptoParameter parameter) | |||
| { | |||
| } | |||
| public int Decrypt(ReadOnlySpan<byte> nonce, Span<byte> plain, ReadOnlySpan<byte> cipher) | |||
| { | |||
| cipher.CopyTo(plain); | |||
| return plain.Length; | |||
| } | |||
| public int Encrypt(ReadOnlySpan<byte> nonce, ReadOnlySpan<byte> plain, Span<byte> cipher) | |||
| { | |||
| plain.CopyTo(cipher); | |||
| return plain.Length; | |||
| } | |||
| public void Init(byte[] key, byte[] iv) | |||
| { | |||
| } | |||
| } | |||
| } | |||
| @@ -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<byte> nonce, ReadOnlySpan<byte> plain, Span<byte> cipher); | |||
| int Decrypt(ReadOnlySpan<byte> nonce, Span<byte> plain, ReadOnlySpan<byte> cipher); | |||
| } | |||
| } | |||
| @@ -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); | |||
| } | |||
| } | |||
| } | |||
| @@ -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<byte> 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<byte> buffer) | |||
| { | |||
| Salt.CopyTo(buffer); | |||
| return length; | |||
| } | |||
| public (bool success, int length) TryLoad(ReadOnlyMemory<byte> buffer) | |||
| { | |||
| if (buffer.Length < length) return (false, length); | |||
| buffer.Slice(0, length).CopyTo(Salt); | |||
| return (true, length); | |||
| } | |||
| } | |||
| } | |||
| @@ -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); | |||
| } | |||
| } | |||
| } | |||
| @@ -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<byte> 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); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -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<Socks5MethodSelectionMessage>(); | |||
| 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<Socks5UserPasswordResponseMessage>(); | |||
| 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<Socks5ReplyMessage>(); | |||
| if (reply.Reply != Socks5Message.ReplySucceed) throw new Exception(); | |||
| await DuplexPipe.CopyDuplexPipe(client, server); | |||
| } | |||
| } | |||
| } | |||
| @@ -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<byte> buffer); | |||
| public abstract (bool success, int length) TryLoad(ReadOnlyMemory<byte> 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<byte> 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<byte> 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 | |||
| } | |||
| } | |||
| @@ -0,0 +1,37 @@ | |||
| using System; | |||
| namespace Shadowsocks.Protocol.Socks5 | |||
| { | |||
| public class Socks5MethodSelectionMessage : Socks5Message | |||
| { | |||
| // 5 auth | |||
| public byte SelectedAuth; | |||
| public override int Serialize(Memory<byte> 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<byte> 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; | |||
| } | |||
| } | |||
| } | |||
| @@ -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<byte> 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<byte> 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; | |||
| } | |||
| } | |||
| } | |||
| @@ -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<string, string> passwords) | |||
| { | |||
| enablePassword = true; | |||
| this.passwords = passwords; | |||
| } | |||
| private readonly bool enablePassword; | |||
| private readonly Dictionary<string, string> passwords = new Dictionary<string, string>(); | |||
| public static int ReadTimeout = 120000; | |||
| public async Task<bool> 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<IDuplexPipe> Handle(IDuplexPipe pipe) | |||
| { | |||
| var pmp = new ProtocolMessagePipe(pipe); | |||
| var hs = await pmp.ReadAsync<Socks5VersionIdentifierMessage>(); | |||
| 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<Socks5UserPasswordRequestMessage>(); | |||
| 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<Socks5RequestMessage>(); | |||
| 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; | |||
| } | |||
| } | |||
| } | |||
| @@ -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<byte> 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<byte> 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); | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,47 @@ | |||
| using System; | |||
| namespace Shadowsocks.Protocol.Socks5 | |||
| { | |||
| public class Socks5UserPasswordRequestMessage : Socks5Message | |||
| { | |||
| // 1 lUser [User] lPassword [Password] | |||
| public Memory<byte> User; | |||
| public Memory<byte> Password; | |||
| public override int Serialize(Memory<byte> 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<byte> 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); | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,37 @@ | |||
| using System; | |||
| namespace Shadowsocks.Protocol.Socks5 | |||
| { | |||
| public class Socks5UserPasswordResponseMessage : Socks5Message | |||
| { | |||
| // 1 success | |||
| public bool Success; | |||
| public override int Serialize(Memory<byte> 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<byte> 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; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,42 @@ | |||
| using System; | |||
| namespace Shadowsocks.Protocol.Socks5 | |||
| { | |||
| public class Socks5VersionIdentifierMessage : Socks5Message | |||
| { | |||
| // 5 lAuth [Auth] | |||
| public Memory<byte> Auth; | |||
| public override int Serialize(Memory<byte> 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<byte> 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); | |||
| } | |||
| } | |||
| } | |||
| @@ -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); | |||
| } | |||
| } | |||
| } | |||
| @@ -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<IStreamService> _services; | |||
| public TcpPipeListener(IPEndPoint localEP) | |||
| { | |||
| _listener = new TcpListener(localEP); | |||
| _services = new[] { new Socks5Service(), }; | |||
| } | |||
| public TcpPipeListener(IPEndPoint endPoint, IEnumerable<IStreamService> 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(); | |||
| } | |||
| } | |||
| @@ -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<byte> GetArray(ReadOnlyMemory<byte> 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<byte> m1, Memory<byte> 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<byte> salt) | |||
| { | |||
| for (var i = 0; i < salt.Length; ++i) | |||
| { | |||
| if (++salt[i] != 0) | |||
| { | |||
| break; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -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 | |||