Table of Contents

如何利用中间件扩展实现socks5

socks5 代理协议已经有很多文章说明,这里不再赘述,想了解的可以参见https://zh.wikipedia.org/wiki/SOCKS

这里列举一下核心实现

internal class Socks5Middleware : ITcpProxyMiddleware
{
    private readonly IDictionary<byte, ISocks5Auth> auths;
    private readonly IConnectionFactory tcp;
    private readonly IHostResolver hostResolver;
    private readonly ITransportManager transport;
    private readonly IUdpConnectionFactory udp;

    public Socks5Middleware(IEnumerable<ISocks5Auth> socks5Auths, IConnectionFactory tcp, IHostResolver hostResolver, ITransportManager transport, IUdpConnectionFactory udp)
    {
        this.auths = socks5Auths.ToFrozenDictionary(i => i.AuthType);
        this.tcp = tcp;
        this.hostResolver = hostResolver;
        this.transport = transport;
        this.udp = udp;
    }

    public Task InitAsync(ConnectionContext context, CancellationToken token, TcpDelegate next)
    {
       // 识别是否为 socks5 路由
        var feature = context.Features.Get<IL4ReverseProxyFeature>();
        if (feature is not null)
        {
            var route = feature.Route;
            if (route is not null && route.Metadata is not null
                && route.Metadata.TryGetValue("socks5", out var b) && bool.TryParse(b, out var isSocks5) && isSocks5)
            {
                feature.IsDone = true;
                return Proxy(context, feature, token);
            }
        }
        return next(context, token);
    }

    public Task<ReadOnlyMemory<byte>> OnRequestAsync(ConnectionContext context, ReadOnlyMemory<byte> source, CancellationToken token, TcpProxyDelegate next)
    {
        return next(context, source, token); // 不做任何处理,原样传递
    }

    public Task<ReadOnlyMemory<byte>> OnResponseAsync(ConnectionContext context, ReadOnlyMemory<byte> source, CancellationToken token, TcpProxyDelegate next)
    {
        return next(context, source, token); // 不做任何处理,原样传递
    }

    private async Task Proxy(ConnectionContext context, IL4ReverseProxyFeature feature, CancellationToken token)
    {
        var input = context.Transport.Input;
        var output = context.Transport.Output;
        // 1. socks5 认证
        if (!await Socks5Parser.AuthAsync(input, auths, context, token))
        {
            context.Abort();
        }
        // 2. 获取 socks5 命令请求
        var cmd = await Socks5Parser.GetCmdRequestAsync(input, token);
        IPEndPoint ip = await ResolveIpAsync(context, cmd, token);
        switch (cmd.Cmd)
        {
            case Socks5Cmd.Connect:
            case Socks5Cmd.Bind:
                // 3. 如果为tcp代理,则会在此分支处理,以命令请求中的地址建立tcp链接
                ConnectionContext upstream;
                try
                {
                    upstream = await tcp.ConnectAsync(ip, token);
                }
                catch
                {  // 为了简单,这里异常没有详细分区各种情况
                    await Socks5Parser.ResponeAsync(output, Socks5CmdResponseType.ConnectFail, token);
                    throw;
                }
                // 4. 服务tcp建立成功,通知 client
                await Socks5Parser.ResponeAsync(output, Socks5CmdResponseType.Success, token);
                var task = await Task.WhenAny(
                               context.Transport.Input.CopyToAsync(upstream.Transport.Output, token)
                               , upstream.Transport.Input.CopyToAsync(context.Transport.Output, token));
                if (task.IsCanceled)
                {
                    context.Abort();
                }
                break;

            case Socks5Cmd.UdpAssociate:
                // 3. 如果为udp代理,则会在此分支处理,建立临时 udp 代理服务地址
                var local = context.LocalEndPoint as IPEndPoint;
                var op = new EndPointOptions()
                {
                    EndPoint = new UdpEndPoint(local.Address, 0),
                    Key = Guid.NewGuid().ToString(),
                };
                try
                {
                    var remote = context.RemoteEndPoint;
                    var timeout = feature.Route.Timeout;
                    op.EndPoint = await transport.BindAsync(op, c => ProxyUdp(c as UdpConnectionContext, remote, timeout), token);
                    // 5. tcp 关闭时 需要关闭临时 udp 服务
                    context.ConnectionClosed.Register(state => transport.StopEndpointsAsync(new List<EndPointOptions>() { state as EndPointOptions }, CancellationToken.None).ConfigureAwait(false).GetAwaiter().GetResult(), op);
                }
                catch
                {
                    await Socks5Parser.ResponeAsync(output, Socks5CmdResponseType.ConnectFail, token);
                    throw;
                }
                 // 4. 服务udp建立成功,通知 client 临时udp地址
                await Socks5Parser.ResponeAsync(output, op.EndPoint as IPEndPoint, Socks5CmdResponseType.Success, token);
                break;
        }
    }

    private async Task ProxyUdp(UdpConnectionContext context, EndPoint remote, TimeSpan timeout)
    {
        using var cts = CancellationTokenSourcePool.Default.Rent(timeout);
        var token = cts.Token;
        // 这里用为了简单 同一个临时地址即监听client 也处理 服务端 response,通过端口比较区分, 当然这样存在一定安全问题 
        if (context.RemoteEndPoint.GetHashCode() == remote.GetHashCode())
        {
            var req = Socks5Parser.GetUdpRequest(context.ReceivedBytes);
            IPEndPoint ip = await ResolveIpAsync(req, token);
            // 请求服务,解包原始请求
            await udp.SendToAsync(context.Socket, ip, req.Data, token);
        }
        else
        {
           
            // 服务response,封包
            await Socks5Parser.UdpResponeAsync(udp, context, remote as IPEndPoint, token);
        }
    }

    private async Task<IPEndPoint> ResolveIpAsync(ConnectionContext context, Socks5Common cmd, CancellationToken token)
    {
        IPEndPoint ip = await ResolveIpAsync(cmd, token);
        if (ip is null)
        {
            await Socks5Parser.ResponeAsync(context.Transport.Output, Socks5CmdResponseType.AddressNotAllow, token);
            context.Abort();
        }

        return ip;
    }

    private async Task<IPEndPoint> ResolveIpAsync(Socks5Common cmd, CancellationToken token)
    {
        IPEndPoint ip;
        if (cmd.Domain is not null)
        {
            var ips = await hostResolver.HostResolveAsync(cmd.Domain, token);
            if (ips.Length > 0)
            {
                ip = new IPEndPoint(ips.First(), cmd.Port);
            }
            else
                ip = null;
        }
        else if (cmd.Ip is not null)
        {
            ip = new IPEndPoint(cmd.Ip, cmd.Port);
        }
        else
        {
            ip = null;
        }

        return ip;
    }
}