This article also has an English version.
这篇文章将简要总结一下目前科学上网的加速方式,并详细介绍我基于 Shadowsocks 做的一个加速工作。
UPDATE 2019-10: 基于这个项目的 idea,我做了一点延伸,将多连接加速独立出来以支持任意 TCP 连接: Rabbit TCP,在稳定性与延迟上有一定提升。
科普(Introduction)
梯子的工作方式大多是类似的。
一般流量需要经过一台境外机器做中转,再由该境外机器访问目标服务器,这种代理诸如 Shadowsocks、 OpenVPN、ShadowVPN、GoAgent。另一种则拓扑比较复杂,支持将流量在多个节点的网络中传输并由一个节点代理掉,这种代理如 V2Ray、Tinc、Tor。
从可代理流量上分,VPN 可以中转所有的 IP 包,对于我们常用的 TCP、UDP、ICMP都可以完美应付,以全局路由形式工作。另一类是SOCKET代理,我们常用的 Shadowsocks、 V2Ray 都属于这种,这种代理不能代理 ICMP 包,往往以 SOCKS5 代理的形式提供服务,故也不便于为UDP提供代理(需要类似 SSTap 的工具强行转发)。
加速(Related Works)
目前对梯子的加速方式有下面几种。
自行实现拥塞控制和可靠性
典型代表:KCPTun、UDPSpeeder、FinalSpeed
这类的加速一般以 UDP 的形式进行通信,需要客户端和服务器端都进行部署。通过动态探测可用带宽,较为激进地多倍发包来降低丢包率继而达到提速效果。当然,因为多倍发包低效率占用带宽也往往被认为是一种损人利己的加速算法。在我一年多前的一次测试中, KCPTun 成功地用了8倍流量达到了加速2倍(还是很慢)的效果。
单边优化TCP拥塞控制
典型代表:BBR、锐速
这种加速需要修改内核,对于OVZ的虚拟主机并不适用(当然有奇怪的方式把它挪到了用户态)。这种算法也是致力于在有一定丢包率的链路上尽可能充分利用带宽。
BBR之前的拥塞控制(CUBIC、Reno等)采用加性增,乘性减的方式试探性增大发送窗口,在发现开始丢包时(包括拥塞丢包和错误丢包)减小窗口大小。因为公网延迟往往较高(同样带宽也较大),所以发送窗口=带宽*延迟
,得出较大的发送窗口,但也会导致较高的丢包继而减小窗口。在一定丢包率的高延迟线路上,该算法最终只会收敛到一个较小的发送窗口。
BBR通过估计带宽和延迟主动调整发送窗口实现了更高效的带宽利用。
避免被 QoS
运营商为了保证用户的使用体验(Quality of Service),对不同的流量会有不同的优先级。毕竟水管就这么大,被一个人占满,其他人就毫无体验了。
运营商QoS判断优先级的方式并不公开。通过一些黑盒观测,可以得出有端口优先级(比起不知名的奇怪端口优先保证80、443的Web访问)、流量类型优先级(UDP流量过大直接断流)和线路优先级(世界加钱可及)。
SSR 通过对加密后的 Shadowsocks 流量添加 HTTP 头伪装自己为 HTTP 流量,可以骗过某种程度的流量类型判别。 UDP2RAW 则通过 RAW SOCKET 给 UDP 包添加 TCP 的Header、模拟 TCP 三次握手等方式欺骗硬件交换机,使其以 TCP 包的优先级处理 UDP 包,可以减少一定的丢包率。
其他的软件,如 V2Ray 也支持动态端口来避免单个端口流量过大。对于其他不支持的软件,你也可以利用 iptables 来做到多个端口。
使用多连接为 Shadowsocks 加速(Methods)
项目地址:https://github.com/ihciah/go-shadowsocks-magic
现有 Shadowsocks 的通信方式和 clowwindy 最初公布的版本差别不大。
浏览器通过 ss-local 的 SOCKS5 协议通信,ss-local 在SOCKS5握手做完后与 ss-server 建立连接。连接建立后,所有通信均通过预定义的加密算法和密钥来加解密。这时 ss-local 通过一定的协议将目标机器的 IP/Hostname、Port 发送至 ss-server,ss-server 收到消息后解密并解析,主动连接。在链路建立后,直接将这几个连接串起来即可(当然加解密还是要做的)。
对于一些比较差的线路,由于较高的丢包率实际带宽不会很高,Youtube 720P都带不动。这时带宽瓶颈并不在 ss-server 到目标服务器上,而是在 ss-local 和 ss-server 的连接上。
对比浏览器下载文件与下载器(如IDM)下载的速度差异,如果单个连接收敛到较小的窗口,那么多个连接就可以尽可能地榨干带宽了。同样这个思路我认为也可以做在上述的瓶颈段:ss-local <====> ss-server
。
协议设计
首先 server 端需要多缓存一些数据,然后通过多条 client 主动建立的连接回传。
这部分可以有两种方式:
- 告诉 client 可选数据范围并给 client 选择数据的权力,client 发送目标数据段,server 回传这些段。
- 不管 client,由 server 主动下发数据
IDM 的下载方式类似于第一种。但是文件下载与数据代理有明显的不同:文件有固定的大小,并且服务器有全部的数据。
由于 server 的 cache 是在变化的,第一种方式较为低效并逻辑复杂,所以在这里我采用了第二种。 当然,在多连接管理上也有类似的问题,我在协议中将决定是否创建连接的权力交给 client 。
协议细节
下文中的 client 代指 ss-client, server 代指 ss-server。在描述中省略了加密解密的部分。
首先 client 与 server 建立连接
发送 Address。其中 Address 的第 1 个 byte 为地址类型。这里我添加了两个 bit 来标记加速开启状态。
magic-main
(0b01000
) 和magic-child
(0b10000
),分别代表主连接和次级连接。为该 Address 添加
magic-main
标记并发送。这时 server 会回应一个 16 byte 的dataKey
。持有该 Key 的其他连接才可以合法地请求到该数据。至此为止我们只有一条连接。该连接会源源不断地回传数据块。数据块格式为:
[BlockID(uint32)][BlockSize(uint32)][Data([BlckSize]byte)]
这时我们可以额外创建连接来加速数据收取。我们在新的线程中构造 “Address”:
[Type([1]byte)][dataKey([16]byte)]
,其中 Type 即magic-child
。创建额外的连接到 server,然后发送该 Address,然后收取数据块即可。
最终所有收到的数据块需要按照
BlockID
进行重排并返回给 SOCKS5 连接。一旦主连接中断,所有次级连接失效。若某个次级连接中断则不影响。
代码实现
我的代码基于 https://github.com/shadowsocks/go-shadowsocks2 。
项目地址:https://github.com/ihciah/go-shadowsocks-magic
在这个最初版本中,bufferSize 设定为 64 KB。为了避免为小文件建立太多连接,每 500ms 创建 2 条次级连接,直到 MaxConnection (此处设定为16)。该部分逻辑较为简单,可以继续做一些优化。
为了有良好的兼容性,server 端同时接受现有版本的 Shadowsocks 协议,即支持市面上的各种已有客户端。client 默认开启加速,只能连接到支持加速协议的 server 端。
(Emmmmm… 又不务正业划了三天的水,希望老板下次见面不要怼我=。=
加速效果(Experiment)
实验环境:
复旦大学校园网(张江校区100M有线网络) <—> AWS LightSail Tokyo <—> qd.myapp.com
Client: Windows 10 Enterprise 1803 (原版协议使用 shadowsocks-windows, 加速版使用静态编译版 go-shadowsocks-magic v1.0)
Server: Ubuntu 18.04.1 LTS (with BBR)
实验脚本:
https://gist.github.com/ihciah/bda1fab5dc1a8d0f70d7b0199f46169a
实验结果:
1 | PS C:\Users\ihciah\Desktop> python .\socks-test.py |
从实验结果可以看出,在比较辣鸡的网络中,多连接可以大大加快大流量的数据传输。
很惭愧,就做了一点微小的工作,谢谢大家。也欢迎提 issues(代码编写比较仓促,难免会有一些问题)。
延伸工作(2019-10)
对于国内 ISP ,多连接的确可以一定程度上起到加速效果。但是本文之前提到的 shadowsocks-magic 可能只能视为一个 POC,实际使用起来有一些问题。
对于每个 ShadowSocks 层面的连接,以一定延迟启动多个连接。这样一会造成连接数暴涨,这些连接断开后系统有大量 TIME_WAIT 状态的连接;二是只对连接时间较长的连接具有加速效果。
后续我做了另一件事:将多连接加速独立为一个单独的模块,可以支持对任意 TCP 流量的双边加速: Rabbit TCP。原理上仍旧是建立多条连接来承载上层连接的数据块并拆分与重组。不同的地方是除了内嵌在 Golang 代码中使用外,可以以独立代理的形式工作,并且可以在固定的连接数上负载任意多的上层连接。
与之前不同,底层连接是持续连接的。这样可以避免大量连接频繁建立与关闭带来的问题;还可以一定程度上降低建立连接带来的延迟;另一个可能的作用是改变了流量的时序特征。
当然坏处也是有的,因为负载的是任意 TCP 流量,并且需要隐藏 Block 数据包格式,所以这里需要在最外层做一次加密,对于 ShadowSocks 这种流量已加密的协议,相当于多做了一次加解密,会有一定的性能消耗。另一个坏处是可能需要做 QoS(目前没有实现),否则所有承载的流量权重相等,可能会有使用体验上的不适。
总的来说 Rabbit TCP 项目在稳定性上远高于之前的 shadowsocks-magic,使用 Rabbit Plugin 加速 ShadowSocks 流量体验良好。