关于 SOCKS5 的协议细节在 RFC 1928 中有详细的记录,本文旨在记录一些值得注意的细节。
握手方法
RFC 中要求合法的 SOCKS5 实现必须实现 GSSAPI。 这是很好的,因为可以通过 SSH 等方式来加密我们的连接。然而,常见客户端(curl, browser)都不支持啊! 所以现有实现中实现了 GSSAPI 的很少。
CONNECT 请求
CONNECT 请求就是在 CONNECT 成功后直接开始透传,即此时这个连接好似就是客户端 直连到远程服务器上一样。
注意 BND.ADDR
和 BND.PORT
。rfc 里是这么说的:
BND.PORT contains the port number that the server assigned to connect to the target host, while BND.ADDR contains the associated IP address. The supplied BND.ADDR is often different from the IP address that the client uses to reach the SOCKS server, since such servers are often multi-homed.
即实现应当把出连接的 port, address 放到 BND
区返回给客户端。例如
UV Example。
BIND 请求
与一般的 CONNECT 连接级别透传不同,BIND 请求实现了端口级别的透传。这个请求
直接占用了 SOCKS5 Server 的一个外部地址上的端口,通过第一个响应包的
BND.ADDR
与 BND.PORT
返回给客户端,客户端再公开给外部,使得外部的主机
能够通过这个地址来访问;外部主机连接时会发送第二个响应包,将外部主机的
地址信息通过 BND.ADDR
与 BND.PORT
返回给客户端。
但是与内网穿透不同的是,一个 BIND 请求只能 accept 到一个连接,随后就停止 listen 了;而且连接来源必须与 BIND 请求中的 dest 相同。这能够最大程度保证 客户端的安全和 SOCKS5 服务器本身的安全。
因此,BIND 请求仅适用于单次反向连接的场景,比如 FTP、SSH 等等。
UDP ASSOCIATE 请求
UDP 与 TCP 最大的不同在于它没有连接的概念,因此只要 bind 一个 udp 端口, 之后就可以用这个端口向任意 destination 发包,也可以接收来自任何 source 的包。
因此 UDP ASSOCIATE 请求会占用一个 UDP 端口(但是不告诉客户端?),用它来收发包, 完全传递给客户端的端口。当然由于包会来自任何地址,这个地址是有用但不会包含在 UDP 包中的,因此 UDP 包会用 RFC1928 #7 定义的格式附加上源地址。
这样,客户端发送格式化的包,然后 SOCKS5 服务器解析后发给目标地址;SOCKS5 服务器收到 UDP 包,然后它加上 SOCKS5 UDP 头后发送给客户端。这样双向传输就实现了。这是一种在 Layer 4 实现的 Full-Cone NAT。
值得注意的是 UDP ASSOCIATE 请求中的 DST 决定了客户端自己将要使用的 UDP 地址端口, 供服务器筛选包。如果不限制应当设为全 0。
然而在实践中,由于 NAT 的存在,这个端口可能并不开放,因此个人认为,在 SOCKS5 服务器实现中, 应当不管 DST,直接允许第一个来源包的地址。