Browse Source

🐚 Shadowsocks.CLI and Shadowsocks.Protocol

- CLI: client, server, utilities (unfinished)
- Merge v5/pipelines by @studentmain into main (unfinished)
pull/3089/head
database64128 3 years ago
parent
commit
4bdb1340dc
No known key found for this signature in database GPG Key ID: 1CA27546BEDB8B01
37 changed files with 1957 additions and 19 deletions
  1. +86
    -11
      .github/workflows/build.yml
  2. +85
    -6
      .github/workflows/release.yml
  3. +65
    -0
      Shadowsocks.CLI/Program.cs
  4. +40
    -0
      Shadowsocks.CLI/Shadowsocks.CLI.csproj
  5. +12
    -0
      Shadowsocks.Protocol/Direct/PortForwardService.cs
  6. +19
    -0
      Shadowsocks.Protocol/DuplexPipe.cs
  7. +18
    -0
      Shadowsocks.Protocol/IProtocolMessage.cs
  8. +11
    -0
      Shadowsocks.Protocol/IStreamClient.cs
  9. +11
    -0
      Shadowsocks.Protocol/IStreamService.cs
  10. +33
    -0
      Shadowsocks.Protocol/PipePair.cs
  11. +150
    -0
      Shadowsocks.Protocol/ProtocolMessagePipe.cs
  12. +13
    -0
      Shadowsocks.Protocol/Shadowsocks.Protocol.csproj
  13. +69
    -0
      Shadowsocks.Protocol/Shadowsocks/AeadBlockMessage.cs
  14. +110
    -0
      Shadowsocks.Protocol/Shadowsocks/AeadClient.cs
  15. +38
    -0
      Shadowsocks.Protocol/Shadowsocks/Crypto/AeadAesGcmCrypto.cs
  16. +21
    -0
      Shadowsocks.Protocol/Shadowsocks/Crypto/CryptoParameter.cs
  17. +34
    -0
      Shadowsocks.Protocol/Shadowsocks/Crypto/CryptoProvider.cs
  18. +95
    -0
      Shadowsocks.Protocol/Shadowsocks/Crypto/CryptoUtils.cs
  19. +28
    -0
      Shadowsocks.Protocol/Shadowsocks/Crypto/UnsafeNoneCrypto.cs
  20. +12
    -0
      Shadowsocks.Protocol/Shadowsocks/ICrypto.cs
  21. +21
    -0
      Shadowsocks.Protocol/Shadowsocks/PayloadProtocolClient.cs
  22. +37
    -0
      Shadowsocks.Protocol/Shadowsocks/SaltMessage.cs
  23. +41
    -0
      Shadowsocks.Protocol/Shadowsocks/ShadowsocksClient.cs
  24. +86
    -0
      Shadowsocks.Protocol/Shadowsocks/UnsafeClient.cs
  25. +64
    -0
      Shadowsocks.Protocol/Socks5/Socks5Client.cs
  26. +185
    -0
      Shadowsocks.Protocol/Socks5/Socks5Message.cs
  27. +37
    -0
      Shadowsocks.Protocol/Socks5/Socks5MethodSelectionMessage.cs
  28. +76
    -0
      Shadowsocks.Protocol/Socks5/Socks5RequestReplyMessage.cs
  29. +126
    -0
      Shadowsocks.Protocol/Socks5/Socks5Service.cs
  30. +56
    -0
      Shadowsocks.Protocol/Socks5/Socks5UdpMessage.cs
  31. +47
    -0
      Shadowsocks.Protocol/Socks5/Socks5UserPasswordRequestMessage.cs
  32. +37
    -0
      Shadowsocks.Protocol/Socks5/Socks5UserPasswordResponseMessage.cs
  33. +42
    -0
      Shadowsocks.Protocol/Socks5/Socks5VersionIdentifierMessage.cs
  34. +16
    -0
      Shadowsocks.Protocol/TcpPipeClient.cs
  35. +69
    -0
      Shadowsocks.Protocol/TcpPipeListener.cs
  36. +53
    -0
      Shadowsocks.Protocol/Util.cs
  37. +14
    -2
      shadowsocks-windows.sln

+ 86
- 11
.github/workflows/build.yml View File

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


+ 85
- 6
.github/workflows/release.yml View File

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


+ 65
- 0
Shadowsocks.CLI/Program.cs View File

@@ -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);
}
}
}

+ 40
- 0
Shadowsocks.CLI/Shadowsocks.CLI.csproj View File

