本文我们介绍下如何在Windows系统上开发一个代理本机流量的客户端,并且对接我们之前开发的Socks5服务端,实现整个代理的一条龙。对于Socks5代理的服务端的开发可以详见之前的文章。
本机流量劫持
通过系统开启手动代理
通过c#程序打开Windows的手动代理, 并且设置端口号和IP地址,这样只要客户端监听该端口就可以获取到本机的Http的流量数据。
通过对注册表的修改,来开启本机的手动代理,并且设置端口,Ip设置为本机,因为客户端是本机启动的,端口设置不冲突的即可。
黑名单则是设置哪些域名或者IP段不走代理,我们这里先把局域网的排除掉。
// 引入Windows API[DllImport("wininet.dll")]public static extern bool InternetSetOption(IntPtr hInternet, int dwOption, IntPtr lpBuffer, int dwBufferLength);public const int INTERNET_OPTION_SETTINGS_CHANGED = 39;public const int INTERNET_OPTION_REFRESH = 37;// 设置系统代理public static void SetProxy(string proxyServer, bool enable){ const string userRoot ="HKEY_CURRENT_USER"; const string subkey ="Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings"; const string keyName = userRoot +"\\" + subkey; // 设置代理服务器地址和端口 Microsoft.Win32.Registry.SetValue(keyName,"ProxyServer", proxyServer); // 启用或禁用代理 Microsoft.Win32.Registry.SetValue(keyName,"ProxyEnable", enable ? 1 : 0); // 通知系统设置已更改 InternetSetOption(IntPtr.Zero, INTERNET_OPTION_SETTINGS_CHANGED, IntPtr.Zero, 0); InternetSetOption(IntPtr.Zero, INTERNET_OPTION_REFRESH, IntPtr.Zero, 0);}/// <summary>/// 黑名单/// </summary>/// <param name="exceptions"></param>public static void SetProxyExceptions(string exceptions){ const string userRoot ="HKEY_CURRENT_USER"; const string subkey ="Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings"; const string keyName = userRoot +"\\" + subkey; Microsoft.Win32.Registry.SetValue(keyName,"ProxyOverride", exceptions);}
SystemProxy.SetProxy($"127.0.0.1:{App.SettingsModel.LocalPort}", true); SystemProxy.SetProxyExceptions("localhost;127.*;10.*;172.16.*;172.17.*;172.18.*;172.19.*;172.20.*;172.21.*;172.22.*;172.23.*;172.24.*;172.25.*;172.26.*;172.27.*;172.28.*;172.29.*;172.30.*;172.31.*;192.168.*;<local>");
客户端开启对应TCP服务
_tcpListener = new TcpListener(IPAddress.Any, _httpProxyPort); _tcpListener.Start();
主要就是开启一个监听服务,让操作系统将对应的流量转发到我们的Socks5客户端。
将http报文进行解析,获取到请求的targetHost和Port,这样才能后面在和Socks5服务端握手的时候才能告知对方需要代理的远端信息。
解析系统的Http请求
/// <summary> /// 解析http请求报文,提取关键信息 /// </summary> /// <param name="request">http请求</param> /// <param name="host">请求主机</param> /// <param name="port">请求主机端口号</param> /// <returns>是否解析成功</returns> private bool TryParseHttpRequest(string request, out string host, out int port) { host = null; port = 0; // 解析 CONNECT 请求(如 CONNECT example.com:443 HTTP/1.1) if (request.StartsWith("CONNECT")) { var parts = request.Split(' ')[1].Split(':'); host = parts[0]; port = int.Parse(parts[1]); return true; } // 2. 处理 GET/POST 请求(HTTP) using (var reader = new StringReader(request)) { string line; while ((line = reader.ReadLine()) != null) { // 从 Host 头提取目标 if (line.StartsWith("Host:", StringComparison.OrdinalIgnoreCase)) { var hostParts = line.Substring(5).Trim().Split(':'); host = hostParts[0]; if (hostParts.Length > 1) port = int.Parse(hostParts[1]); return true; } // 空行表示头结束 if (string.IsNullOrWhiteSpace(line)) break; } } // 3. 旧式HTTP/1.0请求可能没有Host头,从URL解析 if (request.StartsWith("GET") || request.StartsWith("POST")) { var url = request.Split(' ')[1]; if (Uri.TryCreate(url, UriKind.Absolute, out var uri)) { host = uri.Host; port = uri.Port; return true; } } return false; }
远程Socks5服务端握手
这里我们采用的是带有认证的握手,需要服务端也开启认证配置,这里对于握手协议和认证还不清楚的可以看我集合的上面一篇文章
将http的远程信息作为握手信息与服务端建立连接,让服务端建立与目标的连接代理。
/// <summary> /// 带有身份验证的登录 /// </summary> /// <param name="socks5Stream"></param> /// <param name="targetHost"></param> /// <param name="targetPort"></param> /// <param name="username"></param> /// <param name="password"></param> /// <returns></returns> /// <exception cref="Exception"></exception> private async Task PerformSocks5Handshake(NetworkStream socks5Stream, string targetHost, int targetPort, string username, string password) { // === 1. 协商认证方法 === // 发送支持的认证方法:无认证(0x00) 和 用户名/密码(0x02) var authMethods = new byte[] { 0x05, 0x02, 0x00, 0x02 }; await socks5Stream.WriteAsync(authMethods, 0, authMethods.Length); // 读取服务器选择的认证方法 var authResponse = new byte[2]; await socks5Stream.ReadAsync(authResponse, 0, 2); if (authResponse[1] == 0xFF) throw new Exception("SOCKS5服务器不支持任何提供的认证方法"); // === 2. 用户名/密码认证 === if (authResponse[1] == 0x02) { // 构建认证请求包 var authRequest = new byte[3 + username.Length + password.Length]; authRequest[0] = 0x01; // 认证子协商版本 authRequest[1] = (byte)username.Length; Encoding.ASCII.GetBytes(username).CopyTo(authRequest, 2); authRequest[2 + username.Length] = (byte)password.Length; Encoding.ASCII.GetBytes(password).CopyTo(authRequest, 3 + username.Length); await socks5Stream.WriteAsync(authRequest, 0, authRequest.Length); // 读取认证响应 var authResult = new byte[2]; await socks5Stream.ReadAsync(authResult, 0, 2); if (authResult[1] != 0x00) throw new Exception("SOCKS5用户名/密码认证失败"); } // === 3. 发送连接请求 === var request = new byte[7 + targetHost.Length]; request[0] = 0x05; // VER request[1] = 0x01; // CMD=CONNECT request[2] = 0x00; // RSV request[3] = 0x03; // ATYP=域名 request[4] = (byte)targetHost.Length; Encoding.ASCII.GetBytes(targetHost).CopyTo(request, 5); BitConverter.GetBytes(IPAddress.HostToNetworkOrder((short)targetPort)).CopyTo(request, 5 + targetHost.Length); await socks5Stream.WriteAsync(request, 0, request.Length); // === 4. 读取连接响应 === var response = new byte[10]; await socks5Stream.ReadAsync(response, 0, 10); if (response[1] != 0x00) throw new Exception($"SOCKS5连接失败 (状态码: {response[1]})"); }
交换流量
所谓的交换流量就是把远程代理的流量和本机的请求流量通过客户端作为中间人来转发
private async Task ForwardDataAsync(NetworkStream src, NetworkStream dest) { var buffer = new byte[4096]; int bytesRead; while ((bytesRead = await src.ReadAsync(buffer, 0, buffer.Length)) > 0) { await dest.WriteAsync(buffer, 0, bytesRead); } }
await Task.WhenAny(ForwardDataAsync(httpStream, socks5Stream),ForwardDataAsync(socks5Stream, httpStream)
代理流量显示
客户端也需要显示上传和下载流量的一些显示,我们这里简单点,因为我们之前开发的服务端是有基于用户的流量统计的,所以只需要把数据获取到就行,一般情况下为了性能,也可以做双端统计减少压力。
我们这里通过SSE将用户的流量信息基于用户名推送到客户端。
public async Task ConnectAsync(string remoteAddress, string userName){ _httpClient = new HttpClient(new HttpClientHandler { Proxy = new WebProxy($"http://{remoteAddress}:5000"), // 明确指定代理 }); _cts = new CancellationTokenSource(); try { using var response = await _httpClient .GetAsync($"http://{remoteAddress}:5000/account/flow/{userName}", HttpCompletionOption.ResponseHeadersRead,_cts.Token); if (response.IsSuccessStatusCode) { using var stream = await response.Content.ReadAsStreamAsync(); using var reader = new StreamReader(stream); while (!_cts.Token.IsCancellationRequested) { var line = await reader.ReadLineAsync(); if (!string.IsNullOrEmpty(line)) { MessageReceived?.Invoke(line); } } } } catch (Exception ex) { Console.WriteLine($"SSE连接错误: {ex.Message}"); }}
验证
远端开启服务端
开启客户端
添加一些配置
包括:服务端IP,服务端Port,本地代理Port,用户名密码
开启客户端
可以看到代理成功,走的是本机的代理和服务端的代理请求
结尾
因为本身代理采用的修改系统代理设置是一种基础设置,所谓可能存在下面影响:
仅影响支持系统代理的应用(部分UWP应用、游戏等会绕过
无法代理非HTTP/HTTPS流量(如DNS、UDP