首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> C# >> 關於C#

C# 實作 SOCKS5 代理用戶端協議完整解析與程式碼

     編輯:關於C#

SOCKS5 協議流程 (RFC 1928)

  1. TCP 連線:用戶端連線代理伺服器 (預設 1080)。
  2. 協商認證:
    用戶端 → `VER=5, NMETHODS, METHODS...`
    伺服器 → `VER=5, METHOD` (0=無認證, 2=帳密, FF=不支援)
  3. 認證階段 (若 METHOD=2):
    用戶端 → `VER=1, ULEN, UNAME, PLEN, PASSWD`
    伺服器 → `VER=1, STATUS` (0=成功)
  4. 發送請求:
    `VER=5, CMD=1(CONNECT), RSV=0, ATYP, DST.ADDR, DST.PORT`
    ATYP: 1=IPv4(4bytes), 3=域名(1byte長度+內容), 4=IPv6(16bytes)
  5. 伺服器回應:
    `VER=5, REP, RSV=0, ATYP, BND.ADDR, BND.PORT`
    REP: 0=成功, 1=失敗, 2=規則拒絕, 3=網路不可達, 4=主機不可達, 5=連線被拒, 7=命令不支援, 8=地址類型不支援
  6. 資料轉發:建立成功後,代理變身透明 TCP 管道,雙向轉發資料。

核心請求封包建構 (IPv4 範例)


public static byte[] BuildConnectRequest(IPEndPoint destination)
{
    // VER(1) + CMD(1) + RSV(1) + ATYP(1) + IPv4(4) + PORT(2) = 10 bytes
    var buf = new byte[10];
    buf[0] = 0x05; // VER
    buf[1] = 0x01; // CMD = CONNECT
    buf[2] = 0x00; // RSV
    buf[3] = 0x01; // ATYP = IPv4

    byte[] ipBytes = destination.Address.GetAddressBytes(); // 4 bytes
    Buffer.BlockCopy(ipBytes, 0, buf, 4, 4);

    // Port: Network Byte Order (Big Endian)
    byte[] portBytes = BitConverter.GetBytes(IPAddress.HostToNetworkOrder((short)destination.Port));
    Buffer.BlockCopy(portBytes, 0, buf, 8, 2);

    return buf;
}

完整非同步用戶端實作


using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Threading.Tasks;

public class Socks5Client
{
    private readonly string _proxyHost;
    private readonly int _proxyPort;
    private readonly string _username;
    private readonly string _password;

    public Socks5Client(string proxyHost, int proxyPort, string username = null, string password = null)
    {
        _proxyHost = proxyHost; _proxyPort = proxyPort; _username = username; _password = password;
    }

    public async Task ConnectAsync(string destHost, int destPort, CancellationToken ct = default)
    {
        var tcp = new TcpClient();
        await tcp.ConnectAsync(_proxyHost, _proxyPort, ct);
        var stream = tcp.GetStream();

        // 1. 協商認證方法
        await NegotiateAuthAsync(stream, ct);

        // 2. 發送 CONNECT 請求
        await SendConnectRequestAsync(stream, destHost, destPort, ct);

        // 3. 讀取回應
        await ReadConnectResponseAsync(stream, ct);

        return stream; // 成功後直接用此 Stream 收發資料
    }

    private async Task NegotiateAuthAsync(NetworkStream stream, CancellationToken ct)
    {
        // 傳送支援的方法:無認證(0) + 帳密(2)
        var methods = new byte[] { 0x05, 0x02, 0x00, 0x02 };
        await stream.WriteAsync(methods, 0, methods.Length, ct);
        await stream.FlushAsync(ct);

        // 讀取伺服器選擇
        byte[] resp = new byte[2];
        await ReadExactAsync(stream, resp, 0, 2, ct);
        if (resp[0] != 0x05) throw new IOException("SOCKS 版本不符");
        if (resp[1] == 0xFF) throw new IOException("無支援的認證方法");

        // 若選擇帳密認證 (0x02)
        if (resp[1] == 0x02)
        {
            if (string.IsNullOrEmpty(_username)) throw new InvalidOperationException("需要帳密認證但未提供");
            var auth = new byte[3 + _username.Length + 1 + _password.Length];
            auth[0] = 0x01; // Sub-negotiation version
            auth[1] = (byte)_username.Length;
            Encoding.ASCII.GetBytes(_username).CopyTo(auth, 2);
            auth[2 + _username.Length] = (byte)_password.Length;
            Encoding.ASCII.GetBytes(_password).CopyTo(auth, 3 + _username.Length);
            await stream.WriteAsync(auth, 0, auth.Length, ct);
            await stream.FlushAsync(ct);

            byte[] authResp = new byte[2];
            await ReadExactAsync(stream, authResp, 0, 2, ct);
            if (authResp[1] != 0x00) throw new IOException("帳密認證失敗");
        }
    }

