昨天介紹了下微軟的反向代理庫YARP,今天准備實現一個簡單的Http正向代理服務器玩下。首先還是介紹下背景知識:
普通代理(Http)
在Http的時代,大部分是走的RFC 7230中描述的普通代理。這種代理扮演的是「中間人」角色,對於連接到它的客戶端來說,它是服務端;對於要連接的服務端來說,它是客戶端。它就負責在兩端之間來回傳送 HTTP 報文。它的流程是:
-
客戶端瀏覽器將請求原封不動的發送給代理服務器
-
代理服務器從HttpHeader中獲取目標的主機地址,將請求發送給目標主機
-
目標主機將響應回傳給代理服務器
-
代理服務器將響應回傳給客戶端瀏覽器。
-
對於客戶端瀏覽器來說,代理服務器就是目標web服務器。
-
對於web服務器來說來說,它會把代理當做客戶端,完全察覺不到真正客戶端的存在(代理服務器可以通過X-Forwarded-IP這樣的自定義頭部告訴服務端真正的客戶端 IP)。
這種代理服務器實現是比較簡單的,基本上是原封不動的透傳,主要是第2步,需要從header中識別目標主機地址。
隧道代理(Https)
到了Https時代,這種方式就有問題了,代理服務器是一個web服務器,它是影響了客戶端和服務器的TLS加密連接的。此時主要使用RFC中定義的通過 Web 代理服務器用隧道方式傳輸基於 TCP 的協議的隧道代理方式,它的主要流程為:
-
瀏覽器首先發送Http Connect請求給代理服務器,發送目標主機信息。
-
代理服務器建立和目標主機的tcp鏈接,並向瀏覽器回應 Connection Established應答。
-
瀏覽器將請求發送給代理服務器,代理服務器透傳給目標主機。
-
目標主機將響應回給代理服務器,代理服務器將響應回給瀏覽器。
這種模式下,和Sock5等代理協議非常類似了,代理服務器完全就是一個透傳的管道了。只不過是通過http協議協商建立起管道而已。建立連接后,代理服務器只起轉發的作用,理論上也適用於轉發其它TCP協議。
功能實現
兩種代理服務器實際上流程是大同小異的,主要是識別目標主機的指令不同,以及交互的方式有所差異,建立連接和完成第一次交互后,后面基本上都是透傳。從0開始實現也基本上就幾十行代碼,實現帶調試幾個小時差不多可以搞定,如下是我的一個簡單的實現,基於.net core 3.1。

static void Main(string[] args) { ProxyServer.Run(); Thread.Sleep(-1); } class ProxyServer { public static void Run() { TcpServer.Run(3000, async tcp => { using var handlder = new ProxyHandler(tcp); await handlder.Process(); }); } } class ProxyHandler : IDisposable { TcpClient _tcp; TcpClient _remoteTcp; public ProxyHandler(TcpClient tcp) { _tcp = tcp; _buffer = MemoryPool<byte>.Shared.Rent(1024 * 3); } IMemoryOwner<byte> _buffer; Memory<byte> _header; public async Task Process() { var count = await _tcp.GetStream().ReadAsync(_buffer.Memory); _header = _buffer.Memory[..count]; parseHeader(out var method, out var endPoint); //Console.WriteLine(endPoint); if (method.Equals("CONNECT", StringComparison.OrdinalIgnoreCase)) { await createTunnel(endPoint); } else { await createProxy(endPoint); } await pipeStream(_tcp.GetStream(), _remoteTcp.GetStream()); } static byte[] _tunnelReply = Encoding.UTF8.GetBytes("HTTP/1.0 200 Connection Established\r\n\r\n"); async ValueTask createTunnel(string endPoint) { var host = endPoint.Split(":"); _remoteTcp = new TcpClient(); await _remoteTcp.ConnectAsync(host[0], int.Parse(host[1])); await _tcp.GetStream().WriteAsync(_tunnelReply); } async ValueTask createProxy(string endPoint) { var host = new Uri(endPoint); _remoteTcp = new TcpClient(); await _remoteTcp.ConnectAsync(host.Host, host.Port); await _remoteTcp.GetStream().WriteAsync(_header); } void parseHeader(out string method, out string endPoint) { var reader = new SequenceReader<byte>(new ReadOnlySequence<byte>(_header)); method = readToSpace(ref reader); endPoint = readToSpace(ref reader); static string readToSpace(ref SequenceReader<byte> r) { //讀到下一個空格 r.TryReadTo(out ReadOnlySpan<byte> buf, (byte)' '); return Encoding.UTF8.GetString(buf); } } async Task pipeStream(Stream s1, Stream s2) { await Task.WhenAll(pipe(s1, s2), pipe(s2, s1)); static async Task pipe(Stream source, Stream target) { try { await source.CopyToAsync(target); } finally { target.Close(); source.Close(); } } } public void Dispose() { _buffer?.Dispose(); _tcp?.Dispose(); _remoteTcp?.Dispose(); } } class TcpServer { public static async void Run(int port, Func<TcpClient, Task> handler) { var listener = new TcpListener(IPAddress.Loopback, port); listener.Start(); while (true) { var tcp = await listener.AcceptTcpClientAsync(); process(tcp, handler); } } static async void process(TcpClient tcp, Func<TcpClient, Task> handler) { try { await Task.Run(() => handler(tcp)); } catch { // ignored } } }
簡單的運行測試了一下,基本功能應該是完善的,穩定運行貌似沒有什么大問題。雖然沒有什么額外的功能,但還是考慮了一下性能的,用了內存池,解析http頭的時候也盡量較少了內存的分配。考慮帶代碼的可讀性,也沒有太必要做到性能的極致。后面有空的話准備用它來寫一個SS客戶端,使之有更好的擴展性。
這種模式下,和Sock5等代理協議非常類似了,代理服務器完全就是一個透傳的管道了。只不過是通過http協議協商建立起管道而已。建立連接后,代理服務器只起轉發的作用,理論上也適用於轉發其它TCP協議。
參考文章