最近在开发自己的私人代理软件,目标是实现0RTT,并且基于TCP(最近UDP相关的协议很火,Hysteria 2,tuicV5等)。
一些心得随便记录一下:
1、方案
从架构图中我们可以看到,我们要实现极致的速度,就必须要让浏览器与代理客户端,代理客户端与代理服务器之间的时间最优化。
首先,要实现0rtt,如果不特别考虑安全性,很容易实现。主流的socks协议是1rtt,需要先链接建立,然后才能发送请求数据,0rtt就是要在第一次给代理服务器发送消息时,就要携带请求数据(比如Htpps就是ClientHello消息),经过对多种主流代理协议的源码阅读,实现0RTT目前有如下几种方案。
首先看socks这边,因为没法修改socks协议的内容,所以只能从协议实现上着手。
socks连接首先要发送一个Connect消息,表示自己要与目标服务器建立连接,然后socks服务器与目标服务器建立成功之后,告诉socks客户端,连接建立ok了,可以发送数据了,然后socks服务器就在socks客户端和目标服务器之间进行数据relay。
基本上所有代理软件都有一个固定的优化手段:提前返回与目标服务器建立成功给socks client,本质上就是赌与目标服务器的连接建立一定成功。提前告诉socks客户端,连接已经建立,然后socks客户端应该就会发送请求数据了,然后代理软件再将客户端的请求数据和连接请求一起打包发送给服务端。
这种方案实际上没啥大问题,因为如果建立失败了,后面连接依然会断开,但是使用socks代理的软件可能会认为服务器出问题了,明明连接都建立了,又突然断开了。
那么如何解决代理客户端和代理服务端之间的延迟问题呢?
1.1 连接复用方案
正常情况下代理客户端(比如ss)对于在每个socks请求,都会与代理服务端建立一个新的tcp连接,在socks连接关闭之后,关闭tcp连接,这就不可避免带来了tcp连接建立的时延。解决tcp连接建立时延的办法就是tcp连接复用。
tcp连接复用又分为两种方案:
1.1.1 协议复用
是在一个tcp连接上跑多个子流,比如grpc,http2,yamux方案等复用协议,好处就是建立的tcp连接可以比较少,比如naive协议,就用一个tcp连接就可以同时传输几十个代理请求。缺点就是有复用协议开销,可能会损失一部分流量,因为要维护发送窗口等,避免某个子流速度太快导致其他子流没发传输数据,还有就是实现比较复杂,要实现一个完整的http2协议,还需要支持流优先级等,另外一个问题就是协议参数的配置,比如窗口大小等,对实际的代理速度影响很大。
1.1.2 tcp连接复用
tcp连接复用就是在socks连接断开时,不会真的断开与远端代理服务器之间的tcp连接,而是留着,在下一个socks连接请求时,可以直接利用这个tcp连接传输数据,进行代理,典型的协议就是snell协议。这种方案的好处就是没有了tcp连接建立的时延,而且没有协议复用的额外开销,也没有窗口更新等控制消息。缺点就是tcp连接可能会比较多,不过可以通过超时将长期闲置的tcp连接关闭,还有就是socks连接在关闭时,可能还是会接受对端数据,如果是下载等场景,可能会比较大,因为不想tcp连接在关闭的时候,可以不再发送缓存的数据,服务端在接收到socks断开的请求也需要时间,在这段时间内,服务端可能还会持续发送大量数据。还有一个问题就是实现的过程中需要维护好这个tcp连接的状态,另外一个问题就是这个tcp连接如果长时间不用,可能会断开,但是客户端并不知道,导致在下次socks请求中发送tcp失败,导致socks连接出错的问题,不过都是一些小问题。
我自己实现了基于yamux和http2的协议复用以及snell的tcp连接复用,最终的效果是tcp连接复用完胜。所以最终选择了tcp连接复用。