    private async Task SendConnectRequestAsync(NetworkStream stream, string host, int port, CancellationToken ct)
    {
        // 解析目標位址
        IPAddress ip;
        byte atyp;
        byte[] addrBytes;
        if (IPAddress.TryParse(host, out ip) && ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
        {
            atyp = 0x01; addrBytes = ip.GetAddressBytes();
        }
        else if (IPAddress.TryParse(host, out ip) && ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6)
        {
            atyp = 0x04; addrBytes = ip.GetAddressBytes();
        }
        else
        {
            atyp = 0x03; // Domain name
            addrBytes = new byte[1 + Encoding.ASCII.GetByteCount(host)];
            addrBytes[0] = (byte)(addrBytes.Length - 1);
            Encoding.ASCII.GetBytes(host).CopyTo(addrBytes, 1);
        }

        // 組裝封包
        var buf = new byte[4 + addrBytes.Length + 2];
        buf[0] = 0x05; buf[1] = 0x01; buf[2] = 0x00; buf[3] = atyp;
        Buffer.BlockCopy(addrBytes, 0, buf, 4, addrBytes.Length);
        byte[] portBytes = BitConverter.GetBytes(IPAddress.HostToNetworkOrder((short)port));
        Buffer.BlockCopy(portBytes, 0, buf, 4 + addrBytes.Length, 2);

        await stream.WriteAsync(buf, 0, buf.Length, ct);
        await stream.FlushAsync(ct);
    }

    private async Task ReadConnectResponseAsync(NetworkStream stream, CancellationToken ct)
    {
        // 固定前 4 bytes: VER, REP, RSV, ATYP
        byte[] header = new byte[4];
        await ReadExactAsync(stream, header, 0, 4, ct);
        if (header[0] != 0x05) throw new IOException("回應版本錯誤");
        if (header[1] != 0x00) throw new IOException($"代理連線失敗, REP={header[1]:X2}");

        // 略過 BND.ADDR + BND.PORT (依 ATYP 長度)
        int skip = header[3] switch { 0x01 => 4, 0x04 => 16, 0x03 => 1 + stream.ReadByte(), _ => 0 };
        if (skip > 0)
        {
            byte[] dummy = new byte[skip + 2]; // +2 for port
            await ReadExactAsync(stream, dummy, 0, dummy.Length, ct);
        }
    }

    private static async Task ReadExactAsync(Stream stream, byte[] buffer, int offset, int count, CancellationToken ct)
    {
        int read = 0;
        while (read < count)
        {
            int r = await stream.ReadAsync(buffer, offset + read, count - read, ct);
            if (r == 0) throw new EndOfStreamException();
            read += r;
        }
    }
}

// 使用範例
class Program
{
    static async Task Main()
    {
        var client = new Socks5Client("127.0.0.1", 1080, "user", "pass");
        using (var stream = await client.ConnectAsync("example.com", 80))
        using (var writer = new StreamWriter(stream) { AutoFlush = true })
        using (var reader = new StreamReader(stream))
        {
            await writer.WriteLineAsync("GET / HTTP/1.1");
            await writer.WriteLineAsync("Host: example.com");
            await writer.WriteLineAsync("Connection: close");
            await writer.WriteLineAsync("");
            string response = await reader.ReadToEndAsync();
            Console.WriteLine(response);
        }
    }
}

UDP ASSOCIATE 簡述 (進階)

  • 用戶端發送 `CMD=3 (UDP ASSOCIATE)`,綁定本地 UDP 連接埠。
  • 伺服器回應 `BND.PORT`,用戶端向該連接埠發送 UDP 封包,封包格式:`RSV(2) + FRAG(1) + ATYP + DST.ADDR + DST.PORT + DATA`。
  • FRAG 分片機制:0=無分片,1-127=分片序號,>127=最後分片。重組超時 < 5 秒。

總結

SOCKS5 用戶端核心在於 **精確封包組裝/解析 (Big Endian 埠號、ATYP 判斷)** 與 **流式非同步讀寫 (`ReadExactAsync`)**。掌握協商認證、CONNECT 請求、回應碼處理三階段,即可實作穩健的代理穿透功能。生產環境建議加入 **連線池、重試、逾時、健康檢查** 等韌性機制。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved