Location>code7788 >text

How to implement socks5 proxy based on Kestrel

Popularity:582 ℃/2025-04-23 16:56:22

Preface

Made a wheel beforeNZOrz, originally planned to refer to it slowlyKestrelandYarpWriting for a long time

However, Trump came to power, tariffs, engagement cases, himself and his wallet and other fun came one after another, and he had no time to write the wheels slowly.

Some friends also want to know whether they can directly use Kestrel to implement L4 processing.

So in order to make it easier in 2025, the L4/L7 proxy has been implemented again based on KestrelVKProxy(People who are interested can like it), and simply implement socks5 to show it to everyone

(PS: I have limited cognition and ability, and I will never understand/I don’t know anything about Txxxrojan/Sxxxhadowsocks and so on, so please do not consult me, I won’t understand)

How to Unleash Kestrel's Capabilities

As we all know, Kestrel is a web server implemented by Aspnetcore for cross-platform, and only provides the L7 layer capability of http 1/2/3.

But students who have read the source code know that in fact, the Http protocol implemented from the L4 layer (socket) is justOnBindOnly http-related implementations and no relevant public extensions are provided, so its capabilities are limited

But since the code is open source and we also know that dotnet has the ability to overcome access restrictions (Reflection), so it cannot stop our magic claws

(ps
1. However, bypassing the restrictions may beNative AOTThere are problems with the relevant scenarios, and no specific related tests are done yet.
2. There may be API changes in different versions of Kestrel. Currently, in order to save trouble, it does not adapt to the differences between different versions. Net9.0 is temporarily subject to net9.0. After net10 is officially released, it will be migrated to net10. It will no longer be adapted to versions before net9.0.

Example

First, let’s look at completing the effect monitoring and processing tcp/udp/http1/http2/http3 so that everyone can understand our purpose

Simple package releaseKestrelThe ability and simple udp processing capabilities, so you can just use Kestrel to process related content

Install
dotnet add package  --version 0.0.0.1
Program entry
using CoreDemo;
 using;
 using;
 using;

 var app = (args).UseVKProxyCore()
     .ConfigureServices(i =>
     {
         // IListenHandler has been decoupled to listen and process, so everyone can implement it and do whatever they want
         <IListenHandler, TcpListenHandler>();
         <IListenHandler, UdpListenHandler>();
         <IListenHandler, HttpListenHandler>();
     })
     .Build();

 await ();
How to deal with tcp
internal class TcpListenHandler : ListenHandlerBase
 {
     private readonly List<EndPointOptions> endPointOptions = new List<EndPointOptions>();
     private readonly ILogger<TcpListenHandler> logger;
     private readonly IConnectionFactory connectionFactory;

     public TcpListenHandler(ILogger<TcpListenHandler> logger, IConnectionFactory connectionFactory)
     {
          = logger;
          = connectionFactory;
     }

     /// When the program is first started, the relevant initialization operation can be implemented here.
     public override Task InitAsync(CancellationToken cancellationToken)
     {
         (new EndPointOptions()
         {
             EndPoint = ("127.0.0.1:5000"),
             Key = "tcpXXX"
         });
         return ;
     }

     /// This method can be used to monitor which ports and how to handle it. If you need to monitor port changes at runtime, etc., it can be implemented through GetReloadToken and RebindAsync. For simplicity, I will not give any examples here.
     public override async Task BindAsync(ITransportManager transportManager, CancellationToken cancellationToken)
     {
         foreach (var item in endPointOptions)
         {
             try
             {
                 await (item, Proxy, cancellationToken);
                 ($"listen {}");
             }
             catch (Exception ex)
             {
                 (, ex);
             }
         }
     }

     /// Delegate method for processing, the example here is a simple tcp proxy
     private async Task Proxy(ConnectionContext connection)
     {
         ($"begin tcp {} {()} ");
         var upstream = await (new IPEndPoint(("14.215.177.38"), 80));
         var task1 = ();
         var task2 = ();
         await (task1, task2);
         ();
         ();
         ($"end tcp {} {()} ");
     }
 }
How to deal with udp

Simple udp processing is provided by default, so there is no need for everyone to implement the listening loop by themselves. Of course, because the implementation is too simple, complex scenarios may require everyone to implement it by themselves.IConnectionListenerFactoryorIMultiplexedConnectionListenerFactory

internal class UdpListenHandler : ListenHandlerBase
 {
     private readonly ILogger<UdpListenHandler> logger;
     private readonly IUdpConnectionFactory udp;
     private readonly IPEndPoint proxyServer = new(("127.0.0.1"), 11000);

     public UdpListenHandler(ILogger<UdpListenHandler> logger, IUdpConnectionFactory udp)
     {
          = logger;
          = udp;
     }

     public override async Task BindAsync(ITransportManager transportManager, CancellationToken cancellationToken)
     {
         var ip = new EndPointOptions()
         {
             EndPoint = ("127.0.0.1:5000"), // In order to distinguish Kestrel's default tcp implementation, the default tcp listening must be blocked through UdpEndPoint
             Key = "udpXXX"
         };
         await (ip, Proxy, cancellationToken);
         ($"listen {}");
     }

     /// Delegate method for processing, the example here is a simple UDP proxy
     private async Task Proxy(ConnectionContext connection)
     {
         if (connection is UdpConnectionContext context)
         {
             ($"{} received {} from {}");
             var socket = new Socket(, , );
             await (socket, proxyServer, , );
             var r = await (socket, );
             await (, , (), );
         }
     }
 }
How to deal with http
using;
 using;
 using;
 using;
 using;
 using;
 using;
 using;

 namespace CoreDemo;

 public class HttpListenHandler : ListenHandlerBase
 {
    private readonly ILogger<HttpListenHandler> logger;
    private readonly ICertificateLoader certificateLoader;

    public HttpListenHandler(ILogger<HttpListenHandler> logger, ICertificateLoader certificateLoader)
    {
         = logger;
         = certificateLoader;
    }

    private async Task Proxy(HttpContext context)
    {
        var resp = ;
         = 404;
        await (new { });
        await ().ConfigureAwait(false);
    }

    public override async Task BindAsync(ITransportManager transportManager, CancellationToken cancellationToken)
    {
        try
        {
            // http (both http2 and http3 require certificates, so the listening here will be ignored and only listen to http1)
            var ip = new EndPointOptions()
            {
                EndPoint = ("127.0.0.1:4000"),
                Key = "http"
            };
            await (ip, Proxy, cancellationToken);
            ($"listen {}");

            // https
            ip = new EndPointOptions()
            {
                EndPoint = ("127.0.0.1:4001"),
                Key = "https"
            };

            var (c, f) = (new CertificateConfig() { Path = "", Password = "testPassword" }); //Read the certificate
            await (ip, Proxy, cancellationToken, HttpProtocols.Http1AndHttp2AndHttp3, callbackOptions: new HttpsConnectionAdapterOptions()
            {
                //ServerCertificateSelector = (context, host) => c http3 Due to the underlying quic implementation, dynamic ServerCertificate cannot be supported
                ServerCertificate = c,
                CheckCertificateRevocation = false,
                ClientCertificateMode =
            });
            ($"listen {}");
        }
        catch (Exception ex)
        {
            (, ex);
        }
    }
 }

The core points that adapt to Kestrel

The core focus is exposureTransportManagerAPI, so that everyone has the processing power of L4 layer

TransportManagerAdapter implementation

public class TransportManagerAdapter : ITransportManager, IHeartbeat
{
    private static MethodInfo StopAsyncMethod;
    private static MethodInfo StopEndpointsAsyncMethod;
    private static MethodInfo MultiplexedBindAsyncMethod;
    private static MethodInfo BindAsyncMethod;
    private static MethodInfo StartHeartbeatMethod;
    private object transportManager;
    private object heartbeat;
    private object serviceContext;
    private object metrics;
    private int multiplexedTransportCount;
    private int transportCount;
    internal readonly IServiceProvider serviceProvider;

    IServiceProvider  => serviceProvider;

    public TransportManagerAdapter(IServiceProvider serviceProvider, IEnumerable<IConnectionListenerFactory> transportFactories, IEnumerable<IMultiplexedConnectionListenerFactory> multiplexedConnectionListenerFactories)
    {
        (transportManager, heartbeat, serviceContext, metrics) = CreateTransportManager(serviceProvider);
        multiplexedTransportCount = ();
        transportCount = ();
         = serviceProvider;
    }

    private static (object, object, object, object) CreateTransportManager(IServiceProvider serviceProvider)
    {
        foreach (var item in ().DeclaredMethods)
        {
            if ( == "StopAsync")
            {
                StopAsyncMethod = item;
            }
            else if ( == "StopEndpointsAsync")
            {
                StopEndpointsAsyncMethod = item;
            }
            else if ( == "BindAsync")
            {
                if (().Any(i =>  == typeof(ConnectionDelegate)))
                {
                    BindAsyncMethod = item;
                }
                else
                {
                    MultiplexedBindAsyncMethod = item;
                }
            }
        }

        var s = CreateServiceContext(serviceProvider);
        var r = (,
                    (<IConnectionListenerFactory>()).ToList(),
                    (<IMultiplexedConnectionListenerFactory>()).ToList(),
                    CreateHttpsConfigurationService(serviceProvider),
                    
                    );
        return (r, , , );

        static object CreateHttpsConfigurationService(IServiceProvider serviceProvider)
        {
            var CreateLogger = typeof(LoggerFactoryExtensions).GetTypeInfo().(i =>  == "CreateLogger" && );
            var r = ();
            var m = ("Initialize");
            var log = <ILoggerFactory>();
            var l = ().Invoke(null, new object[] { log });
            (r, new object[] { <IHostEnvironment>(), <KestrelServer>(), l });
            return r;
        }

        static (object context, object heartbeat, object metrics) CreateServiceContext(IServiceProvider serviceProvider)
        {
            var m = CreateKestrelMetrics();
            var KestrelCreateServiceContext = ("CreateServiceContext",  | );
            var r = (null, new object[]
            {
                <IOptions<KestrelServerOptions>>(),
                <ILoggerFactory>(),
                null,
                m
            });
            var h = ().(i =>  == "Heartbeat");
            StartHeartbeatMethod = ().(i =>  == "Start");
            return (r, ().Invoke(r, null), m);
        }

        static object CreateKestrelMetrics()
        {
            return (, ());
        }
    }

    public Task<EndPoint> BindAsync(EndPointOptions endpointConfig, ConnectionDelegate connectionDelegate, CancellationToken cancellationToken)
    {
        return (transportManager, new object[] { , connectionDelegate, (), cancellationToken }) as Task<EndPoint>;
    }

    public Task<EndPoint> BindAsync(EndPointOptions endpointConfig, MultiplexedConnectionDelegate multiplexedConnectionDelegate, CancellationToken cancellationToken)
    {
        return (transportManager, new object[] { , multiplexedConnectionDelegate, (), cancellationToken }) as Task<EndPoint>;
    }

    public Task StopEndpointsAsync(List<EndPointOptions> endpointsToStop, CancellationToken cancellationToken)
    {
        return (transportManager, new object[] { (endpointsToStop), cancellationToken }) as Task;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        return (transportManager, new object[] { cancellationToken }) as Task;
    }

    public void StartHeartbeat()
    {
        if (heartbeat != null)
        {
            (heartbeat, null);
        }
    }

    public void StopHeartbeat()
    {
        if (heartbeat is IDisposable disposable)
        {
            ();
        }
    }

    public IConnectionBuilder UseHttpServer(IConnectionBuilder builder, IHttpApplication<> application, HttpProtocols protocols, bool addAltSvcHeader)
    {
        (null, new object[] { builder, serviceContext, application, protocols, addAltSvcHeader });
        return builder;
    }

    public IMultiplexedConnectionBuilder UseHttp3Server(IMultiplexedConnectionBuilder builder, IHttpApplication<> application, HttpProtocols protocols, bool addAltSvcHeader)
    {
        KestrelExtensions.(null, new object[] { builder, serviceContext, application, protocols, addAltSvcHeader });
        return builder;
    }

    public ConnectionDelegate UseHttps(ConnectionDelegate next, HttpsConnectionAdapterOptions tlsCallbackOptions, HttpProtocols protocols)
    {
        if (tlsCallbackOptions == null)
            return next;
        var o = (new object[] { next, tlsCallbackOptions, protocols, <ILoggerFactory>(), metrics });
        return <ConnectionDelegate>(o);
    }

    public async Task BindHttpApplicationAsync(EndPointOptions options, IHttpApplication<> application, CancellationToken cancellationToken, HttpProtocols protocols = HttpProtocols.Http1AndHttp2AndHttp3, bool addAltSvcHeader = true, Action<IConnectionBuilder> config = null
        , Action<IMultiplexedConnectionBuilder> configMultiplexed = null, HttpsConnectionAdapterOptions callbackOptions = null)
    {
        var hasHttp1 = (HttpProtocols.Http1);
        var hasHttp2 = (HttpProtocols.Http2);
        var hasHttp3 = (HttpProtocols.Http3);
        var hasTls = callbackOptions is not null;

        if (hasTls)
        {
            if (hasHttp3)
            {
                ().Protocols = protocols;
                (callbackOptions);
            }
            //(protocols);
            //if (hasHttp3)
            //{
            //    HttpsConnectionAdapterOptions
            //    (callbackOptions);
            //}
        }
        else
        {
            // Http/1 without TLS, no-op HTTP/2 and 3.
            if (hasHttp1)
            {
                hasHttp2 = false;
                hasHttp3 = false;
            }
            // Http/3 requires TLS. Note we only let it fall back to HTTP/1, not HTTP/2
            else if (hasHttp3)
            {
                throw new InvalidOperationException("HTTP/3 requires HTTPS.");
            }
        }

        // Quic isn't registered if it's not supported, throw if we can't fall back to 1 or 2
        if (hasHttp3 && multiplexedTransportCount == 0 && !(hasHttp1 || hasHttp2))
        {
            throw new InvalidOperationException("Unable to bind an HTTP/3 endpoint. This could be because QUIC has not been configured using UseQuic, or the platform doesn't support QUIC or HTTP/3.");
        }

        addAltSvcHeader = addAltSvcHeader && multiplexedTransportCount > 0;

        // Add the HTTP middleware as the terminal connection middleware
        if (hasHttp1 || hasHttp2
            || protocols == )
        {
            if (transportCount == 0)
            {
                throw new InvalidOperationException($"Cannot start HTTP/ or HTTP/2 server if no {nameof(IConnectionListenerFactory)} is registered.");
            }

            var builder = new ConnectionBuilder(serviceProvider);
            config?.Invoke(builder);
            UseHttpServer(builder, application, protocols, addAltSvcHeader);
            var connectionDelegate = UseHttps((), callbackOptions, protocols);

             = await BindAsync(options, connectionDelegate, cancellationToken).ConfigureAwait(false);
        }

        if (hasHttp3 && multiplexedTransportCount > 0)
        {
            var builder = new MultiplexedConnectionBuilder(serviceProvider);
            configMultiplexed?.Invoke(builder);
            UseHttp3Server(builder, application, protocols, addAltSvcHeader);
            var multiplexedConnectionDelegate = ();

             = await BindAsync(options, multiplexedConnectionDelegate, cancellationToken).ConfigureAwait(false);
        }
    }
}

Secondly, by rewritingVKServerThis will remove the influence of the OnBind method so that everyone can use itITransportManagerDo any L4/L7 processing

public class VKServer : IServer
{
    private readonly ITransportManager transportManager;
    private readonly IHeartbeat heartbeat;
    private readonly IListenHandler listenHandler;
    private readonly GeneralLogger logger;
    private bool _hasStarted;
    private int _stopping;
    private readonly SemaphoreSlim _bindSemaphore = new SemaphoreSlim(initialCount: 1);
    private readonly CancellationTokenSource _stopCts = new CancellationTokenSource();
    private readonly TaskCompletionSource _stoppedTcs = new TaskCompletionSource();
    private IDisposable? _configChangedRegistration;

    public VKServer(ITransportManager transportManager, IHeartbeat heartbeat, IListenHandler listenHandler, GeneralLogger logger)
    {
         = transportManager;
         = heartbeat;
         = listenHandler;
         = logger;
    }

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        try
        {
            if (_hasStarted)
            {
                throw new InvalidOperationException("Server already started");
            }
            _hasStarted = true;
            await (cancellationToken);
            ();
            await BindAsync(cancellationToken).ConfigureAwait(false);
        }
        catch
        {
            Dispose();
            throw;
        }
    }

    private async Task BindAsync(CancellationToken cancellationToken)
    {
        await _bindSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);

        try
        {
            if (_stopping == 1)
            {
                throw new InvalidOperationException("Server has already been stopped.");
            }

            IChangeToken? reloadToken = ();
            await (transportManager, _stopCts.Token).ConfigureAwait(false);
            _configChangedRegistration = reloadToken?.RegisterChangeCallback(TriggerRebind, this);
        }
        finally
        {
            _bindSemaphore.Release();
        }
    }

    private void TriggerRebind(object? state)
    {
        if (state is VKServer server)
        {
            _ = ();
        }
    }

    private async Task RebindAsync()
    {
        await _bindSemaphore.WaitAsync();

        IChangeToken? reloadToken = null;
        try
        {
            if (_stopping == 1)
            {
                return;
            }

            reloadToken = ();
            await (transportManager, _stopCts.Token).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            ("Unable to reload configuration", ex);
        }
        finally
        {
            _configChangedRegistration = reloadToken?.RegisterChangeCallback(TriggerRebind, this);
            _bindSemaphore.Release();
        }
    }

    public async Task StopAsync(CancellationToken cancellationToken)
    {
        if ((ref _stopping, 1) == 1)
        {
            await _stoppedTcs.(false);
            return;
        }

        ();

        _stopCts.Cancel();

        await _bindSemaphore.WaitAsync().ConfigureAwait(false);

        try
        {
            await (transportManager, cancellationToken).ConfigureAwait(false);
            await (cancellationToken).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            _stoppedTcs.TrySetException(ex);
            throw;
        }
        finally
        {
            _configChangedRegistration?.Dispose();
            _stopCts.Dispose();
            _bindSemaphore.Release();
        }

        _stoppedTcs.TrySetResult();
    }

    public void Dispose()
    {
        StopAsync(new CancellationToken(canceled: true)).GetAwaiter().GetResult();
    }
}

