Implementación de túneles SOCKS5 a través de WebSockets para evasión de restricciones

La técnica de tunelización de red permite encapsular protocolos de transporte dentro de otros protocolos más comunes para superar restricciones de firewalls o gateways que solo permiten tráfico web estándar. Un enfoque común y eficiente consiste en envolver el tráfico SOCKS5 dentro de una conexión WebSocket. Debido a que WebSockets comienza como una solicitud HTTP/1.1 normal, la mayoría de los proxies y balanceadores de carga permiten el paso del tráfico sin inspeccionar el contenido binario que fluye posteriormente.

El flujo operativo se divide en dos componentes principales: el emisor, que transforma una conexión SOCKS5 entrante en una petición WebSocket, y el receptor, que extrae los datos del WebSocket para procesarlos nuevamente como SOCKS5.

Middleware de salida: Conversión de SOCKS5 a WebSocket

Este componente actúa como un cliente que intercepta los paquetes del protocolo SOCKS5 y, en lugar de enviarlos directamente al destino, inicia un proceso de "Upgrade" de HTTP para establecer un canal bidireccional mediante WebSockets.

internal class TunnelSocksOverWS : ITcpProxyMiddleware
{
    private readonly IForwarderHttpClientFactory _clientFactory;
    private readonly ILoadBalancingPolicyFactory _balancer;
    private readonly TimeProvider _clock;

    public TunnelSocksOverWS(IForwarderHttpClientFactory clientFactory, ILoadBalancingPolicyFactory balancer, TimeProvider clock)
    {
        _clientFactory = clientFactory;
        _balancer = balancer;
        _clock = clock;
    }

    public async Task InitAsync(ConnectionContext context, CancellationToken ct, TcpDelegate next)
    {
        var proxyFeature = context.Features.Get<IL4ReverseProxyFeature>();
        if (proxyFeature?.Route?.Metadata != null && 
            proxyFeature.Route.Metadata.TryGetValue("useWebSocketTunnel", out var val) && 
            bool.Parse(val))
        {
            proxyFeature.IsDone = true;
            proxyFeature.Route.ClusterConfig?.InitHttp(_clientFactory);
            await ExecuteTunneling(context, proxyFeature, ct);
            return;
        }
        await next(context, ct);
    }

    private async Task ExecuteTunneling(ConnectionContext context, IL4ReverseProxyFeature feature, CancellationToken ct)
    {
        var targetNode = feature.SelectedDestination ?? _balancer.PickDestination(feature);
        if (targetNode == null)
        {
            context.Abort();
            return;
        }

        targetNode.ConcurrencyCounter.Increment();
        try
        {
            await InitiateWebSocketHandshake(context, feature, targetNode, ct);
            targetNode.ReportSuccessed();
        }
        catch
        {
            targetNode.ReportFailed();
            throw;
        }
        finally
        {
            targetNode.ConcurrencyCounter.Decrement();
        }
    }

    private async Task InitiateWebSocketHandshake(ConnectionContext context, IL4ReverseProxyFeature feature, DestinationState node, CancellationToken ct)
    {
        var cluster = feature.Route.ClusterConfig;
        var request = new HttpRequestMessage
        {
            Method = HttpMethod.Get,
            RequestUri = new Uri(node.Address),
            Version = HttpVersion.Version11,
            VersionPolicy = HttpVersionPolicy.RequestVersionOrLower
        };

        // Configuración de cabeceras estándar para WebSocket Upgrade
        request.Headers.TryAddWithoutValidation(HeaderNames.Connection, HeaderNames.Upgrade);
        request.Headers.TryAddWithoutValidation(HeaderNames.Upgrade, "websocket");
        request.Headers.TryAddWithoutValidation(HeaderNames.SecWebSocketVersion, "13");
        request.Headers.TryAddWithoutValidation(HeaderNames.SecWebSocketKey, Guid.NewGuid().ToString("b").Substring(0, 24));
        
        var httpClient = cluster?.HttpMessageHandler ?? throw new InvalidOperationException("HTTP Client not initialized");
        var response = await httpClient.SendAsync(request, ct);

        if (response.StatusCode == HttpStatusCode.SwitchingProtocols)
        {
            using var remoteStream = await response.Content.ReadAsStreamAsync(ct);
            var localStream = new DuplexPipeStreamAdapter<Stream>(null, context.Transport, s => s);
            
            // Copia bidireccional de datos entre el cliente SOCKS5 y el servidor WebSocket
            var clientToTarget = StreamCopier.CopyAsync(true, localStream, remoteStream, -1, _clock, null, ct).AsTask();
            var targetToClient = StreamCopier.CopyAsync(false, remoteStream, localStream, -1, _clock, null, ct).AsTask();

            await Task.WhenAny(clientToTarget, targetToClient);
        }
        else
        {
            context.Abort();
        }
    }
}

