Skip to content

第 6 章 SOCKS5:从方法协商到 TCP 与 UDP 转发 ​

返回目录 · 编写进度

SOCKS5 没有要求目标必须理解 SOCKS。目标服务器通常只看到一条普通连接或一个普通 UDP 数据报;SOCKS 控制信息存在于客户端与代理之间。这使它成为理解代理协议的合适起点:可以清楚分开代理控制、身份子协议、目标寻址和原始业务内容。

本章以 RFC 1928 为基础协议依据,以 RFC 1929 解释用户名密码子协议。所有十六进制会话都是按规范构造的示例,不冒充现场抓包;不把某个软件界面的“SOCKS5 支持”当作实现完整规范的证据。

6.1 一个会话中的三种语言 ​

客户端首先建立到 SOCKS 服务的 TCP 连接。常用端口为 1080,但端口只是约定;它既不证明监听者支持 SOCKS5,也不证明连接已经受到加密保护。TCP 建连之后,双方才开始解释 SOCKS 字节。

第一种语言是认证方法协商。客户端报告自己能接受哪些方法,服务器选一种;如果没有可接受方法,连接结束。这里协商的是接下来用什么规则,而不是已经验证某个用户名。选择无认证方法时,无需再交换凭据,但目标连接请求仍然没有发送。

第二种语言是选定方法的子协议。如果选择用户名密码,就按 RFC 1929 的格式交换相应内容;若选择其它方法,就依据它的规范。子协议有自己的版本、消息与结束条件。接收端不能只看到首字节不是 05 就把合法子协议拒绝,也不能把未知子协议字节猜成目标地址。

第三种语言才是SOCKS 请求与结果。客户端表达 CONNECT、BIND 或 UDP ASSOCIATE,并附地址和端口。服务器据此检查策略、准备对应资源,然后回复。响应长得相似,但其地址字段的含义由命令决定,不总是网站地址。

阶段客户端发送的含义成功推进的条件
TCP 建连请求到达代理服务的传输连接连接建立
方法协商可接受的认证方法集合服务器选中可接受方法
方法子协议按所选方法提供验证材料子协议成功,或选择无认证
请求处理命令及相应端点信息收到该命令所需的成功结果
数据转发目标流或带头数据报遵循命令和所选方法的封装规则

普通无认证 CONNECT 成功之后,TCP 中后续目标流不再逐块重复 SOCKS 请求头。UDP ASSOCIATE 则不同:TCP 连接建立关联,实际数据报携带 UDP 头另行传送;不能把它理解为“UDP 字节改在控制 TCP 里连续发送”。BIND 还需要两个结果阶段。分清三条路径,才能确定何时进入可用的转发状态。

认证方法也可能规定对请求、响应和业务数据继续封装。RFC 1928 明确保留了这种能力,因此“SOCKS5 永远明文”和“SOCKS5 自带加密”都过度概括。常见无认证和 RFC 1929 用户名密码路径自身不提供通用保密层;其它方法必须逐项查其规范。把 SOCKS 放在另一个安全通道内,又是对外层的独立选择。RFC 1928 §3—7。

规范与实现也要分别阅读。RFC 1928 对合规实现的 GSSAPI 支持提出 MUST,对用户名密码提出 SHOULD;现实中的某个入口可能只实现它的一部分。不能因常用入口只配置了无认证,就删除规范中的其它路径,也不能因规范列出某能力,就推断手边的软件已经实现。后面的报文实例选择无认证或用户名密码,以便把每一阶段完整展现。

6.2 方法协商:一串候选值不等于已经认证 ​

方法协商的客户端消息非常短,但有明确的数量约束:

字段宽度含义
VER1 字节SOCKS 版本,此处为 05
NMETHODS1 字节后面方法标识的个数,范围 1—255
METHODSNMETHODS 字节每种方法用一字节表示

消息总长为 2+NMETHODS,最短三字节,最长 257 字节。METHODS 是方法标识序列,不是一个按位掩码;00 02 表示提供两种候选方法,不能把两字节合成十六位整数。

