`); }

使用WebSocket进行网络穿透

创建于 访问量:

网络穿透的本质就是代理,而想要稳定的翻墙,必须要把代理伪装成一个正常的网络请求,而这一点上,在拜读了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连接建立起来之后,我们需要自定义协议,来让服务端知道需要干什么。

握手

握手阶段使用标准的JSON格式能让我们减少很多工作。此时我们使用的应当是Websocket标准中规定的文本帧。

客户端请求

为了验证身份,我们首先需要一个握手包来让服务端知道,这个连接来自可靠的客户端。

{
    "VERSION": 1,
    "USERNAME": "用户名",
    "PASSWORD": "密码"
}

服务端响应

当服务端验证账户密码正确时,需要返回本机支持的一些功能,这能让客户端避免一些无效的操作。

{
    "VERSION": 1,
    "IPV4": true,
    "IPV6": true,
    "TCP": true,
    "UDP": true
}

如果验证失败,我们就假定这是一个恶意攻击者,回复

{
    "VERSION": 1,
    "MESSAGE": "用户名或密码错误",
    "STATUS": "ERROR"
}

请求代理

当握手完成后,客户端根据服务端返回的配置,进行本地的配置(例如本地Socks5代理服务器支持的方法等)。当有数据流需要走代理时,应做如下的请求。

客户端请求

客户端发送一个如下的数据包给服务端,用以约定此连接是代理TCP还是UDP。当约定为UDP代理时,DST.ADDR与DST.PORT为任意值均可,服务端无需解析此部分。

{
    "VERSION": 1,
    "CMD": "TCP" | "UDP",
    "ADDR": "目标的地址",
    "PORT": "目标的端口"
}

服务端响应

服务端根据客户端请求进行响应——如果是TCP方法,则尝试连接客户端指定的地址和端口;如果是UDP方法,则视自身情况而定是否同意UDP转发。
当服务端对此次请求处理完毕后,视情况而定回复一个数据包。

{
    "VERSION": 1,
    "STATUS": "SUCCESS"
}

数据传输

当进行完以上两步后,可进行正式的数据传输。数据传输阶段使用二进制帧。

TCP

对于TCP方法,客户端直接将数据发送到此WebSocket连接即可,而服务端也只需要单纯的转发。

UDP

对于UDP方法,客户端需要将UDP数据进行封装

+------+----------+----------+----------+
| ATYP | DST.ADDR | DST.PORT |   DATA   |
+------+----------+----------+----------+
|  1   | Variable |    2     | Variable |
+------+----------+----------+----------+

而服务端进行解包转发后,所接收到的UDP包也解析为同样格式,再发给客户端。

题外话

本协议实现的开始,只是我想突破学校的网络封锁。后来想着,用了别人的软件那么久,也该自己写一个,以防不测了。于是就有了此文。

本文只涉及协议的设计,而没有任何的代码实现。