- CLI: client, server, utilities (unfinished) - Merge v5/pipelines by @studentmain into main (unfinished)pull/3089/head
@@ -7,47 +7,122 @@ on: | |||||
- 'rm' | - 'rm' | ||||
paths-ignore: | paths-ignore: | ||||
- 'README.md' | - 'README.md' | ||||
- 'LICENSE' | |||||
- 'LICENSE.txt' | |||||
pull_request: | pull_request: | ||||
branches-ignore: | branches-ignore: | ||||
- 'v4' | - 'v4' | ||||
- 'rm' | - 'rm' | ||||
paths-ignore: | paths-ignore: | ||||
- 'README.md' | - 'README.md' | ||||
- 'LICENSE' | |||||
- 'LICENSE.txt' | |||||
jobs: | jobs: | ||||
build: | build: | ||||
name: 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: | steps: | ||||
- uses: actions/checkout@v2 | - uses: actions/checkout@v2 | ||||
- name: Restore dependencies | - name: Restore dependencies | ||||
if: matrix.os == 'windows-latest' | |||||
run: dotnet restore | run: dotnet restore | ||||
- name: Build | - name: Build | ||||
if: matrix.os == 'windows-latest' | |||||
run: dotnet build --no-restore | run: dotnet build --no-restore | ||||
- name: Test | - name: Test | ||||
if: matrix.os == 'windows-latest' | |||||
run: dotnet test --no-build --verbosity normal | 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 | 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 | # 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 | 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 | # uses: actions/upload-artifact@v2 | ||||
# with: | # with: | ||||
# name: shadowsocks-wpf-${{ github.sha }}-windows-arm64 | # name: shadowsocks-wpf-${{ github.sha }}-windows-arm64 | ||||
# path: Shadowsocks.WPF/bin/Release/net5.0-windows10.0.19041.0/win-arm64/publish/ | # 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 | uses: actions/upload-artifact@v2 | ||||
with: | with: | ||||
name: shadowsocks-wpf-${{ github.sha }}-windows-x64 | name: shadowsocks-wpf-${{ github.sha }}-windows-x64 | ||||
path: Shadowsocks.WPF/bin/Release/net5.0-windows10.0.19041.0/win-x64/publish/ | 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 | uses: actions/upload-artifact@v2 | ||||
with: | with: | ||||
name: shadowsocks-wpf-${{ github.sha }}-windows | name: shadowsocks-wpf-${{ github.sha }}-windows | ||||
@@ -8,22 +8,57 @@ on: | |||||
jobs: | jobs: | ||||
publish_upload: | publish_upload: | ||||
name: Publish and 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: | steps: | ||||
- uses: actions/checkout@v2 | - uses: actions/checkout@v2 | ||||
- name: Restore dependencies | - name: Restore dependencies | ||||
if: matrix.os == 'windows-latest' | |||||
run: dotnet restore | run: dotnet restore | ||||
- name: Build | - name: Build | ||||
if: matrix.os == 'windows-latest' | |||||
run: dotnet build --no-restore | run: dotnet build --no-restore | ||||
- name: Test | - name: Test | ||||
if: matrix.os == 'windows-latest' | |||||
run: dotnet test --no-build --verbosity normal | 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 | 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 | # 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 | run: dotnet publish Shadowsocks.WPF -c Release -r win-x64 --self-contained | ||||
# Get version | # Get version | ||||
- name: Get version | - name: Get version | ||||
@@ -31,8 +66,23 @@ jobs: | |||||
shell: bash | shell: bash | ||||
run: echo ::set-output name=VERSION::$(echo $GITHUB_REF | cut -d / -f 3) | run: echo ::set-output name=VERSION::$(echo $GITHUB_REF | cut -d / -f 3) | ||||
# Package | # 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 | - name: Package for Windows | ||||
if: matrix.os == 'windows-latest' | |||||
run: | | run: | | ||||
# WPF | |||||
cd Shadowsocks.WPF/bin/Release/net5.0-windows10.0.19041.0/publish | 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 -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 . | 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 | cd ../../win-x64/publish | ||||
7z a -tzip -mx=9 -mfb=128 ../../shadowsocks-wpf-${{ steps.get_version.outputs.VERSION }}-windows-x64.zip . | 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 . | 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 | # 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 | uses: svenstaro/upload-release-action@v2 | ||||
with: | with: | ||||
repo_token: ${{ secrets.GITHUB_TOKEN }} | 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 | EndProject | ||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shadowsocks.Interop", "Shadowsocks.Interop\Shadowsocks.Interop.csproj", "{1CC6E8A9-1875-430C-B2BB-F227ACD711B1}" | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shadowsocks.Interop", "Shadowsocks.Interop\Shadowsocks.Interop.csproj", "{1CC6E8A9-1875-430C-B2BB-F227ACD711B1}" | ||||
EndProject | 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 | 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 | EndProject | ||||
Global | Global | ||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution | 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}.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.ActiveCfg = Release|Any CPU | ||||
{97C056B0-2AEB-4467-AAC9-E0FE0639BA9E}.Release|Any CPU.Build.0 = 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 | EndGlobalSection | ||||
GlobalSection(SolutionProperties) = preSolution | GlobalSection(SolutionProperties) = preSolution | ||||
HideSolutionNode = FALSE | HideSolutionNode = FALSE | ||||