本章使用三个取值:00 表示无需认证,01 表示 GSSAPI,02 表示用户名密码;ff 用于服务器表示无可接受方法。RFC 1928 发布时还划分了 IANA 分配和私有方法范围;若使用其它值,应查询相应登记与方法规范,不能把历史范围表当成今天每个值都未使用的证明。RFC 1928 §3。

例如客户端发送:

text
05 02 00 02

偏移 0 是版本;偏移 1 的 02 是候选数量;偏移 2 的 00 与偏移 3 的 02 是两个方法值。服务器如果只接受用户名密码,可以返回:

text
05 02

这个响应固定两字节,第二字节是选中的方法,不再是数量。它表示下一阶段改用用户名密码子协议,没有表示用户名密码已经验证成功。如果选择无认证,返回 05 00,随后可进入请求阶段;如果没有可接受方法,返回 05 ff,客户端必须关闭连接。

选中的方法应来自客户端提供的集合。规范没有把候选数组的排列自动定义成强制偏好顺序,也没有要求服务器一定选择“最强”的方法。双方需要根据自己的安全策略决定哪些候选可接受。若客户端必须认证身份,却把无认证也列为可接受候选,就不能在服务器合法选择 00 后再声称协议保证了用户名密码认证。

这也暴露协商的信任边界:在没有外层保护、没有方法自身提供绑定的场景中,方法列表和选择并不凭空受到密码学认证。主动修改协商的风险,不能只靠“服务器支持密码”解决。客户端拒绝不符合自身策略的选择,可以限制静默回退;完整的抗篡改能力还需要相应保护机制。

解析器必须按数量收齐消息。假设第一次只收到 05 02 00,服务器已经看到无认证,但还缺第二种方法标识;它不能把一次读取结束当成列表结束,更不能把后续 02 当成用户名长度或命令。NMETHODS=0 则不符合规范图中的 1—255 项要求;接收端应按无效协商处理,而不是自行替客户端补上默认方法。

反向也一样:客户端必须收齐两字节方法响应,再解释下一阶段。如果服务器回复 05 02,用户名密码子协议的响应版本之后将是 01,这并不代表 SOCKS 被降级成版本 1;它属于不同的子协议命名空间。仅根据抓包里版本数字变化判断安全性,会忽略控制阶段。

本阶段没有最终目标字段。正常 SOCKS 方法选择服务不必知道用户稍后要访问哪个网站,因此应避免把尚未取得的目标或权限结果提前塞进“协商成功”的含义中。协商成功、认证成功和目标连接成功,是三次不同的判断。

6.3 用户名密码:认证不等于加密 ​

当服务器返回 05 02,客户端接下来发送的格式由 RFC 1929 规定:

字段宽度约束
VER1 字节子协议版本 01
ULEN1 字节用户名长度,1—255
UNAMEULEN 字节用户名字节
PLEN1 字节密码长度,1—255
PASSWDPLEN 字节密码字节

长度按线上字节计算。RFC 1929 不提供一套可直接推广为“任意 Unicode 字符串”的统一编码约定;两个实现若分别按不同本地编码处理非 ASCII 名称,即使字符显示相同,送来的字节也可能不同。为把协议定界与编码互操作分开,本节示例只用 ASCII。

使用公开、无实际用途的教学用户名 ann 和教学密码 demo,请求为:

text
01 03 61 6e 6e 04 64 65 6d 6f
偏移范围字节解释
[0,1)01子协议版本,不是 SOCKS 主版本
[1,2)03用户名三字节
[2,5)61 6e 6eann
[5,6)04密码四字节
[6,10)64 65 6d 6fdemo

总长 3+ULEN+PLEN=10 字节。注意 ULEN 只约束紧随其后的 UNAME,不能用它跳过密码长度;PLEN 的位置取决于先前用户名有多长。长度都在一字节范围内,并不意味着可以提前按“固定十字节认证包”读取,因为别的账户长度会不同。

服务端检查凭据以后发送固定两字节响应:

text
01 00

