AberSheeran
Aber Sheeran

使用WebSocket进行网络穿透

起笔自
所属文集: Hack
共计 3604 个字符
落笔于

网络穿透的本质就是代理,而想要稳定的翻墙,必须要把代理伪装成一个正常的网络请求,而这一点上,在拜读了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 指定此次请求升级到 websocketSec-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: 指定代理方式
    1. "TCP"
    2. "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 协议重构中。

如果你觉得本文值得,不妨赏杯茶
没有上一篇
使用WebSocket进行网络穿透(续)