网络穿透的本质就是代理,而想要稳定的翻墙,必须要把代理伪装成一个正常的网络请求,而这一点上,在拜读了Shadowsocks的源码之后,我觉得它还不够,因为Shadowsocks的连接只能让GFW知道这个是个未知的协议。虽然SSR的混淆做的比较好,然而Breakwa11都删库了,他那个神仙代码我实在是没法维护,还是自己写吧。
在之前的一篇绕开校园网计费里,我写了一个Sock5代理,但单纯的Socks5代理是无法翻墙的,因为GFW能轻易的分析出你是一个代理,从而封掉你的海外IP。
在研究完了Shadowsocks的混淆代码之后,我把目光盯上了WebSocket。
WebSocket
WebSocket是一种基于TCP的长连接,它会使用一次HTTP的报文握手,在那之后便是WebSocket自身规定的方式进行通讯了。它是个在各种网站都可能被用到的协议。既然它常见,那么就安全。
为什么要使用WebSocket
使用WebSocket的原因除了它常见以外,还有就是可以利用各种CDN服务,将你的WebSocket进行代理,而让真实的服务器IP隐藏在重重CDN之后。哪怕真实的服务器IP被封锁了,你的代理也不会失效。
并且标准Websocket的协议设计,使得客户端发送到服务端的信息都有一次性的掩码进行混淆,使得我们不需要做二次处理,只需要稍微处理一下服务器发送到客户端的数据即可。
WebSocket 浅析
在使用 WebSocket 之前,先了解一下 WebSocket。
HTTP 握手
客户端发送到服务端的 HTTP 请求中,请求头必须包含如下四个字段。Connection: Upgrade
指定此次请求需要从 HTTP 协议升级,Upgrade
指定此次请求升级到 websocket
, Sec-WebSocket-Version
指定 WebSocket 的版本(一般都是 13),Sec-WebSocket-Key
是一个 16 byte 长度的随机字符串经过 base64 处理后的结果。
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
服务端视情况决定是否允许升级到 websocket,如果不同意,则返回一个常规 HTTP 响应即可。如果同意,则返回状态码为 101 的 HTTP 响应。
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Accept
的值为客户端发送的 Sec-WebSocket-Key
拼接 "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
的字符串的 sha-1 值的 base64 编码结果——用伪代码编写应该是 base64encode(sha1(SecWebSocketKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))
。
握手阶段完成后,后续所有的数据传输都与 HTTP 协议无关了,使用纯粹的 WebSocket 报文进行交互。
WebSocket 报文结构
- Fin:1bit,用于标记当前数据帧是不是最后一个数据帧,单条消息可能会分成多个数据帧来传递。(如果只需要一个数据帧,第一个数据帧也就是最后一个)
- RSV1,RSV2,RSV3:各自1bit,用于扩展用途,一般全部置0即可。
- Opcode:4bit,操作码,用于描述此数据帧的类型:
- 0x0:标示当前数据帧为分片的数据帧,也就是当一个消息需要分成多个数据帧来传送的时候,需要将opcode设置位0x0。
- 0x1:标示当前数据帧传递的内容是文本,编码为UTF-8。
- 0x2:标示当前数据帧传递的内容是二进制数据。
- 0x8:标示请求关闭连接。
- 0x9:标示此数据帧为ping帧。
- 0xA:标示此数据帧为pong帧,仅在接收ping帧后发送pong。
- 0x3~0x7以及0xB~0xF:留作其他用途。
- MASK:1bit,标示数据有没有使用掩码。在RFC中规定,服务端发送给客户端的数据帧不能使用掩码,客户端发送给服务端的数据帧必须使用掩码。
- Payload length:7bit的无符号整数,用于标识Payload data的长度。
值在0-125之间时,这就是Payload data的长度。
值为126时,使用后续2个bytes也就是16 bit的无符号整数来标识Payload data的长度。
值为127时,使用后续8个bytes也就是64 bit的无符号整数用于标识Payload data的长度。 - Masking-key:数据掩码。如果Mask位为0,则该部分可以省略;如果Mask位为1,则为32 bit (4 byte)的掩码。
自定义协议
当一个WebSocket连接建立起来之后,我们需要自定义协议,来让服务端知道需要干什么。
身份验证
当客户端与服务端建立连接时, 需将 Basic 格式的身份认证信息通过 Authorization
头发送到服务器。
服务端响应应按照HTTP标准,如身份验证失败则返回401,身份验证成功但被限制访问则返回403,成功则返回101。
请求代理
当有数据流需要走代理时,应做如下的请求。
客户端请求
客户端发送一个如下的数据包给服务端,用以约定此连接是代理TCP还是UDP。当约定为UDP代理时,DST.ADDR与DST.PORT为任意值均可,服务端无需解析此部分。
{
"VERSION": 1,
"CMD": "TCP" | "UDP",
"ADDR": "目标的地址",
"PORT": "目标的端口"
}
- VERSION: 协议版本号,此处为'01'
- CMD: 指定代理方式
- "TCP"
- "UDP"
- ADDR: 目标的地址
- PORT: 目标的端口(number类型)
服务端响应
服务端根据客户端请求进行响应——如果是TCP方法,则尝试连接客户端指定的地址和端口;如果是UDP方法,则视自身情况而定是否同意UDP转发。
当服务端对此次请求处理完毕后,视情况而定回复一个数据包。
{
"VERSION": 1,
"STATUS": "SUCCESS"
}
- STATUS: 为
SUCCESS
时,客户端可进行下一步。否则,客户端认为服务器无法做到此次请求的要求,立刻关闭连接。
数据传输
当进行完以上两步后,可进行正式的数据传输。数据传输阶段使用二进制帧。
TCP
对于TCP方法,客户端直接将数据发送到此WebSocket连接即可,而服务端也只需要单纯的转发。
UDP
对于UDP方法,客户端需要将UDP数据进行封装
+------+----------+----------+----------+ | ATYP | DST.ADDR | DST.PORT | DATA | +------+----------+----------+----------+ | 1 | Variable | 2 | Variable | +------+----------+----------+----------+
-
ATYP: 指定DST.ADDR的类型
- IPV4: X'01'
- 域名: X'03'
- IPV6: X'04'
-
DST.ADDR: 该数据包渴望到达的目标地址
- DST.PORT: 该数据包渴望到达的目标端口
- DATA: 实际要传输的数据
而服务端进行解包转发后,所接收到的UDP包也解析为同样格式,再发给客户端。
题外话
本协议实现的开始,只是我想突破学校的网络封锁。后来想着,用了别人的软件那么久,也该自己写一个,以防不测了。于是就有了此文。
代码实现在websocks。
但现在已经不住学校寝室了,所以网络环境不再受限,且 UDP 跑在 WebSocket 上的确延迟很高,于是我又编写了使用WebSocket进行网络穿透(续)仅实现 TCP 的转发,至于 UDP 协议,正在使用 UDP 协议重构中。