首字节仍是子协议版本,第二字节 STATUS 的 00 表示成功;任何非零状态表示失败,服务器必须关闭连接。RFC 1929 没有给非零值规定一套可依赖的“用户不存在”“密码错误”等细分表,不能把某个软件私有结果推广成标准字段含义。RFC 1929 §2。

成功以后,客户端回到 SOCKS5 的请求语言,下一条请求以 05 开始。完整阶段可能先后出现 05 02、01 ...、01 00、05 01 ...;这些版本与类型值不冲突,每一段都由当前状态决定。接收端必须在认证失败时停止推进,而不是只记录日志后仍尝试读取目标请求。

这里的密码字节是明文发送的。十六进制排版、二进制长度字段、TCP 传输和用户名密码认证,都没有把它变成密文。能观察该未受保护连接内容的一方可以恢复凭据;RFC 1929 的安全说明明确指出这项限制。RFC 1929 §3。

若 SOCKS 连接放在经过验证的安全通道内部,外部链路观察者看到的是外层保护结果;SOCKS 服务器作为认证接收者仍会取得密码字节。若只有随后访问网站的 HTTPS,则网站安全会话开始得更晚,它不会倒过来保护先前已经发给 SOCKS 入口的认证消息。这是安全端点和时间顺序共同造成的边界。

“服务器把密码安全存储”也无法改变线上格式。存储方法决定服务器侧凭据泄漏后的风险;客户端按 RFC 1929 发出的仍是原始密码材料。认证策略还需考虑错误次数、权限范围和入口访问控制,但那些是服务实现问题,不会凭空在此协议中增加挑战、签名或防重放字段。

这一简短子协议很好地展示了安全目标的独立性:它能用于让服务器判断某个账户凭据是否正确,却不自行提供通道保密、服务器身份验证或完整的抗主动篡改机制。后文讨论 VLESS 的身份字段时,也要先问身份材料怎样被保护,而不是因出现“认证”二字就把所有安全性质一并假定。

6.4 CONNECT:请求的目标不等于代理的监听端点 ​

方法阶段完成后,客户端按下列格式发送 SOCKS5 请求。先讨论命令 CONNECT,其值为 01,要求服务器建立到目标的连接。

流内顺序字段宽度解释
1VER105
2CMD101,CONNECT
3RSV1保留字段,必须为 00
4ATYP1地址表示类型
5DST.ADDR随类型目标地址或域名表示
6DST.PORT2目标端口,网络字节序

地址类型不是文本标签:01 后是四字节 IPv4,04 后是十六字节 IPv6,03 后则是一字节名称长度和相应名称字节。域名没有结尾 NUL,也不采用 DNS 报文中逐标签长度与压缩指针的编码。SOCKS 域名字段和真正的 DNS 消息是两份不同格式。RFC 1928 §4—5。

三类请求可以这样构造:

text
IPv4,192.0.2.1:443,共 10 字节:
05 01 00 01 c0 00 02 01 01 bb

域名,example.com:443,共 18 字节:
05 01 00 03 0b 65 78 61 6d 70 6c 65 2e 63 6f 6d 01 bb

IPv6,[2001:db8::1]:443,共 22 字节:
05 01 00 04 20 01 0d b8 00 00 00 00 00 00 00 00 00 00 00 01 01 bb

IPv4 地址中的 c0 是 192,端口中的 01 bb 是 1×256+187=443。这两处都不是把十进制文本编码进报文。IPv6 的十六字节不含显示用的冒号;为它加上字符串长度前缀,反而会破坏地址布局。

以域名请求为例,偏移从本条请求起点计算:

范围内容
[0,4)05 01 00 03,固定前缀
[4,5)0b,名称字节数
[5,16)example.com 的十一字节
[16,18)01 bb,目标端口

通用长度公式是 IPv4 4+4+2=10,IPv6 4+16+2=22,域名 4+1+N+2=N+7。一字节名称长度的表示上限是 255,因而这一格式最多占 262 字节;是否接受某个名称还要检查名称规则、实现限制与策略,不能仅凭它在长度范围内就判定可用。

