SOCKS是一种网络传输协议,主要用于客户端与外网服务器之间通讯的中间传递。SOCKS是"SOCKetS"的缩写。
当防火墙后的客户端要访问外部的服务器时,就跟SOCKS代理服务器连接。这个代理服务器控制客户端访问外网的资格,允许的话,就将客户端的请求发往外部的服务器。
这个协议最初由David Koblas开发,而后由NEC的Ying-Da Lee将其扩展到版本4。最新协议是版本5,与前一版本相比,增加支持UDP、验证,以及IPv6。
根据OSI模型,SOCKS是会话层的协议,位于表示层与传输层之间。
协商认证
客户端使用TCP协议连接上Socks5服务器之后,发送一个数据包:
+-----+----------+----------+ | VER | NMETHODS | METHODS | +-----+----------+----------+ | 1 | 1 | 1 to 255 | +-----+----------+----------+
- VER: 指定协议版本号,此处为X'05'
- NMETHODS: 指定客户端支持的认证方法数量
-
METHODS: 每个byte对应一个认证方法
- X'00' 不需要身份验证(NO AUTHENTICATION REQUIRED)
- X'01' GSSAPI
- X'02' 用户密码认证(USERNAME/PASSWORD)
- X'03' to X'7F' IANA ASSIGNED
- X'80' to X'FE' RESERVED FOR PRIVATE METHODS
服务器需要回应一个数据包
+-----+--------+ | VER | METHOD | +-----+--------+ | 1 | 1 | +-----+--------+
- VER: 指定协议版本号,此处为X'05'
- METHOD: 指定认证方法。该方法应从客户端提供的认证方法中挑选一个,或者是X'FF'用以拒绝认证。
密码认证
若服务器选择密码认证,则客户端接下来需要发送如下数据包
+-----+------+----------+------+----------+ | VER | ULEN | UNAME | PLEN | PASSWD | +-----+------+----------+------+----------+ | 1 | 1 | 1 to 255 | 1 | 1 to 255 | +-----+------+----------+------+----------+
- VER: 指定协议版本号,此处为X'01'
- ULEN: 指定后续UNAME的长度
- UNAME: 用户名
- PLEN: 指定后续PASSWD的长度
- PASSWD: 密码
服务器将解析该数据包,并使用其用户名与密码进行验证,然后回复一个数据包
+-----+--------+ | VER | STATUS | +-----+--------+ | 1 | 1 | +-----+--------+
- VER: 指定协议版本号,此处为X'01'
- STATUS: X'00'代表验证成功,可以进行下面的流程。如果该值不是X'00'则代表验证失败,服务器需要关闭此连接。
请求代理
客户端在认证成功之后,需要发送一个数据包来请求服务端:
+-----+-----+-------+------+----------+----------+ | VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT | +-----+-----+-------+------+----------+----------+ | 1 | 1 | X'00' | 1 | Variable | 2 | +-----+-----+-------+------+----------+----------+
- VER: 协议版本号,此处为X'05'
-
CMD: 指定代理方式
- CONNECT X'01'
- BIND X'02'
- UDP ASSOCIATE X'03'
-
RSV: 预留位置,标准Socks5应为X'00'
-
ATYP: 指定DST.ADDR的类型
- IPV4地址: X'01'
- 域名: X'03'
- IPV6地址: X'04'
-
DST.ADDR: 目标的地址,长度由ATYP指定
- IPV4: 4个byte
- 域名: 第一个byte指定长度n,其后跟着n个byte标识域名
- IPV6: 16个byte
-
DST.PORT: 目标的端口
服务端在对请求进行评估判断之后,回应数据包:
+-----+-----+-------+------+----------+----------+ | VER | REP | RSV | ATYP | BND.ADDR | BND.PORT | +-----+-----+-------+------+----------+----------+ | 1 | 1 | X'00' | 1 | Variable | 2 | +-----+-----+-------+------+----------+----------+
- VER: 协议版本号 X'05'
-
REP: 回复请求的状态
- X'00' 成功代理
- X'01' SOCKS服务器出现了错误
- X'02' 不允许的连接
- X'03' 找不到网络
- X'04' 找不到主机
- X'05' 连接被拒
- X'06' TTL超时
- X'07' 不支持的CMD
- X'08' 不支持的ATYP
- X'09' to X'FF' Socks5标准中没有分配对应的状态
-
RSV: 预留位
-
ATYP: BND.ADDR的类型
- IPV4: X'01'
- 域名: X'03'
- IPV6: X'04'
-
BND.ADDR: 服务端指定的地址,长度由ATYP指定
- IPV4: 4个byte
- 域名: 第一个byte指定长度n,其后跟着n个byte标识域名
- IPV6: 16个byte
-
BND.PORT: 服务端指定的端口
Connect方法
当服务端接收到的数据包中CMD为X'01'时,服务器使用Connect方法进行代理。
此时,服务端尝试使用TCP协议连接对应的(DST.ADDR, DST.PORT),根据连接的情况,决定REP的值。
如果连接成功,回复的数据包中的BND.ADDR,BND.PORT没有太大的意义,象征性的填写Socks服务端在此次连接中使用的ADDR和PORT即可。
Bind方法
Bind方法使用于目标主机需要主动连接客户机的情况
当服务端接收到的数据包中CMD为X'02'时,服务器使用Bind方法进行代理。使用Bind方法代理时服务端需要回复客户端至多两次数据包。
服务端使用TCP协议连接对应的(DST.ADDR, DST.PORT),如果失败则返回失败状态的数据包并且关闭此次会话。如果成功,则监听(BND.ADDR, BND.PORT)来接受请求的主机的请求,然后返回第一次数据包,该数据包用以让客户机发送指定目标主机连接客户机地址和端口的数据包。
在目标主机连接服务端指定的地址和端口成功或失败之后,回复第二次数据包。此时的(BND.ADDR, BND.PORT)应该为目标主机与服务端建立的连接的地址和端口。
UDP转发
使用UDP ASSOCIATE时,客户端的请求包中(DST.ADDR, DST.PORT)不再是目标的地址,而是客户端指定本身用于发送UDP数据包的地址和端口。
如果客户端在发送UDP包之前并不能确定自身的将使用的IP和端口(像个人用户都是NAT,是没办法在发送前确定自己将使用的公网IP以及端口的),那么此时,(DST.ADDR, DST.PORT)必须全部设置为0。
为了支持UDP转发,服务端应该建立一个UDP服务器用以接收客户端发送过来的UDP包。然后在TCP连接中回复一个数据包,(BND.ADDR, BND.PORT)应该是UDP服务器绑定的地址与端口。需要注意,在这个会话没有结束之前,TCP连接不能中断,否则客户端会认为服务端挂了。
UDP的数据处理
相比于TCP单纯的数据转发,UDP的数据处理稍显复杂。
UDP服务器接收到信息之后,对来源进行筛选,如果收到的UDP包来自第一次收到的请求包中的(DST.ADDR, DST.PORT),那么,就认为是来自客户端,需要对数据包的头部进行解析
+-----+------+------+----------+----------+----------+ | RSV | FRAG | ATYP | DST.ADDR | DST.PORT | DATA | +-----+------+------+----------+----------+----------+ | 2 | 1 | 1 | Variable | 2 | Variable | +-----+------+------+----------+----------+----------+
- RSV: 保留字段,默认X'0000'。
-
FRAG: 该数据包的片段序号,如果值为X'00'则说明该数据包为独立数据包,如果为1~127的某个值,则说明为整个数据包的一个片段。
当收到数据包的片段之后,如果构建的Socks5代理服务器支持UDP分包代理,那么需要接收包之后,进行一个缓存拼接,去掉头部信息之后根据序号从小到大拼接。
但这种方法较为复杂,服务器可以不接受此种方式。即,丢弃所有FRAG≠X'00'
的数据包。 -
ATYP: 指定DST.ADDR的类型
- IPV4: X'01'
- 域名: X'03'
- IPV6: X'04'
-
DST.ADDR: 该数据包渴望到达的目标地址
- DST.PORT: 该数据包渴望到达的目标端口
- DATA: 实际要传输的数据
当接收到数据包后,将DATA转发至此次数据包头部指定的(DST.ADDR, DST.PORT)。如果UDP服务器接收到来自对应的(DST.ADDR, DST.PORT)的UDP包,则需要将UDP包加上同样的头部转发至客户端。
RFC1928与RFC1929,原文档言简意赅,令人难懂。本文是我结合实际使用翻译的RFC文档。对段落的顺序做了一些调整,并将RFC1929揉合进了RFC1928中。