bigywater.tech
网络协议 · 101

SOCKS5:代理握手与 UDP 转发

SOCKS5 是最简单、最通用的代理协议:几个字节的握手,之后就是一根透明的管子。这篇自己实现了一个 SOCKS5 服务器,手工发每个字节,看 socks5 与 socks5h 到底谁解析域名,认证和各种错误怎么表示,以及它怎么转发 UDP。

25 字节
SOCKS5 CONNECT 握手的上下行总字节(13 + 12),共 2 个往返
ATYP 1 vs 3
socks5 与 socks5h 下代理收到的地址类型(IPv4 / 域名)
10 字节
SOCKS5 的 UDP 数据报头长度(IPv4)

握手

实测 · 本机执行输出
  客户端问候 05 01 00:05 = 版本;01 = 1 种方法;00 = “无需认证”
  服务器选择 05 00:05;00 = 选定“无需认证”(ff 表示没有可接受的方法)
  客户端请求 05 01 00 01 7f 00 00 01 24 ba:05 版本 | 01 = CONNECT | 00 保留 | 01 = 地址类型 IPv4 | 7f 00 00 01 = 127.0.0.1 | 24 ba = 端口 9402
  服务器回复 05 00 00 01 7f 00 00 01 e7 12:05 | 00 = 成功 | 00 保留 | 01 = IPv4 | 7f 00 00 01:59154 = 服务器一侧出站连接的本地地址与端口(BND.ADDR / BND.PORT)
  之后这条 TCP 连接就是一根透明的管子:收到目标发来的 b'hello from target\n'
  整个握手共 2 个往返:问候 / 选择 1 个往返,请求 / 回复 1 个往返(共 13 字节上行,12 字节下行)

问候选认证方法,请求带命令、地址类型、地址、端口,回复带状态码和绑定地址。

谁解析域名

实测 · 本机执行输出
  socks5://:代理收到 CMD=1(1=CONNECT),ATYP=1(IPv4),地址 '104.20.23.154',端口 80;代理连目标用时 160 ms;客户端最终 200,577 字节
  socks5h://:代理收到 CMD=1(1=CONNECT),ATYP=3(域名),地址 'example.com',端口 80;代理连目标用时 163 ms;客户端最终 200,577 字节
  socks5:本机先做 DNS 解析(本机 DNS 查询可能被污染 / 泄露),代理只看到 IP;socks5h:域名交给代理去解析,本机不发 DNS 查询——这是隐私和绕开本地 DNS 的关键区别

socks5 本机解析,socks5h 代理解析;写配置时差一个字母,隐私就不同。

认证与错误

实测 · 本机执行输出
  正确的用户名密码 → 选定方法 02(02 = 用户名/密码);认证答复 01 00(成功)
  错误的密码 → 选定方法 02(02 = 用户名/密码);认证答复 01 01(失败)
  只提供“无需认证”方法 → 服务器答复 05 ff:没有可接受的认证方法
  注意:用户名和密码在 RFC 1929 里是明文传输的(只用于控制代理的访问,没有加密)
  连接一个没有监听的端口:回复 05 05 00 01 00 00 00 00 00 00 → 状态码 5 = 连接被拒绝
  域名无法解析:回复 05 04 00 01 00 00 00 00 00 00 → 状态码 4 = 主机不可达
  不支持的命令(BIND = 2):回复 05 07 00 01 00 00 00 00 00 00 → 状态码 7 = 不支持的命令
  不支持的地址类型(0x09):回复 05 08 00 01 00 00 00 00 00 00 → 状态码 8 = 不支持的地址类型

用户名密码明文(RFC 1929);状态码 5 拒绝、4 主机不可达、7 命令不支持、8 地址类型不支持。

UDP

实测 · 本机执行输出
  UDP ASSOCIATE 回复 05 00 00 01 7f 00 00 01 ca f2:状态 0 = 成功,服务器的 UDP 中继端口 51954
  UDP 数据报头(10 字节)= 00 00 00 01 01 01 01 01 00 35:00 00 = RSV;00 = FRAG(0 = 不分片);01 = IPv4;01 01 01 01 = 1.1.1.1;00 35 = 端口 53;之后是 29 字节的 DNS 查询
  收到 UDP 中继回来的数据报 71 字节(前 10 字节是 SOCKS5 UDP 头,来源 1.1.1.1:53),里面的 DNS 响应:['172.66.147.243', '104.20.23.154'];用时 88 ms
  UDP 中继的生命周期绑定在 TCP 控制连接上:关掉 TCP 控制连接后,服务器就停止中继(服务器日志里记录了 1 个被中继的 UDP 数据报)

UDP ASSOCIATE 之后,每个 UDP 数据报加 10 字节头;中继随 TCP 控制连接结束。

分步演示

SOCKS5
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64(Apple 芯片)/ Python 3.14.8 / tcpdump(libpcap)/ scapy 2.8 / dpkt / dnspython / h2 / aioquic / OpenSSL / curl 8.7.1;抓包在 lo0(环回)上做,少数实验用真实网卡 en0(这台机器上 en0 是 Wi-Fi,tcpdump 看到的是系统转成以太网格式的帧,所以文中的“以太网头”都指这个 14 字节的头)或访问公网(会在文章里注明);需要丢包 / 延迟 / 限速时,用一个只作用于 lo0 端口 9400-9499 的 dummynet 辅助脚本(netlab);所有服务端都是本机自己起的进程;时间戳取自 tcpdump 的 -tt,数字每次运行会有小幅抖动。SOCKS5 服务器是我写的最小实现(线程 + 套接字,只支持 CONNECT 与 UDP ASSOCIATE,不支持 BIND 与 IPv6 的 UDP);客户端第一节是手工拼字节,第二节用 requests + PySocks;UDP 中继测试的 DNS 目标是 1.1.1.1;没有使用任何现成的代理软件
    • 没有验证:BIND 命令(反向连接);GSSAPI 认证;UDP 分片(FRAG 非零);IPv6 目标;大量并发连接下的性能;与 HTTP 代理(w83)在 HTTPS 上的行为对比;Tor 之类的 SOCKS5 实际部署;本机 DNS 被污染时 socks5 与 socks5h 的实际差别(没有去验证污染)
    京ICP备2026067502号-1京公网安备11010502063487号