服务器解析到域名后,负责把它转化成自己的实际连接目标。它可以得到多个候选地址,随后如何排序、并行尝试或回退,不由 SOCKS5 这份字段表统一规定。客户端若已发送 IPv4 地址,服务器没有义务猜回原始名称,也不能假定该地址属于客户端最初输入的唯一网站。

CONNECT 的成功是目标传输连接的成功,不要求服务器解析目标网站的 HTTP 或 TLS。目标端口也只是服务选择信息,不改变后续数据的定界规则。若代理策略不允许访问某类目标,可以拒绝;若命令或地址类型根本不支持,应回复对应 SOCKS 层结果,而不是把完整请求转给一个任意默认网站。

还要特别注意“保留”字段。RSV 必须按规范填零,不能因自己暂时不使用就塞入私有标志,同时期待任意 SOCKS5 实现互通。新能力需要合法扩展机制或双方另有明确约定,不能靠覆写保留字段悄悄升级协议。

本节给出的请求紧接在认证阶段之后,同一 TCP 读取可能把请求与后续字节一起取到,但请求本身没有“等到 Socket 读完”为止的尾部。解析器必须由 ATYP 和名称长度计算精确结束位置,保留未消费内容;目标数据的发送时机仍受成功响应和方法规则约束。

6.5 响应:BND.ADDR 是哪一个地址 ​

SOCKS5 的响应与请求具有相似布局,但 CMD 换成 REP,目标字段换成绑定端点:

text
VER(1) | REP(1) | RSV(1) | ATYP(1) | BND.ADDR(变长) | BND.PORT(2)

VER 为 05,RSV 为 00。ATYP 决定地址部分长度,仍按 IPv4、域名或 IPv6 格式解析。REP 给出请求结果,不能用“收到任何响应”替代对 REP 的检查。

REPRFC 1928 的含义
00成功
01通用 SOCKS 服务器失败
02规则不允许连接
03网络不可达
04主机不可达
05连接被拒绝
06TTL 到期
07不支持的命令
08不支持的地址类型

具体实现怎样把操作系统错误或上游代理错误映射到这些值,需要另外核验;不能仅看代码就精确推断完整物理原因。例如 04 表达主机不可达这一代理层结果,不提供逐级 DNS、路由和防火墙诊断记录。没有细分信息时,01 也可能是唯一能可靠表达的结果。RFC 1928 §6。

CONNECT 成功响应中的 BND.ADDR 和 BND.PORT,按规范表示服务器用于连接目标的本地绑定地址和端口。它们不是请求中的目标端点,也不保证等于客户端访问 SOCKS 服务时使用的地址和端口。服务器可能有多个网卡、多个出口,还可能在其外部遇到 NAT。

沿用前面的端点例子,客户端连接代理 198.51.100.20:1080,代理从 198.51.100.20:41000 连接目标 203.0.113.30:443,一个相应响应为:

text
05 00 00 01 c6 33 64 14 a0 28

其中四字节地址是 198.51.100.20,a0 28 等于 160×256+40=41000。这十字节告诉客户端代理请求成功,并报告本次下游绑定端点。把最后两字节误当网站端口,会得到 41000 而非 443;这不是端口被随意改了,而是两个字段描述不同端点。

地址字段的通用格式不因常见 IPv4 响应而变成固定十字节。若 ATYP 为 IPv6,就要读取二十二字节基本响应;若为域名,就先读取名称长度再计算。只等到 05 00 就切换到目标流,会把 RSV、ATYP 和绑定地址污染进目标应用的数据;一律读取十字节,又会截断其它类型。

假设一次读取获得上述十字节成功响应,再跟随三个目标返回字节。客户端应消费十字节,把后三字节留给已建立的目标流。若成功响应本身只到九字节,连接随后结束,就不能因为 REP 已经是 00 而宣布整个控制响应有效;响应仍然截断。

失败路径也有时间语义。RFC 1928 要求服务器在发送失败结果后尽快结束 TCP,且从检测到失败条件起不超过十秒。这是规范的关闭上限,不是要求所有失败都等待十秒。客户端不得在失败后继续发送业务数据,期待服务器按先前请求悄悄开始转发。