Middleware de entrada: Decodificación de WebSocket a SOCKS5

En el extremo del servidor, el middleware debe validar que la petición entrante sea un intento legítimo de cambio de protocolo a WebSocket. Una vez aceptado el Handshake, el flujo de datos se extrae del objeto HttpContext y se redirige a un motor de procesamiento de SOCKS5.

internal class WebSocketToSocksRedirector : IMiddleware
{
    private readonly Socks5Middleware _socksHandler;
    private static readonly byte[] WS_MAGIC_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"u8.ToArray();

    public WebSocketToSocksRedirector(Socks5Middleware socksHandler)
    {
        _socksHandler = socksHandler;
    }

    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        var proxyInfo = context.Features.Get<IReverseProxyFeature>();
        if (proxyInfo?.Route?.Metadata?.ContainsKey("TunnelingEnabled") == true)
        {
            var wsFeature = context.Features.Get<IHttpWebSocketFeature>();
            if (wsFeature != null && wsFeature.IsWebSocketRequest)
            {
                await ProcessSocksTunnel(context);
                return;
            }
        }
        await next(context);
    }

    private async Task ProcessSocksTunnel(HttpContext context)
    {
        var upgrade = context.Features.Get<IHttpUpgradeFeature>();
        var clientKey = context.Request.Headers.SecWebSocketKey.ToString();

        // Respuesta manual de Handshake para establecer el túnel
        context.Response.Headers.Connection = "Upgrade";
        context.Response.Headers.Upgrade = "websocket";
        context.Response.Headers.SecWebSocketAccept = GenerateAcceptKey(clientKey);

        using var rawStream = await upgrade.UpgradeAsync();
        
        var memPool = (context.Features.Get<IMemoryPoolFeature>())?.MemoryPool ?? MemoryPool<byte>.Shared;
        
        var reader = PipeReader.Create(rawStream, new StreamPipeReaderOptions(pool: memPool, leaveOpen: true));
        var writer = PipeWriter.Create(rawStream, new StreamPipeWriterOptions(pool: memPool, leaveOpen: true));

        var tunnelConnection = new InternalWSConnection
        {
            Transport = new WebSocketPipePair { Input = reader, Output = writer },
            ConnectionId = context.Connection.Id,
            Items = context.Items
        };

        // Redirección al manejador interno de protocolo SOCKS5
        await _socksHandler.Proxy(tunnelConnection, null, context.RequestAborted);
    }

    private string GenerateAcceptKey(string key)
    {
        var combined = key + Encoding.UTF8.GetString(WS_MAGIC_GUID);
        var hash = SHA1.HashData(Encoding.UTF8.GetBytes(combined));
        return Convert.ToBase64String(hash);
    }
}

internal class InternalWSConnection : ConnectionContext
{
    public override IDuplexPipe Transport { get; set; }
    public override string ConnectionId { get; set; }
    public override IFeatureCollection Features { get; } = new FeatureCollection();
    public override IDictionary<object, object?> Items { get; set; }
}

internal class WebSocketPipePair : IDuplexPipe
{
    public PipeReader Input { get; init; }
    public PipeWriter Output { get; init; }
}

Consideraciones sobre la arquitectura

Al utilizar este método, el servidor intermedio (como un firewall corporativo o un servicio de balanceo) percibe una conexión de larga duración bajo el protocolo WebSocket. El payload binario de SOCKS5 viaja de forma opaca, permitiendo que las aplicaciones finales se comuniquen mediante TCP sin necesidad de que el firewall comprenda el protocolo SOCKS5 subyacente.

Esta implementación es especialmente útil en infraestructuras donde el tráfico saliente está estrictamente limitado a los puertos 80 y 443, garantizando que el flujo de datos no sea interrumpido por sistemas de inspección de paquetes (DPI) que no realicen un análisis exhaustivo de la capa de aplicación dentro de WebSockets.

Etiquetas: SOCKS5 WebSocket Net-Core C# Network-Tunneling

Publicado el 7-21 14:25