找回密码
 立即注册
首页 业界区 业界 如何使用 websocket 完成 socks5 网络穿透

如何使用 websocket 完成 socks5 网络穿透

硫辨姥 2025-6-2 23:53:02
有盆友好奇所谓的网络穿透是怎么做的
然后talk is cheap,please show code
所以只好写个简单且常见的websocket例子,
这里的例子大致是这个原理
浏览器插件(或者其他)首先将正常访问请求  --> 转换为socks5访问 --> 假代理服务器建立websocket链接,然后传输socks5协议数据 --> 允许websocket的网关由于不解析websocket数据而不知道是socks5所以未做拦截 --> 真代理服务器从websocket中解析socks5进行转发处理
代码如下
Socks5 --> websocket 端
  1. internal class Socks5ToWSMiddleware : ITcpProxyMiddleware
  2. {
  3.     private readonly IForwarderHttpClientFactory httpClientFactory;
  4.     private readonly ILoadBalancingPolicyFactory loadBalancing;
  5.     private readonly ProxyLogger logger;
  6.     private readonly TimeProvider timeProvider;
  7.     public Socks5ToWSMiddleware(IForwarderHttpClientFactory httpClientFactory, ILoadBalancingPolicyFactory loadBalancing, ProxyLogger logger, TimeProvider timeProvider)
  8.     {
  9.         this.httpClientFactory = httpClientFactory;
  10.         this.loadBalancing = loadBalancing;
  11.         this.logger = logger;
  12.         this.timeProvider = timeProvider;
  13.     }
  14.     public Task InitAsync(ConnectionContext context, CancellationToken token, TcpDelegate next)
  15.     {
  16.         // 过滤符合的路由配置
  17.         var feature = context.Features.Get<IL4ReverseProxyFeature>();
  18.         if (feature is not null)
  19.         {
  20.             var route = feature.Route;
  21.             if (route is not null && route.Metadata is not null
  22.                 && route.Metadata.TryGetValue("socks5ToWS", out var b) && bool.TryParse(b, out var isSocks5) && isSocks5)
  23.             {
  24.                 feature.IsDone = true;
  25.                 route.ClusterConfig?.InitHttp(httpClientFactory);
  26.                 return Proxy(context, feature, token);
  27.             }
  28.         }
  29.         return next(context, token);
  30.     }
  31.     private async Task Proxy(ConnectionContext context, IL4ReverseProxyFeature feature, CancellationToken token)
  32.     { // loadBalancing 选取有效 ip
  33.         var route = feature.Route;
  34.         var cluster = route.ClusterConfig;
  35.         DestinationState selectedDestination;
  36.         if (cluster is null)
  37.         {
  38.             selectedDestination = null;
  39.         }
  40.         else
  41.         {
  42.             selectedDestination = feature.SelectedDestination;
  43.             selectedDestination ??= loadBalancing.PickDestination(feature);
  44.         }
  45.         if (selectedDestination is null)
  46.         {
  47.             logger.NotFoundAvailableUpstream(route.ClusterId);
  48.             Abort(context);
  49.             return;
  50.         }
  51.         selectedDestination.ConcurrencyCounter.Increment();
  52.         try
  53.         {
  54.             await SendAsync(context, feature, selectedDestination, cluster, route.Transformer, token);
  55.             selectedDestination.ReportSuccessed();
  56.         }
  57.         catch
  58.         {
  59.             selectedDestination.ReportFailed();
  60.             throw;
  61.         }
  62.         finally
  63.         {
  64.             selectedDestination.ConcurrencyCounter.Decrement();
  65.         }
  66.     }
  67.     private async Task<ForwarderError> SendAsync(ConnectionContext context, IL4ReverseProxyFeature feature, DestinationState selectedDestination, ClusterConfig? cluster, IHttpTransformer transformer, CancellationToken token)
  68.     {
  69.         // 创建 websocket 请求, 这里为了简单,只创建简单 http1.1 websocket
  70.         var destinationPrefix = selectedDestination.Address;
  71.         if (destinationPrefix is null || destinationPrefix.Length < 8)
  72.         {
  73.             throw new ArgumentException("Invalid destination prefix.", nameof(destinationPrefix));
  74.         }
  75.         var route = feature.Route;
  76.         var requestConfig = cluster.HttpRequest ?? ForwarderRequestConfig.Empty;
  77.         var httpClient = cluster.HttpMessageHandler ?? throw new ArgumentNullException("httpClient");
  78.         var destinationRequest = new HttpRequestMessage();
  79.         destinationRequest.Version = HttpVersion.Version11;
  80.         destinationRequest.VersionPolicy = HttpVersionPolicy.RequestVersionOrLower;
  81.         destinationRequest.Method = HttpMethod.Get;
  82.         destinationRequest.RequestUri ??= new Uri(destinationPrefix, UriKind.Absolute);
  83.         destinationRequest.Headers.TryAddWithoutValidation(HeaderNames.Connection, HeaderNames.Upgrade);
  84.         destinationRequest.Headers.TryAddWithoutValidation(HeaderNames.Upgrade, HttpForwarder.WebSocketName);
  85.         destinationRequest.Headers.TryAddWithoutValidation(HeaderNames.SecWebSocketVersion, "13");
  86.         destinationRequest.Headers.TryAddWithoutValidation(HeaderNames.SecWebSocketKey, ProtocolHelper.CreateSecWebSocketKey());
  87.         destinationRequest.Content = new EmptyHttpContent();
  88.         if (!string.IsNullOrWhiteSpace(selectedDestination.Host))
  89.         {
  90.             destinationRequest.Headers.TryAddWithoutValidation(HeaderNames.Host, selectedDestination.Host);
  91.         }
  92.         
  93.         // 建立websocket 链接,成功则直接 复制原始 req/resp 数据,不做任何而外处理
  94.         var destinationResponse = await httpClient.SendAsync(destinationRequest, token);
  95.         if (destinationResponse.StatusCode == HttpStatusCode.SwitchingProtocols)
  96.         {
  97.             using var destinationStream = await destinationResponse.Content.ReadAsStreamAsync(token);
  98.             var clientStream = new DuplexPipeStreamAdapter<Stream>(null, context.Transport, static i => i);
  99.             var activityCancellationSource = ActivityCancellationTokenSource.Rent(route.Timeout);
  100.             var requestTask = StreamCopier.CopyAsync(isRequest: true, clientStream, destinationStream, StreamCopier.UnknownLength, timeProvider, activityCancellationSource,
  101.                 autoFlush: destinationResponse.Version == HttpVersion.Version20, token).AsTask();
  102.             var responseTask = StreamCopier.CopyAsync(isRequest: false, destinationStream, clientStream, StreamCopier.UnknownLength, timeProvider, activityCancellationSource, token).AsTask();
  103.             var task = await Task.WhenAny(requestTask, responseTask);
  104.             await clientStream.DisposeAsync();
  105.             if (task.IsCanceled)
  106.             {
  107.                 Abort(context);
  108.                 activityCancellationSource.Cancel();
  109.                 if (task.Exception is not null)
  110.                 {
  111.                     throw task.Exception;
  112.                 }
  113.             }
  114.         }
  115.         else
  116.         {
  117.             Abort(context);
  118.             return ForwarderError.UpgradeRequestDestination;
  119.         }
  120.         return ForwarderError.None;
  121.     }
  122.     public Task<ReadOnlyMemory<byte>> OnRequestAsync(ConnectionContext context, ReadOnlyMemory<byte> source, CancellationToken token, TcpProxyDelegate next)
  123.     {
  124.         return next(context, source, token);
  125.     }
  126.     public Task<ReadOnlyMemory<byte>> OnResponseAsync(ConnectionContext context, ReadOnlyMemory<byte> source, CancellationToken token, TcpProxyDelegate next)
  127.     {
  128.         return next(context, source, token);
  129.     }
  130.     private static void Abort(ConnectionContext upstream)
  131.     {
  132.         upstream.Transport.Input.CancelPendingRead();
  133.         upstream.Transport.Output.CancelPendingFlush();
  134.         upstream.Abort();
  135.     }
  136. }