最后,BND 字段的具体含义受命令约束。CONNECT 报告下游连接绑定端点;BIND 的两次响应分别涉及监听端点和连接进来的对端;UDP ASSOCIATE 报告客户端应发送带头数据报的中继端点。统一字段布局让解析器复用定界逻辑,却不把三种命令变成同一种资源操作。

6.6 BIND:成功监听与成功回连是两件事 ​

CONNECT 让代理主动连接目标;某些应用却需要远端服务再建立一条返回客户端方向的数据连接。如果客户端没有可被外部访问的监听入口,可以请求代理代为等待这条连接。SOCKS5 用 BIND 表达这种操作,CMD 为 02。RFC 1928 以带控制与独立数据连接的 FTP 作为动机,但 BIND 的机制不依赖理解全部 FTP 命令。

先把两条客户端—代理 TCP 分开。主业务控制连接可以已经通过一次 CONNECT 建好,用来与远端应用交换控制信息;BIND 使用另一条完成了方法阶段的 SOCKS 连接。BIND 的结果将告诉客户端在哪里等待回连,客户端再通过已有业务控制连接把这个会合端点告诉远端应用。SOCKS 本身不替任意应用生成这条通知。

假设远端应用准备从 203.0.113.30:45000 发起数据连接,本例客户端把预期对端写进 BIND 请求:

text
05 02 00 01 cb 00 71 1e af c8

af c8 为十进制 45000。DST.ADDR 与 DST.PORT 在此用于描述和评估预期回连对端,不是要求代理像 CONNECT 一样主动拨出连接。本例选用已知对端端口,仅为使所有字段明确;实际应用若不能预知来源,需要依实现和策略提供的能力处理,不能假定所有服务器都支持任意通配写法。

服务器创建并绑定可等待回连的 Socket 后,发出第一次响应。例如:

text
05 00 00 01 c6 33 64 14 a4 10

这表示本例的会合端点为 198.51.100.20:42000,因为 a4 10=164×256+16=42000。第一次成功只表示代理准备了等待入口,并不表示预期远端已经连进来。客户端需要把该端点交给应用控制流程,远端才知道去哪里连接。

远端发起连接以后,服务器发出第二次响应。如果预期连接成功,本例可以返回:

text
05 00 00 01 cb 00 71 1e af c8

此时 BND.ADDR 和 BND.PORT 表示连接进来的对端,即 203.0.113.30:45000。同一组字段在第一次响应里是监听端点,第二次则是入站对端;必须结合阶段解释。若预期入站失败,第二次响应应表达失败,而不是把第一次成功视为永久有效。RFC 1928 §6,BIND。

正在绘制图表…

查看图表原文
sequenceDiagram
    participant C as 客户端
    participant P as SOCKS 代理
    participant T as 远端应用
    Note over C,T: 已存在一条业务控制连接,可由另一 CONNECT 承载
    C->>P: 在另一 SOCKS 会话发 BIND
    P->>P: 创建并绑定等待入口
    P-->>C: 第一次响应:监听端点
    C->>T: 经既有控制连接通知会合端点
    T->>P: 发起到会合端点的连接
    P-->>C: 第二次响应:入站结果及对端
    T->>P: 独立数据连接内容
    P->>C: 第二次成功响应之后的数据

第二次成功以后,原来的 BIND TCP 连接才承载这条回连对应的数据。客户端若只消费第一次响应就把后续全部当正文,会把第二次 SOCKS 响应误交给目标应用;若把第一次的 BND 永久当作远端数据来源,又会搞错两个地址角色。BIND 的状态机比 CONNECT 多出“入口已准备、还在等待回连”这一阶段,不能只共用一个成功布尔值。

可达性仍取决于实际网络。代理报告了监听端点,不代表外部防火墙、NAT 或访问控制都允许远端到达它。安全策略也需要检查谁能连接此入口,避免把 BIND 变成无限制接收入站的通用端口开放服务。具体服务器可以不支持 BIND,此时应按命令不支持等适用结果失败,不能从它能做 CONNECT 推断 BIND 也必然可用。