@@ -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 &amp; The Community</Authors>
<Product>Shadowsocks CLI</Product>
<Description>CLI for Shadowsocks server and client implementation in C#.</Description>
<Copyright>© 2021 Clowwindy &amp; 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>

+ 12
- 0
Shadowsocks.Protocol/Direct/PortForwardService.cs View File

@@ -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);
}
}

+ 19
- 0
Shadowsocks.Protocol/DuplexPipe.cs View File

@@ -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);
}
}
}

+ 18
- 0
Shadowsocks.Protocol/IProtocolMessage.cs View File

@@ -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);
}
}

+ 11
- 0
Shadowsocks.Protocol/IStreamClient.cs View File

@@ -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);
}
}

+ 11
- 0
Shadowsocks.Protocol/IStreamService.cs View File

@@ -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);
}
}

+ 33
- 0
Shadowsocks.Protocol/PipePair.cs View File

@@ -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,
};
}
}
}

+ 150
- 0
Shadowsocks.Protocol/ProtocolMessagePipe.cs View File

@@ -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;
}
}
}

+ 13
- 0
Shadowsocks.Protocol/Shadowsocks.Protocol.csproj View File

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

+ 69
- 0
Shadowsocks.Protocol/Shadowsocks/AeadBlockMessage.cs View File

@@ -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);
}
}
}

+ 110
- 0
Shadowsocks.Protocol/Shadowsocks/AeadClient.cs View File

@@ -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;
}
}
}

+ 38
- 0
Shadowsocks.Protocol/Shadowsocks/Crypto/AeadAesGcmCrypto.cs View File

@@ -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;
}
}
}

+ 21
- 0
Shadowsocks.Protocol/Shadowsocks/Crypto/CryptoParameter.cs View File

@@ -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;
}
}

+ 34
- 0
Shadowsocks.Protocol/Shadowsocks/Crypto/CryptoProvider.cs View File

@@ -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;
}
}
}

+ 95
- 0
Shadowsocks.Protocol/Shadowsocks/Crypto/CryptoUtils.cs View File

@@ -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);
}
}
}
}

+ 28
- 0
Shadowsocks.Protocol/Shadowsocks/Crypto/UnsafeNoneCrypto.cs View File

@@ -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)
{
}
}
}

+ 12
- 0
Shadowsocks.Protocol/Shadowsocks/ICrypto.cs View File

@@ -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);
}
}

+ 21
- 0
Shadowsocks.Protocol/Shadowsocks/PayloadProtocolClient.cs View File

@@ -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);
}
}
}

+ 37
- 0
Shadowsocks.Protocol/Shadowsocks/SaltMessage.cs View File

@@ -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);
}
}
}

+ 41
- 0
Shadowsocks.Protocol/Shadowsocks/ShadowsocksClient.cs View File

@@ -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);
}
}
}

+ 86
- 0
Shadowsocks.Protocol/Shadowsocks/UnsafeClient.cs View File

@@ -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);
}
}
}

}
}

+ 64
- 0
Shadowsocks.Protocol/Socks5/Socks5Client.cs View File

@@ -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);
}
}
}

+ 185
- 0
Shadowsocks.Protocol/Socks5/Socks5Message.cs View File

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

+ 37
- 0
Shadowsocks.Protocol/Socks5/Socks5MethodSelectionMessage.cs View File

@@ -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;
}
}
}

+ 76
- 0
Shadowsocks.Protocol/Socks5/Socks5RequestReplyMessage.cs View File

@@ -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;
}
}
}

+ 126
- 0
Shadowsocks.Protocol/Socks5/Socks5Service.cs View File

@@ -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;
}
}
}

+ 56
- 0
Shadowsocks.Protocol/Socks5/Socks5UdpMessage.cs View File

@@ -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);
}
}
}

+ 47
- 0
Shadowsocks.Protocol/Socks5/Socks5UserPasswordRequestMessage.cs View File

@@ -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);
}
}
}

+ 37
- 0
Shadowsocks.Protocol/Socks5/Socks5UserPasswordResponseMessage.cs View File

@@ -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;
}
}
}

+ 42
- 0
Shadowsocks.Protocol/Socks5/Socks5VersionIdentifierMessage.cs View File

@@ -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);
}
}
}

+ 16
- 0
Shadowsocks.Protocol/TcpPipeClient.cs View File

@@ -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);
}
}
}

+ 69
- 0
Shadowsocks.Protocol/TcpPipeListener.cs View File

@@ -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();
}
}

+ 53
- 0
Shadowsocks.Protocol/Util.cs View File

@@ -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;
}
}
}
}
}

+ 14
- 2
shadowsocks-windows.sln View File

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


Loading…
Cancel
Save