How to implement socks5

There are many articles to explain the socks5 proxy protocol, so I won't go into details here. If you want to know, please refer to it./wiki/SOCKS

Here are the core implementations

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)
     {
          = (i => );
          = tcp;
          = hostResolver;
          = transport;
          = udp;
     }

     public Task InitAsync(ConnectionContext context, CancellationToken token, TcpDelegate next)
     {
        // Identify whether it is a socks5 route
         var feature = <IL4ReverseProxyFeature>();
         if (feature is not null)
         {
             var route = ;
             if (route is not null && is not null
                 && ("socks5", out var b) && (b, out var isSocks5) && isSocks5)
             {
                  = 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 = ;
         var output = ;
         // 1. socks5 certification
         if (!await (input, auths, context, token))
         {
             ();
         }
         // 2. Get the socks5 command request
         var cmd = await (input, token);
         IPEndPoint ip = await ResolveIpAsync(context, cmd, token);
         switch ()
         {
             case:
             case:
                 // 3. If it is a tcp proxy, it will be processed in this branch to establish a tcp link with the address in the command request
                 ConnectionContext upstream;
                 try
                 {
                     upstream = await (ip, token);
                 }
                 catch
                 { // For simplicity, there is no detailed partitioning of the exception here.
                     await (output, , token);
                     throw;
                 }
                 // 4. The service tcp is successfully established and notify the client
                 await (output, , token);
                 var task = await (
                                (, token)
                                , (, token));
                 if ()
                 {
                     ();
                 }
                 break;

             case:
                 // 3. If it is an udp proxy, it will be processed in this branch to establish a temporary udp proxy service address.
                 var local = as IPEndPoint;
                 var op = new EndPointOptions()
                 {
                     EndPoint = new UdpEndPoint(, 0),
                     Key = ().ToString(),
                 };
                 try
                 {
                     var remote = ;
                     var timeout = ;
                      = await (op, c => ProxyUdp(c as UdpConnectionContext, remote, timeout), token);
                     // 5. When tcp is closed, the temporary udp service needs to be closed
                     (state => (new List<EndPointOptions>() { state as EndPointOptions }, ).ConfigureAwait(false).GetAwaiter().GetResult(), op);
                 }
                 catch
                 {
                     await (output, , token);
                     throw;
                 }
                  // 4. The service udp is successfully established and notify the client of the temporary udp address
                 await (output, as IPEndPoint, , token);
                 break;
         }
     }

     private async Task ProxyUdp(UdpConnectionContext context, EndPoint remote, TimeSpan timeout)
     {
         using var cts = (timeout);
         var token = ;
         // This is used for simplicity. The same temporary address is used to listen to the client and also handle the server response. It is differentiated by ports. Of course, there are certain security problems.
         if (() == ())
         {
             var req = ();
             IPEndPoint ip = await ResolveIpAsync(req, token);
             // Request service and unpack the original request
             await (, ip, , token);
         }
         else
         {
           
             // Service response, package
             await (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 (, , token);
             ();
         }

         return ip;
     }

     private async Task<IPEndPoint> ResolveIpAsync(Socks5Common cmd, CancellationToken token)
     {
         IPEndPoint ip;
         if ( is not null)
         {
             var ips = await (, token);
             if ( > 0)
             {
                 ip = new IPEndPoint((), );
             }
             else
                 ip = null;
         }
         else if ( is not null)
         {
             ip = new IPEndPoint(, );
         }
         else
         {
             ip = null;
         }

         return ip;
     }
 }

In this way, everyone can see that everyone does not need to be crazywhile(true) { await ... }, Reduced a lot of burden on everyone