BIND 由此提供了一个典型的两阶段确认案例:先确认资源准备,再确认预期事件。连接数量、消息格式与动作承诺一起决定何时可以交付数据,而不是看见第一个 REP=00 就结束所有控制解析。

6.7 UDP ASSOCIATE:TCP 管关联,UDP 运数据报 ​

UDP ASSOCIATE 的 CMD 为 03。它仍然在已完成方法阶段的 TCP 连接上发送,但请求的目的不是把随后 TCP 字节当成 UDP。它要求服务器建立 UDP 中继关联,返回客户端应该向哪里发送数据报。控制 TCP 的生命周期决定该关联的生命周期。

请求里的 DST.ADDR 和 DST.PORT 表示客户端预期用来发送 UDP 的自身端点信息,服务器可以据此限制关联访问;它们不是某一个最终 UDP 目标。如果客户端当时不知道自己的相应地址与端口,规范要求使用全零。例如用 IPv4 形式发送:

text
05 03 00 01 00 00 00 00 00 00

服务器成功响应可以是:

text
05 00 00 01 c6 33 64 14 c3 50

这十字节表示客户端应把带 SOCKS UDP 头的数据报发到本例中继端点 198.51.100.20:50000,因为 c3 50=50000。这里报告的是中继 UDP 端口,不是控制 TCP 的 1080,也不是最终服务端口。关联在原控制 TCP 连接终止时结束,客户端不能建好关联便随手关闭控制连接。RFC 1928 §6,UDP ASSOCIATE。

每个发往中继的 UDP 数据报都携带以下结构:

字段宽度含义
RSV2 字节00 00
FRAG1 字节独立数据报为 00;其它值涉及本协议分片
ATYP1 字节01、03、04 三类地址
DST.ADDR变长本数据报实际目标
DST.PORT2 字节实际目标端口,大端
DATA数据报剩余内容要交给目标的原始数据报负载

它没有 VER。首个 00 是两字节保留字段的一部分,不能因为它不是 05 就认为这是错误版本。它也没有 DATA 总长前缀,因为外层 UDP 数据报本身提供这条封装消息的结束边界;不能把接收缓冲区过小导致的截断,当成一条更短但正常的业务数据报。

本例客户端要给 203.0.113.30:9000 的教学 UDP 服务发送三字节 abc:

text
00 00 00 01 cb 00 71 1e 23 28 61 62 63

其中 23 28=35×256+40=9000,前十字节为 SOCKS UDP 头,后三字节为内容,总共十三字节。外层 UDP 的目的地址是中继 198.51.100.20:50000;头内目标是 203.0.113.30:9000。这两个“目的”各属于自己的层次。代理去掉 SOCKS 头,再把三字节原始内容送给最终目标,不能把十三字节整个转给它。

目标回复以后,中继也按同样头部布局封装返回数据报,必要时再应用方法要求的封装。客户端去掉代理头才得到业务负载。没有这个头,客户端即使收到一个 UDP 包,也可能无法区分它来自关联中哪个远端;关联不是只能写死一个目标的普通 UDP Socket 抽象。

地址类型SOCKS UDP 头长度,不含任何外层 UDP/IP 头
IPv42+1+1+4+2=10
域名,N 字节2+1+1+1+N+2=N+7
IPv62+1+1+16+2=22

这些数值来自 §7 字段图的宽度计算。RFC 1928 §7 末尾的缓冲区建议对 IPv6 写了 20,与同节字段图逐项相加不一致;本书不把该处数字直接当作头长,而明确采用上面的字段布局计算,也不声称这一差异已经获得某项正式勘误处理。域名的 262 字节最大值则包含最大 255 字节名称。

FRAG 指的是 SOCKS 层自己的分片,与 IP 分片是两套机制。00 为独立数据报;启用分片时,低七位表示片段位置,最高位标记序列结束。接收方需要重组队列与计时器,规范要求重组计时器至少五秒,并规定超时或片段序号回退时放弃原重组状态。分片支持是可选的,不支持的实现必须丢弃 FRAG 非零的数据报。因而客户端不能看见 UDP ASSOCIATE 成功,就假定任意大报文都可被 SOCKS 分片并可靠重组。RFC 1928 §7。

