加速Shadowsocks

这篇文章将简要总结一下目前科学上网的加速方式,并详细介绍我基于 Shadowsocks 做的一个加速工作

科普(Introduction)

梯子的工作方式大多是类似的。

一般流量需要经过一台境外机器做中转,再由该境外机器访问目标服务器,这种代理诸如 ShadowsocksOpenVPN、ShadowVPN、GoAgent。另一种则拓扑比较复杂,支持将流量在多个节点的网络中传输并由一个节点代理掉,这种代理如 V2RayTincTor

从可代理流量上分,VPN 可以中转所有的 IP 包,对于我们常用的 TCP、UDP、ICMP都可以完美应付,以全局路由形式工作。另一类是SOCKET代理,我们常用的 ShadowsocksV2Ray 都属于这种,这种代理不能代理 ICMP 包,往往以 SOCKS5 代理的形式提供服务,故也不便于为UDP提供代理(需要类似 SSTap 的工具强行转发)。

目前对梯子的加速方式有下面几种。

自行实现拥塞控制和可靠性

典型代表:KCPTunUDPSpeederFinalSpeed

这类的加速一般以 UDP 的形式进行通信,需要客户端和服务器端都进行部署。通过动态探测可用带宽,较为激进地多倍发包来降低丢包率继而达到提速效果。当然,因为多倍发包低效率占用带宽也往往被认为是一种损人利己的加速算法。在我一年多前的一次测试中, KCPTun 成功地用了8倍流量达到了加速2倍(还是很慢)的效果。

单边优化TCP拥塞控制

典型代表:BBR、锐速

这种加速需要修改内核,对于OVZ的虚拟主机并不适用(当然有奇怪的方式把它挪到了用户态)。这种算法也是致力于在有一定丢包率的链路上尽可能充分利用带宽。

BBR之前的拥塞控制(CUBIC、Reno等)采用加性增,乘性减的方式试探性增大发送窗口,在发现开始丢包时(包括拥塞丢包和错误丢包)减小窗口大小。因为公网延迟往往较高(同样带宽也较大),所以发送窗口=带宽*延迟,得出较大的发送窗口,但也会导致较高的丢包继而减小窗口。在一定丢包率的高延迟线路上,该算法最终只会收敛到一个较小的发送窗口。

BBR通过估计带宽和延迟主动调整发送窗口实现了更高效的带宽利用。

避免被 QoS

典型代表:SSRUDP2RAW

运营商为了保证用户的使用体验(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。在描述中省略了加密解密的部分。

  1. 首先 client 与 server 建立连接

  2. 发送 Address。其中 Address 的第 1 个 byte 为地址类型。这里我添加了两个 bit 来标记加速开启状态。

    magic-main(0b01000) 和 magic-child(0b10000),分别代表主连接和次级连接。

    为该 Address 添加 magic-main 标记并发送。这时 server 会回应一个 16 byte 的 dataKey。持有该 Key 的其他连接才可以合法地请求到该数据。

  3. 至此为止我们只有一条连接。该连接会源源不断地回传数据块。数据块格式为:

    [BlockID(uint32)][BlockSize(uint32)][Data([BlckSize]byte)]

  4. 这时我们可以额外创建连接来加速数据收取。我们在新的线程中构造 “Address”:[Type([1]byte)][dataKey([16]byte)],其中 Type 即 magic-child

    创建额外的连接到 server,然后发送该 Address,然后收取数据块即可。

    最终所有收到的数据块需要按照 BlockID 进行重排并返回给 SOCKS5 连接。

  5. 一旦主连接中断,所有次级连接失效。若某个次级连接中断则不影响。

代码实现

我的代码基于 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
2
3
4
5
6
7
8
9
10
11
PS C:\Users\ihciah\Desktop> python .\socks-test.py
Start Download https://qd.myapp.com/myapp/qqteam/pcqq/QQ9.0.8_3.exe.
<Response [200]>
[Original]Done 22.401 seconds.
Start Download https://qd.myapp.com/myapp/qqteam/pcqq/QQ9.0.8_3.exe.
<Response [200]>
[MY]Done 14.744 seconds.
==========
Original: 22.40120 sec
My : 14.74440 sec
==========

从实验结果可以看出,在比较辣鸡的网络中,多连接可以大大加快大流量的数据传输。

很惭愧,就做了一点微小的工作,谢谢大家。也欢迎提 issues(代码编写比较仓促,难免会有一些问题)。