摘要

  • SOCKS5 通过一段完成方法协商的 TCP 会话创建 UDP 中继关联,并规定 TCP 连接一旦结束,关联也随之终止;TCP 提供的是状态寿命,不承载 UDP 业务数据。
  • 服务端返回的中继地址、客户端来源 IP、每个报文声明的远端目的地、认证方法与分片队列各有边界;任何一项都不能单独证明用户身份、最终收件方或实际交付。

活跃报文没有续期权

设想一个客户端正通过 SOCKS 中继发送查询或实时媒体。UDP 报文仍然连续抵达,另一条 TCP socket 却因进程退出、NAT 状态丢失、移动网络切换或代理重启而关闭。几毫秒后又有一个语法正确的 UDP 包打到原端口,中继也仍知道该往哪里送。它该不该继续?

1996 年 3 月发布的 RFC 1928 给出了明确答案:承载 UDP ASSOCIATE 请求的 TCP 连接终止时,这项 UDP association 同时终止。中继此刻也许仍有内存、路由和新鲜流量,却已经失去把这些报文纳入既有策略的有效上下文。

这条规则没有把 UDP 伪装成 TCP。业务数据仍按独立报文传输,仍可能丢失、重复或乱序。TCP 承载认证方法协商和创建关联的控制请求,其关闭则提供 UDP 本身无法表达的清理证据。

协议由此避免了两个极端:既没有为 UDP 另造一套关闭握手,也没有把静默超时当作唯一现实。无连接数据借到一个可观察的始点和终点,但没有借到流式传输的可靠性承诺。

第一个 UDP 包之前,权限已经分层

SOCKS5 客户端先在 TCP 上提交可用认证方法,服务端选择其一,双方完成该方法规定的子协商。之后客户端才能发送 CONNECT、BIND 或 UDP ASSOCIATE。

UDP ASSOCIATE 请求可声明客户端预计用来发送 UDP 的地址与端口,服务端可以据此限制访问。如果客户端当时还不知道端点,就必须填写全零地址和端口。这里的零不是“任何来源都可信”,只是“请求没有给出具体值”。真正接受什么来源,仍由服务端结合当前 TCP 会话、观察结果与本地政策决定。

成功响应里的 BND.ADDR 与 BND.PORT 告诉客户端:把待中继 UDP 报文发到这里。多宿主代理返回的地址可能与 TCP 控制连接所用地址不同。因此它是一条中继入口指令,不是最终目的地主机的身份。

同一时刻至少存在三组不能混写的坐标:客户端连接 SOCKS 服务的 TCP 地址、服务端返回的 UDP relay endpoint,以及每个 UDP 封装内部声明的远端地址和端口。只写一个“代理 IP”,会把规范有意暴露的控制面压扁。

每个报文自行陈述目的地

关联建立后,客户端并没有被锁定在一个永久 UDP 目的地。发往中继的每个报文都带一段 SOCKS UDP header:两个保留字节、FRAG、地址类型、目的地址、目的端口和数据。

这个小信封允许同一 association 面向多个远端目标。中继因此不是透明导线,而是逐报文解释请求的执行者:去掉 SOCKS 信封,按目的字段发送负载;远端回复回来后,再以同一语法封装来源信息交给客户端。

这份语法扩大了可核查性,却没有创造终局证明。中继能够记录它收到什么、允许什么、向哪里发出以及从哪里收到回复。它不能仅凭本地 UDP 端口接收成功,就断言远端应用收到数据、执行动作,或回复属于某个自然人。

旧的 BND.ADDR 也不是可长期复用的能力票据。控制 TCP 已结束时,把完全合法的 SOCKS UDP 报文继续投往那个地址,并不会重新生成已失效关联。记得地址不等于仍有权限。

来源约束与用户认证不是同一件事

RFC 1928 要求 UDP relay 从 SOCKS server 获取这项关联预期的客户端 IP,并静默丢弃来自其他源 IP 的报文。这能阻止另一台主机轻易借用已存在的 relay context 注入流量。

但 IP 检查只能回答有限问题。一个公网地址后面可能有多个用户、进程和设备;移动后它也可能改变。它说明报文在中继边缘从哪里出现,不证明是谁打开控制连接、哪个进程生成数据,也不证明远端允许该动作。

真正的认证属于此前的方法协商,而且“用了 SOCKS5”并不自动等于一种统一安全属性。RFC 1929 定义 username/password 子协商,凭据在其中以明文传送,文档明确警告不宜用于可被嗅探的环境。RFC 1961 则定义 GSS-API 方法,可以协商认证、消息完整性与可选保密性。

基础协议提供了容纳不同保护方法的框架,却不替任何部署补造保护。审计至少要保留选中了哪种方法、该方法实际启用了什么保护,以及保护是否延伸到后续中继报文。版本号和功能开关都不是收据。

FRAG 保存的是有限重组状态

FRAG 很容易被误解成 IP Fragment 字段,其实它属于 SOCKS UDP 信封。零表示该报文在这套语法中独立完整;1 到 127 表示分片顺序,高位标出序列末端。

实现可以完全不支持这种分片,但此时必须丢弃所有 FRAG 非零的报文。支持者为序列维护重组队列和计时器:时间一到,所有未完成片段被放弃;新到片段的编号若低于已经处理的最高值,队列也要重置。计时器不得短于五秒,规范同时建议应用尽量避免使用分片。

这不是一套缩小版可靠传输。没有 ACK 语法,没有自动重传保证,也没有权利无限期保存残缺数据。队列只服务于本地重组,能被明确丢弃;它不能证明消息最终交付。

所以,同样表现为“应用没收到”,原因可能完全不同:包未到中继、来源不符、实现不支持 FRAG、重组过期、发往远端后在路上丢失,或者远端已收到却没有响应。若不定位决策点,“代理丢了 UDP”只是一个无法复核的标签。

TCP 关闭阻止孤儿权限

为什么不用 idle timeout 让中继多活一阵?超时擅长回收资源,却不擅长解释沉默。安静的应用、断掉的回程和已经死亡的客户端,在中继眼里都可能没有流量。反过来,仍有 UDP 流量只能证明有人能向端口发包,不能证明创建 association 的主体仍控制它。

TCP 控制连接提供了更强但仍有限的生命周期事实。它存在时,SOCKS server 能把方法状态、请求政策与 UDP relay entry 绑定到同一段活会话;它结束时,协议选择撤销而不是猜测。若客户端还需要服务,就创建新上下文。

这会付出代价:短暂 TCP 故障可能中断健康 UDP;移动换址会让控制连接与来源绑定同时失效;长期开着一条流也占资源。然而这些成本可见、可测试、可恢复。若只因报文仍在抵达就保留孤儿中继,权限反而交给了最难归因的信号。

跨地址族网关没有抹平层次

2001 年的 Informational 文档 RFC 3089 描述了一种基于 SOCKS 的 IPv6/IPv4 gateway。它在应用层终止两侧连接并转发数据,继承 CONNECT、BIND 与 UDP ASSOCIATE,还可把域名解析交给双栈网关,帮助只能容纳 IPv4 地址的旧应用访问 IPv6 目标。

这说明 SOCKS 的共同语法能够跨越地址族差异,也说明它的边界。网关不是 IP 层路由器;域名、解析结果、控制 association 与转发数据仍是不同对象。地址转换能力没有替网关创造关于身份、授权或交付的额外知识。

SOCKS5 的历史贡献不是一条万能隧道,而是一份小型协作契约:端点可以要求中继做有限工作,中继可以逐项执行和拒绝,本地政策继续归本地,任何单一字段都不得接管整段现实。

来源