复制代码
websocket --> Socks5 端
  1. internal class WSToSocks5HttpMiddleware : IMiddleware
  2. {
  3.     private static ReadOnlySpan<byte> EncodedWebSocketKey => "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"u8;
  4.     private WebSocketMiddleware middleware;
  5.     private readonly Socks5Middleware socks5Middleware;
  6.     public WSToSocks5HttpMiddleware(IOptions<WebSocketOptions> options, ILoggerFactory loggerFactory, Socks5Middleware socks5Middleware)
  7.     {
  8.         middleware = new WebSocketMiddleware(Scoks5, options, loggerFactory);
  9.         this.socks5Middleware = socks5Middleware;
  10.     }
  11.     private async Task Scoks5(HttpContext context)
  12.     {
  13.         var upgradeFeature = context.Features.Get<IHttpUpgradeFeature>();
  14.         // 检查是否未正确 websocket 请求
  15.         var f = context.Features.Get<IHttpWebSocketFeature>();
  16.         if (f.IsWebSocketRequest)
  17.         {
  18.             // 返回 websocket 接受信息
  19.             var responseHeaders = context.Response.Headers;
  20.             responseHeaders.Connection = HeaderNames.Upgrade;
  21.             responseHeaders.Upgrade = HttpForwarder.WebSocketName;
  22.             responseHeaders.SecWebSocketAccept = CreateResponseKey(context.Request.Headers.SecWebSocketKey.ToString());
  23.             var stream = await upgradeFeature!.UpgradeAsync(); // Sets status code to 101
  24.             
  25.             // 建原始 websocket stream 包装成 pipe 方便使用原来的 socks5Middleware 实现
  26.             var memoryPool = context is IMemoryPoolFeature s ? s.MemoryPool : MemoryPool<byte>.Shared;
  27.             StreamPipeReaderOptions readerOptions = new StreamPipeReaderOptions
  28.             (
  29.                 pool: memoryPool,
  30.                 bufferSize: memoryPool.GetMinimumSegmentSize(),
  31.                 minimumReadSize: memoryPool.GetMinimumAllocSize(),
  32.                 leaveOpen: true,
  33.                 useZeroByteReads: true
  34.             );
  35.             var writerOptions = new StreamPipeWriterOptions
  36.             (
  37.                 pool: memoryPool,
  38.                 leaveOpen: true
  39.             );
  40.             var input = PipeReader.Create(stream, readerOptions);
  41.             var output = PipeWriter.Create(stream, writerOptions);
  42.             var feature = context.Features.Get<IReverseProxyFeature>();
  43.             var route = feature.Route;
  44.             using var cts = CancellationTokenSourcePool.Default.Rent(route.Timeout);
  45.             var token = cts.Token;
  46.             context.Features.Set<IL4ReverseProxyFeature>(new L4ReverseProxyFeature() { IsDone = true, Route = route });
  47.             // socks5Middleware 进行转发
  48.             await socks5Middleware.Proxy(new WebSocketConnection(context.Features)
  49.             {
  50.                 Transport = new WebSocketDuplexPipe() { Input = input, Output = output },
  51.                 ConnectionId = context.Connection.Id,
  52.                 Items = context.Items,
  53.             }, null, token);
  54.         }
  55.         else
  56.         {
  57.             context.Response.StatusCode = StatusCodes.Status400BadRequest;
  58.         }
  59.     }
  60.     public static string CreateResponseKey(string requestKey)
  61.     {
  62.         // "The value of this header field is constructed by concatenating /key/, defined above in step 4
  63.         // in Section 4.2.2, with the string "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", taking the SHA-1 hash of
  64.         // this concatenated value to obtain a 20-byte value and base64-encoding"
  65.         // https://tools.ietf.org/html/rfc6455#section-4.2.2
  66.         // requestKey is already verified to be small (24 bytes) by 'IsRequestKeyValid()' and everything is 1:1 mapping to UTF8 bytes
  67.         // so this can be hardcoded to 60 bytes for the requestKey + static websocket string
  68.         Span<byte> mergedBytes = stackalloc byte[60];
  69.         Encoding.UTF8.GetBytes(requestKey, mergedBytes);
  70.         EncodedWebSocketKey.CopyTo(mergedBytes[24..]);
  71.         Span<byte> hashedBytes = stackalloc byte[20];
  72.         var written = SHA1.HashData(mergedBytes, hashedBytes);
  73.         if (written != 20)
  74.         {
  75.             throw new InvalidOperationException("Could not compute the hash for the 'Sec-WebSocket-Accept' header.");
  76.         }
  77.         return Convert.ToBase64String(hashedBytes);
  78.     }
  79.     public Task InvokeAsync(HttpContext context, RequestDelegate next)
  80.     {
  81.        // 过滤符合的路由配置
  82.         var feature = context.Features.Get<IReverseProxyFeature>();
  83.         if (feature is not null)
  84.         {
  85.             var route = feature.Route;
  86.             if (route is not null && route.Metadata is not null
  87.                 && route.Metadata.TryGetValue("WSToSocks5", out var b) && bool.TryParse(b, out var isSocks5) && isSocks5)
  88.             {
  89.                 // 这里偷个懒,利用现成的 WebSocketMiddleware 检查 websocket 请求,
  90.                 return middleware.Invoke(context);
  91.             }
  92.         }
  93.         return next(context);
  94.     }
  95. }
  96. internal class WebSocketConnection : ConnectionContext
  97. {
  98.     public WebSocketConnection(IFeatureCollection features)
  99.     {
  100.         this.features = features;
  101.     }
  102.     public override IDuplexPipe Transport { get; set; }
  103.     public override string ConnectionId { get; set; }
  104.     private IFeatureCollection features;
  105.     public override IFeatureCollection Features => features;
  106.     public override IDictionary<object, object?> Items { get; set; }
  107. }
  108. internal class WebSocketDuplexPipe : IDuplexPipe
  109. {
  110.     public PipeReader Input { get; set; }
  111.     public PipeWriter Output { get; set; }
  112. }
复制代码
所以利用 websocket 伪装的例子大致就是这样就可以完成 tcp的 socks5 处理了 udp我就不来了
最后有兴趣的同学给 L4/L7的代理 VKProxy 点个赞呗 (暂时没有使用文档,等啥时候有空把配置ui站点完成了再来吧)

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册