SOCKS5 是最简单、最通用的代理协议:几个字节的握手,之后就是一根透明的管子。这篇自己实现了一个 SOCKS5 服务器,手工发每个字节,看 socks5 与 socks5h 到底谁解析域名,认证和各种错误怎么表示,以及它怎么转发 UDP。
客户端问候 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 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 控制连接结束。