Socks 协议是一种代理 (Proxy) 协议, 例如我们所熟知的 Shdowsocks 便是 Socks 协议的一个典型应用程序, Socks 协议有多个版本, 目前最新的版本为 5, 其协议标准文档为 RFC 1928, 本文讨论 Socks 5 协议的设计

代理服务器

通常在组织内部会有自己专用的网络, 该网络与公共网络 (如 Internet) 是隔离的, 而代理服务器可以创建一个从内网到外网的通道, 用于组织内的主机与组织外的主机进行通信, 同时组织管理者可以在代理服务器上配置相应的权限管控和监控, 以约束和审查组织内外主机的通信行为, 这里, 代理服务器实际上相当于一个应用层网关, 在 Socks 协议之前没有一套通用的标准来约束组织内主机与代理服务器的交互方式, 这需要工程师自行实现相应的数据转发, 而且对于应用层协议往往是不透明的 (例如可能需要根据实际需求实现多种应用层协议的网关, 如 HTTP 代理, FTP 代理, SSH 代理等等), Socks 5 协议工作在传输层 (Transport Layer) 与应用层 (Application Layer) 的中间, 提供了一种对应用层协议透明的代理服务, 当组织内主机与代理服务器完成 Socks 握手之后, 应用层对代理服务器是无感知的, 例如地址分别为 A, B 的分布在组织内外的两个进程进行 HTTP 通信, 其中 C 为代理服务器, 实际的数据链路为 A → C → B, 但在引入 Socks 协议之后, 从应用层的视角来看, 整个通信过程仍然是 A → B 的模式

基于 TCP 的 Socks 5

Socks 5 协议是对 Socks 4 的改进, 在 Socks 4 协议中, 对于传输层协议仅支持 TCP 协议, 并且 Socks 4 协议没有安全性相关的设计, Socks 5 协议增加了对 UDP 的支持, 同时提供了安全加密认证机制, Socks 5 协议的第一步是与代理服务器握手, 首先客户端向代理服务器发起握手请求, 其数据包格式如下所示:

  • VER 字段表征 Socks 协议版本, 占 1 字节, 对于 Socks 5 其值固定为 0x05
  • NMETHODS 字段指示其后的 METHOD 字段所占的字节数, 其本身占 1 字节
  • METHODS 字段为可变长字段, 用来指示客户端和代理服务器之间的认证方法, 其长度区间为 [1, 255] 个字节, 即客户端在向代理服务器发起握手时同时声明其所支持的认证方法的列表, 代理服务器会从中选择一个方法作为接下来与客户端进行认证的方法, 所以对于 Socks 5 协议来说, 客户端发起的握手实际上本身也是启动了一个协商过程

代理服务器在收到客户端发起的请求之后, 向客户端发回握手响应, 其数据包格式如下:

  • 其中 VER 字段与客户端请求数据包的 VER 字段含义相同, 表征协议版本, 固定为 0x05
  • METHOD 字段表征代理服务器所选择的协议版本, 长度为 1 字节, 当然代理服务器可能对于客户端所声明的所有认证方法都不支持, 此时代理服务器将 METHOD 字段值为 0xFF, 客户端收到该数据包时便知晓代理服务器不支持自己所声明的认证方法, 即协商失败, 客户端应主动关闭 TCP 连接

Socks 协议支持多种认证方法, 每一种认证方法都有一个对应的编号, Socks 握手环节的 METHOD 字段便存放了认证方法的编号, 如 0x00 代表不需要认证, 0x02 代表用户名/密码方式的认证等, 不同的认证方法将导致后续不同的认证流程, 本文不一一展开叙述相关的认证流程

当认证过程通过后, Socks 握手正式完成, 此时客户端向代理服务器发起正式请求以指示所要访问的目标进程的地址, 端口等信息, 其数据包格式如下所示:

  • VER 字段表征 Socks 版本, 固定为 0x05
  • CMD 字段指示连接的类型, 占 1 个字节, 共有 3 个取值, 分别为 0x01 (CONNECT), 0x02 (BIND), 0x03 (UDP ASSOCIATE), 关于每个值的含义将在下面讨论
  • RSV 字段为保留字段, 占 1 个字节, 固定为 0x00
  • ATYP 字段指示地址类型 (DST.ADDR 字段的类型), 0x01 为 IPv4 地址, 0x03 为域名, 0x04 为 IPv6 地址
  • DST.ADDR 字段指示客户端所要访问的目的地址, 这是一个变长字段, 其长度由前一个字段的值来决定, 当 ATYP 字段的值为 0x01 时, 表示该字段为 IPv4 地址, 从而长度为 4 个字节, 当 ATYP 字段的值为 0x03 时代表地址类型为域名, 此时 DST.ADDR 字段的第一个字节的值指示其后所跟随的域名所占的字节数, 当 ATYP 字段的值为 0x04 时, 表示该字段为 IPv6 地址, 其长度为 16 个字节