关联还不是一张允许任何来源使用的通行证。RFC 要求中继取得该关联预期客户端的 IP,并丢弃不同源 IP 的数据报;端口等更多限制要结合请求信息与实现策略判断。这项来源约束与密码学认证不是同一性质,NAT 映射、地址变化和控制连接都可能影响实际可用性。

最后,中继通常不会逐报文返回 SOCKS 成功确认。规范允许它静默转发,无法或不愿转发时静默丢弃;UDP 的丢失、重复和乱序也没有因为关联而自动消失。控制连接活着,只表示关联还存在,不能证明最近一条业务数据报已经送达目标。实时应用应按自己的协议解释反馈,而不是把控制 TCP 的可靠性错误地套到 UDP 数据路径上。

6.8 一次无认证 CONNECT:两条字节流与四个控制消息 ​

让客户端通过 SOCKS 服务访问 example.com:80,然后发送第 5 章的最小 HTTP 请求。本例只讨论客户端直接连接该 SOCKS 服务的单级路径,不引入第二种远端代理协议。它按 RFC 构造,所有字节确定;DNS 和目标端点使用文档示例值。

按时间顺序,会话有四条 SOCKS 控制消息:

顺序方向完整字节结果
1客户端→代理05 01 00提供无认证方法
2代理→客户端05 00接受无认证
3客户端→代理05 01 00 03 0b 65 78 61 6d 70 6c 65 2e 63 6f 6d 00 50请求连接域名的 80 端口
4代理→客户端05 00 00 01 c6 33 64 14 a0 28下游连接成功,绑定端点如前例

客户端请求没有用户名,也没有默认 UUID。不能因为没有凭据字段就说这个报文截断:身份子协议由选定方法决定。00 50 为 80,远端代理的监听端口 1080 不出现在目标字段中。

代理在第 3 步完整解析域名后,按本例假设得到 203.0.113.30,再建立到其 80 端口的连接。DNS 未成功或策略不允许,就不能产生本例第 4 步的成功语义。连接建立以后,代理把相应结果发回,客户端才开始目标 HTTP 交换。

两个方向的 TCP 流各有自己的偏移,不能把双向抓包行简单拼成一条流:

方向本方向范围解释
客户端→代理[0,3)方法候选,三字节
客户端→代理[3,21)CONNECT 请求,十八字节
客户端→代理[21,58)完整 HTTP 请求,三十七字节
代理→客户端[0,2)方法选择,两字节
代理→客户端[2,12)CONNECT 成功响应,十字节
代理→客户端[12,52)完整 HTTP 响应,四十字节

这里的请求和响应分别是:

text
客户端目标内容:GET / HTTP/1.1\r\nHost: example.com\r\n\r\n
服务器目标内容:HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK

可见的 \r\n 每处代表两个真实字节,不含展示换行。方法与请求的累计控制数据是客户端二十一字节、服务器十二字节,双向共三十三字节。加上七十七字节目标 HTTP 内容,两条应用层流累计一百一十字节;这个数字不包含 TCP/IP 首部、TCP 建连、DNS 或重传。

代理不会把客户端前二十一字节发给 HTTP 服务。下游目标连接上行从 GET 开始;代理也不会把服务器方向的十二字节 SOCKS 控制响应插进目标 HTTP 响应正文。这是“终止代理协议并转发目标流”的字节级含义。

读写分块可以改变,阶段不能乱。例如客户端收到完整方法选择后发送 CONNECT;CONNECT 响应可能先到五字节,再到五字节。首批五字节只包含固定前缀和绑定地址首字节,客户端须保留状态继续读取,不能把后五字节当 HTTP。随后 HTTP 的四十字节可以一次到齐,也可以逐字节到达,浏览器的 HTTP 解析应恢复同一结果。

正在绘制图表…

