逐字节读懂 SOCKS5:握手、认证与流量中继
1. 三次交互流程:协商、认证与连接
SOCKS5 协议(RFC 1928)的核心交互分为三个阶段,全程基于 TCP 完成控制信令交换:
- 方法协商(Method Negotiation):客户端告知支持的认证方式,服务端从中选择一种。
- 身份认证(Authentication):若协商结果为用户名密码(
0x02),则按 RFC 1929 交换凭据;若为0x00则跳过。 - 连接建立(Connection Request):客户端发送目标地址与命令,服务端返回绑定地址或错误码,随后开始透明中继。
整个流程在应用层与传输层之间建立了一条“受控管道”,之后所有业务流量均直接转发,不再携带 SOCKS 头部。
2. 报文结构逐字节拆解
2.1 客户端问候(Client Greeting)
| 字段 | 长度 | 示例值 | 说明 |
|---|---|---|---|
VER | 1 | 0x05 | 协议版本 |
NMETHODS | 1 | 0x02 | 支持的方法数量 |
METHODS | N | 0x00 0x02 | 无认证 / 用户名密码 |
2.2 服务端回应(Server Greeting)
| 字段 | 长度 | 示例值 | 说明 |
|---|---|---|---|
VER | 1 | 0x05 | 协议版本 |
METHOD | 1 | 0x02 | 选中的认证方式(0xFF 表示拒绝) |
2.3 客户端请求(Request)
| 字段 | 长度 | 示例值 | 说明 |
|---|---|---|---|
VER | 1 | 0x05 | 协议版本 |
CMD | 1 | 0x01 | 01连接 / 02绑定 / 03UDP关联 |
RSV | 1 | 0x00 | 保留字段 |
ATYP | 1 | 0x03 | 01IPv4 / 03域名 / 04IPv6 |
DST.ADDR | 变长 | 09 65 78... | 目标地址(域名首字节为长度) |
DST.PORT | 2 | 00 50 | 目标端口(大端序) |
2.4 服务端回复(Reply)
结构与请求一致,但 DST.ADDR/PORT 替换为 BND.ADDR/BND.PORT,表示代理服务器实际绑定的中继地址。常用于 UDP ASSOCIATE 获取 UDP 监听端口。
3. CONNECT / BIND / UDP ASSOCIATE 边界
CONNECT (0x01):最常用。代理向目标发起 TCP 连接,成功后双向透传字节流。适用于 HTTP/HTTPS、SSH、数据库等绝大多数协议。BIND (0x02):用于被动连接场景(如 FTP Active Mode)。代理监听一个端口,等待第三方连接,再将连接句柄返回客户端。因 NAT 与防火墙普及,现代极少使用。UDP ASSOCIATE (0x03):建立一条长期 TCP 控制连接,用于协商 UDP 中继参数。业务 UDP 流量走独立通道,控制连接断开即视为会话结束。
4. UDP 透传:控制流与数据流的分离设计
SOCKS5 对 UDP 的支持是其区别于 HTTP 代理的核心优势。协议采用 “控制与数据分离” 架构:
- 客户端发送
UDP ASSOCIATE请求(TCP)。 - 服务端回复
BND.ADDR与BND.PORT,即 UDP 中继监听点。 - 客户端将原始 UDP 报文封装为 SOCKS5 UDP 头部,发送至该中继点:
| 字段 | 长度 | 说明 |
|---|---|---|
RSV | 2 | 固定 0x0000 |
FRAG | 1 | 分片标识(通常 0x00,现代实现极少支持分片) |
ATYP/DST.ADDR/DST.PORT | 变长 | 同请求报文,指明 UDP 最终目的地 |
DATA | 变长 | 原始 UDP 载荷 |
服务端剥离头部后转发至目标,收到目标回复时再反向封装。TCP 控制连接仅用于维持会话存活与错误信令传递,不参与实际数据转发。
5. 状态机与最小伪代码实现
状态流转图
Client Proxy Target
| | |
|--- Greeting(05,[]) ->| |
|<-- Greeting(05,0x) --| |
| (若需认证) | |
|--- Auth Req -------->| |
|<-- Auth Res(0x00) ---| |
|--- Request(CMD,Addr)->| |
| |--- TCP/UDP Conn ---->|
|<-- Reply(BND,REP) ---|<-- Ack/Bind -------|
| | |
|==================== DATA RELAY (Transparent) ====================|
服务端核心逻辑(伪代码)
function handle_client(conn):
ver, nmethods = conn.read(2)
if ver != 0x05: reject()
methods = conn.read(nmethods)
selected = negotiate(methods)
conn.write([0x05, selected])
if selected == AUTH_USERPASS:
authenticate(conn)
cmd, rsv, atyp = conn.read(3)
dst = read_address(conn, atyp)
if cmd == CMD_CONNECT:
target = tcp_connect(dst)
conn.write([0x05, 0x00, 0x00, 0x01, 0,0,0,0, 0,0]) # 成功回复
relay_bidirectional(conn, target)
elif cmd == CMD_UDP_ASSOCIATE:
udp_port = bind_udp_listener()
conn.write([0x05, 0x00, 0x00, 0x01, 0,0,0,0, port_high, port_low])
wait_control_conn_alive() // 保持 TCP 连接
实际生产实现需处理粘包/半包、超时断开、并发连接池与地址解析容错。现代语言(Go/Rust)的异步运行时可极大简化该状态机编写。
6. 总结与下一篇预告
SOCKS5 协议的优雅在于极简的头部设计与严格的控制/数据分离。掌握三次握手与报文结构后,无论是调试连接失败、排查 UDP 丢包,还是二次开发代理网关,都能做到有的放矢。
👉 下一篇《从零搭建 SOCKS5 服务:服务端、客户端与联动玩法》将提供可一键运行的 Docker Compose 模板、主流客户端配置指南与高频排错清单。