- 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 | |||