查看图表原文
sequenceDiagram
    participant C as SOCKS 客户端
    participant P as SOCKS 代理
    participant D as 解析服务
    participant T as HTTP 目标
    C->>P: 05 01 00
    P-->>C: 05 00
    C->>P: CONNECT example.com:80
    P->>D: 解析目标
    D-->>P: 本例目标地址
    P->>T: 建立 TCP
    T-->>P: 建立成功
    P-->>C: 十字节成功响应
    C->>P: 37 字节 HTTP 请求
    P->>T: 同一目标字节序列
    T-->>P: 40 字节 HTTP 响应
    P-->>C: 同一返回字节序列

若把网站改成 HTTPS,前四个 SOCKS 消息只有目标端口等相应字段改变;成功之后不再立刻出现明文 GET,而是网站 TLS 的握手及记录。SOCKS 仍提供流,不替浏览器验证网站证书,也不把认证网站与认证代理合成一次操作。

这个实例还说明缓存为什么不能随状态重置一起清空。状态从“等成功响应”转到“转发”时,某些目标服务可能已经主动发来欢迎信息,读取缓冲区便可能同时包含成功响应和目标数据。HTTP 的本例没有预先发送欢迎信息,但一个通用 SOCKS 客户端不能把所有目标都假定成 HTTP 的请求响应顺序。状态转移只改变后续字节的解释者,不能消灭已经到达的字节。

6.9 互操作失败:能力名字相同,边界仍可能不同 ​

一个客户端能够成功使用某个 SOCKS5 服务,并不意味着另一种客户端或另一类业务一定可用。相同名称下,失败可能来自不同能力交集;精确的诊断要落到协议阶段,而不是只说“节点不通”。

如果 TCP 连接建立后立刻结束,应先确定双方是否在这一端口使用同一种入口协议。HTTP 代理客户端发送的首字节可能是方法文本,而 SOCKS5 方法列表首字节是 05;没有明确协议探测的入口,不能把一种请求当成另一种去解析。一个端口同时支持多种格式属于实现能力,不由 SOCKS5 规范自动提供。

如果方法选择返回 ff,问题在于方法交集或服务策略;尚未走到目标解析。若用户名密码返回非零,问题处于子协议认证阶段,不能据此判断目标网络不可达。若 CONNECT 返回 07 或 08,服务器报告的是命令或地址表示不支持,不是网站返回了错误页面。把这些结果全部折叠成“连接失败”,会丢失已经明确的故障位置。

IPv4 请求成功而域名请求失败,则至少应区分解析能力、名称策略和编码约定;它不自动证明 TCP 传输坏了。UDP ASSOCIATE 成功而业务无回包,也可能是客户端没有把数据报送到响应给定的中继端点、控制 TCP 已关闭、来源地址约束不满足、FRAG 不受支持,或最终应用没有返回。关联建立与每个数据报交付是不同观察对象。

认证策略还影响入口的使用范围。无认证表示协议不要求客户端提交身份材料,不等于这个服务必须允许所有网络来源和所有目的地。服务可以依监听范围、来源地址或目标规则限制访问;反过来,开启用户名密码也不自动提供服务器身份确认和通道保密。二者是需要组合考虑的权限与通信保护维度。

在代理链里,某个上游服务看到的“客户端”可能是另一台代理,而非最初浏览器。因此方法认证确认的是这一跳请求者提供的资格,不会自动证明整条链上的所有用户。源地址策略也可能看到上一跳出口,而不是用户原始地址。把每一跳的身份和目标分别列出,比假定一个密码贯穿全链更准确。

SOCKS5 的优点是抽象明确:把目标选择、认证方法和流或数据报服务分离,最终应用无需理解代理协议。它的边界也因此明确:不承诺任意 IP 网络语义,不统一规定所有方法的安全性,不代替目标应用的身份验证,也不统一实现所有部署优化。后文研究 VLESS、Trojan 等协议时,将比较它们怎样改变这些职责,而不是仅按名称判断先进程度。

本章已使用的规范范围是 RFC 1928 §3—8 与 RFC 1929 §2—3。字段宽度、状态与义务按这两份规范解释;对未点名软件的可能行为使用条件描述,没有把示例当作实现实测。下一章换用 HTTP 的请求语言表达代理访问,并观察 CONNECT 怎样在成功之后切换解释规则。

从字节与状态机出发,理解每一层的职责。