代理服务器在收到以上请求后, 其返回的数据包格式如下:

  • VER 字段占 1 字节, 表征协议版本, 固定为 0x05
  • REP 字段占 1 字节, 可以理解为状态码, 它的值表征了此次连接的状态:
    • 0x00 连接成功
    • 0x01 代理服务器出错
    • 0x02 连接不允许
    • 0x03 网络不可达
    • 0x04 主机不可达
    • 0x05 连接被拒绝
    • 0x06 TTL 到期
    • 0x07 命令 (CMD) 不支持
    • 0x08 地址类型不支持
    • 0x09 ~ 0xFF 目前没有分配
  • RSV 字段占 1 字节, 为保留字段, 固定为 0x00
  • ATYP 字段与请求的 ATYP 字段含义相同
  • BND.ADDR 与 BND.PORT 的含义随请求中的 CMD 的不同而不同, 下面我们依次展开讨论 3 种 CMD: CONNECT, BIND 以及 UDP ASSOCIATE

当客户端发往代理服务器的数据包的 CMD 字段的值为 0x01 时, 代表 CONNECT, 此时 DST.ADDR 和 DST.PORT 指示客户端所想要访问的目标主机的地址和端口, 代理服务器在收到该请求后建立 "代理服务器到目标主机" 的 TCP 连接, 并将代理服务器分配的 IP 地址和端口在返回的数据包中的 BIND.ADDR 和 BIND.PORT 字段中告诉客户端

当客户端发往代理服务器的数据包的 CMD 字段的值为 0x02 时, 代表 BIND, BIND 主要用在双向连接场景中, 最典型的例子为 FTP, FTP 会建立两个连接, 第一个连接为 客户端→服务器, 用于发送指令及状态信息, 第二个连接为 服务器→客户端, 用于传输数据, 在 Socks 5 协议中, 客户端只有首先发送 CONNECT 连接之后, 才允许发送 BIND 连接, 客户端在向代理服务器发送 BIND 请求之后, 代理服务器将会向客户端发起两次回复, 其中第一次回复发生在代理服务器建立并绑定用于接收 目标主机→代理服务器 连接的套接字时后 (此时只是代理服务器自己创建套接字, 目标主机到代理服务器的连接还没有建立), 代理服务器在 BIND.ADDR 和 BIND.PORT 字段指示其用于监听目标主机连接的地址和端口, 当目标主机→代理服务器连接建立完成后, 代理服务器会向客户端发送第二次回复, 其中 BIND.ADDR 和 BIND.PORT 指示目标主机的地址和端口

当客户端发往代理服务器的数据包的 CMD 字段的值为 0x03 时, 代表 UDP ASSOCIATE, 这里的细节在下一节讨论

Socks 5 UDP 穿透

当 CMD 字段的值为 0x03 时, 此时客户端发起了 UDP 穿透请求, 此时 DST.ADDR 和 DST.PORT 为客户端所要发送 UDP 数据包所使用的地址和端口, 代理服务器返回的数据包的 BIND.ADDR 和 BIND.PORT 为客户端要发送 UDP 包所使用的目标地址和端口 (代理服务器的 UDP 中继进程的地址和端口), 此时 UDP 穿透通道建立完成, 但此时不能向常规发送 UDP 包那样直接发送, 因为截止目前还没有将目标主机的地址和端口告诉 UDP 中继, 在 Socks 5 协议中, 客户端发往 UDP 中继的数据包格式实际如下所示:

  • RSV 字段为保留字段, 占两个字节, 其值固定为 0x0000
  • FRAG 字段为占 1 个字节, 指示当前 UDP 分片的编号
  • ATYP 指示地址类型
  • DST.ADDR 和 DST.PORT 为所要发往的目标主机的地址和端口
  • DATA 字段为原始的 UDP 报文 (的数据部分?)

代理服务器收到该数据包后, 提取 DATA 字段即为客户端想要向目标主机发送的原始的 UDP 报文(的数据部分?), 此时根据 DST.ADDR 和 DST.PORT 字段将 UDP 报文投递给目标主机, 所以从目标主机来看这就是一次朴素的 UDP 通信, 代理服务器收到目标主机响应的数据包之后也会将 UDP 报文按上图所示的格式组装后发送给客户端, 前面提到, Socks 5 协议的 UDP 穿透需要首先由客户端发起 CMD 为 UDP ASSOCIATE 的 TCP 请求, 整个 UDP 穿透与该 TCP 连接的生命周期相同, 当该连接中止时, UDP 穿透也随之中止

可以阅读 Chromium V8 引擎实现的 Socks 5: 链接