Skip to content

Latest commit

 

History

History
232 lines (116 loc) · 16.1 KB

vless_v1_discussion.md

File metadata and controls

232 lines (116 loc) · 16.1 KB

vless v1 讨论

目前v1仍然处于研发当中。建议先用v0,等v1 完善了再说,本文只是理论探索,标准的实现 实际暂未完整实现所有的设计和讨论.

握手协议格式

具体我的探讨的一部分还可以查看 v2fly/v2ray-core#1655

总的来说 vless v1 简化了一些流程, 并重点考虑 非多路复用的 udp over tcp的 fullcone实现。

除了fullcone,我还想到了关于 内层加密, 连接池,以及 自动dns的 对协议的改进,请阅读全文了解详情。

vless v0中服务端 的回复也是有数据头的,第一字节版本号,第二字节addon长度;而首先v2ray根本没有addon,所以第二字节总是0,

而版本号的话,我约定服务端和客户端必须使用相同版本,

就是说,客户端请求使用v0,服务端不会返回一个v1出来;客户端请求v1,服务端也不会强制告诉客户端必须用v0,而是客户端要什么就是什么,

因此也不用再包含版本号,这样在v1中 服务端的回复就不必包数据头,可以减少内存操作,比v0更快。

而且udp的话, 会有 crumfurs 模式以及 普通多路复用模式这两种模式, 所以还是要加以区分.所以v1的addon部分依然不能删掉; 不过我在这里做一些变化, v1 的addon第一字节不再是 addon长度, 而是addon类型,之后可以通过addon类型来判断addon长度.

在v1 传递udp握手时, addon第一字节为1时, 表示 “选择传输udp使用 分离信道方式“,此时 单个通道不会再传输 raddr,因为仅用于单个raddr的信息传输.

所以v1的 tcp读写特别简单,一旦握手成功,不会有任何数据头产生,没有丝毫额外开销,直接直连 (与trojan相同)

传输udp数据 还是要有数据长度头的,这是udp的性质决定的,udp是基于包的,所以必须带长度头 来标明边界

但是因为websocket的数据包实际上首部就是带长度的,而且它这是在tls内部的,就和我们直接写数据长度一样

https://datatracker.ietf.org/doc/html/rfc6455#page-27

https://www.zhihu.com/question/29916578

所以,vless v1外面包websocket的话,是不需要再重新加长度的,加了就是多此一举。

虽然udp加长度只是加了两字节,但这只是write部分,read部分的话,为了防止粘包,就加了buffer,确实是多了一层内存读写操作,所以能精简就要精简,毕竟视频直播视频聊天等这种使用udp的部分都是 流量大户。总之这种优化能视udp的使用用途有不同程度的性能提升

而且作为翻墙要素我们是推荐ws或grpc来配合cdn的,所以还真是有用

隔离信道的 udp over tcp 的 fullcone 的实现属于 v1 重要的创新

关于udp over tcp 的 vless 的 fullcone

将目标地址放到一个map里,udp建立过的连接 也放到一个map里。然后第二次访问时查看map看有没有用过的,有的话就使用之前的连接

然而,这只实现了一半,只记录旧连接是不够的,因为需要把目标的地址也附带发送到客户端,不然的话,客户端是无法区分到底是谁发来的。

这就是udp和tcp的不同。tcp可以确保目标总是自己拨号的那个,而udp则是可以任意人都向你发信息

总之,还是要通过升级vless到1版本来实现。

具体的话,也不一定需要新cmd,直接在 CmdUDP时,使用不同的数据包格式即可,可以参考socks5和trojan的格式标准

比如trojan的: https://trojan-gfw.github.io/trojan/protocol

udp fullcone的信息传输过程

tcp代理是不需要fullcone的,也不可能实现;而因为udp的特殊性质,可能需要fullcone

先观察正常的tcp代理请求,发送一个tcp请求链接到代理服务器,然后直接就会使用这个连接传输双向的数据,这是因为目标是单一的。

而udp(fullcone)的代理请求是较为复杂的,尽管客户端只是发送了到 一个特定的远程 udp地址的 数据请求,但是因为要实现fullcone, 代理服务器所传来的信息不一定仅仅是 客户端第一次请求的 那个远程地址 所传来的,而可能是新的其它 地址所传来的(参考NAT打洞); 然后,在客户端接到这个新地址所发来的数据后, 客户端可能还会接着向这个新地址发送数据,而这时,代理服务器 需要能够判别,这个新地址是不是之前接受过信息的地址,如果是的话,则依然使用之前 代理服务器所使用的 端口进行发送,如果不是的话,那说明这个是无效信息(即,作为fullcone,某个端口(A) 只能向自己 第一个发送信息的 目的地址,以及 所有 曾经向 A 发送过来信息 的 远程地址 发送信息)

所以,随着udp代理的请求的时间推移,可发送信息的 远程地址会 呈增加的趋势,也就是说建立了与多个远程udp地址的 p2p链接。

如果是xray那种使用mux的办法,就是复用一个相同的tcp链接 去传输 多个连续的可以目标不同的 udp连接。

如果是我们要实现的新方法,则不是mux,即 每一次udp请求 都要产生一个 tcp连接,然后进行handshake。

看起来,反倒比mux复杂?但是,因为这个是并发的,所以虽然延迟可能高一些,但是却提高了吞吐量,网速能快一些,适合一些视频流的情况

所以,我们在vless v1版本中,除了并发的udp fullcone实现,也是要实现多路复用的,这样兼顾游戏与视频;

为了避免抄袭嫌疑,我当然不会重新使用mux协议来实现多路复用,而是自己设计一个传输协议。不过为了兼容现有客户端,我早晚还是要添加mux的支持,再说。

实际上,我还在思索,为什么vless一定要放到tcp上呢,为什么一定要udp over tcp呢?不能仿照socks5,直接在udp上传呢?也许是为了防探测吧,毕竟正常网页浏览的流量都是tcp的。但是实际上,完全可以伪装成 迅雷下载/微信视频流这种。也有人说可能是怕udp限流、QOS等,所以才出现的hysteria吧。

好像quic功能就是做这个的.

总之这个纯udp上的实现问题以后再讨论。先把 udp over tcp搞定。

之所以以后需要讨论纯udp的问题,可能是因为为了实现fullcone,实际情况会与 udp over tcp 有所不同

非多路复用的 udp over tcp的 fullcone实现

xray的 vless的mux的fullcone的办法是,在vless里包一个mux协议,然后持续使用该tcp连接 来在内部传送多个udp请求

而我们将要在这里实现的办法是通用办法,即不复用tcp连接的办法, 即每一个tcp链接里只包含于一个固定的远程地址的通讯。

不过,问题出现在监听上,我们监听的话,是会随时主动向客户端程序发送 远程服务器传来的信息的,这时,客户端提交 请求是 一个请求地址 一个tcp连接,是一一对应的,但是我们 返回数据的话,是无法主动跟客户端 建立连接的,而且 返回的数据的地址不一定是 和请求所用的 tcp连接 一一对应, 因为有可能出现 新的 客户端不知道 的远程地址 向 客户端 发送信息

这就是和 socks5(或trojan)的不同;socks5和trojan在传udp时实际上都是多路复用,使用的是同一条 代理客户端和代理服务端 建立的链接 来传输多个地址可能不同的数据

所以,我的想法是,使用一种“居中”的办法,即不完全模仿socks5; 当 代理服务器收到的udp信息的来源是 客户端已知的来源时,直接使用之前 客户端用于向该地址发送请求 的tcp连接 将数据发回 客户端 而 当新udp信息的来源(A)时未知的时,使用一条单独的信道向客户端发送该数据;

然后如果客户端 有新的向这个 A 发送的 数据请求时,再向代理服务器发起新的tcp连接,然后该tcp连接 会被保存,等未来 该A 有新数据 想发送到客户端时,使用这个特定的tcp连接。

这种好处是,如果目标地址 我客户端之前向其 发送过信息,则会复用同一条tcp连接,专门与这个地址发送信息;

既不是多路复用,也不是纯每个请求都建立新tcp链接。

可以说是 “隔离信道”。

具体实现

那个单独的用于向客户端发送 “新远程地址链接” 的信道,是提前由 客户端主动向代理服务端建立好的一条信道。

暂且称为 “未知udp地址的信息的接收信道”, "a channel that receives udp messages from unknown remote source"

我这里简称 "CRUMFURS"(信道). 该特殊信息称为 "UMFURS" (信息)

这个信道是多路复用的,但是仅限于未知udp远程地址发来的信息,而且仅限于服务端向客户端发送数据。

在vless从未主动申请过 udp请求时,不必 特地向代理服务器请求建立 CRUMFURS, 因为没有udp的需求。

那么,就需要新增一条 命令,比如 Cmd_CRUMFURS

当客户端 发送的vless信息中 使用 Cmd_CRUMFURS 命令后,则该链接保持连接(keep alive),专门用于客户端接受新udp数据。 同时 此时服务端就意识到,客户端以后会产生各种udp需求,并做好一些相关准备。

然后客户端第一次 申请向 一个远程地址(A) 发送 udp请求时,还是使用普通的 CMD_UDP命令 新建连接,而且 如果客户端期待回复的话,则客户端也不关闭这条连接(也是keep alive的),然后,当客户端想要 第二次向 A发送数据时,使用这个之前建立好的tcp连接。监听来自A发来的信息 也是通过读取这个 建立好的tcp连接。

CMD_UDP 信息 的数据和v0协议的内容保持不变。

Cmd_CRUMFURS 指令 的信息,到指令这一项之后 就会完毕。理所当然,因为只是发送一条建立信道的指令,不包含其它信息。

服务端接收到 Cmd_CRUMFURS 指令后(当然也是要在验证uuid符合之后),会返回一字节 CRUMFURS_ESTABLISHED

UMFURS 信息内容:1字节atype,几字节的IP(视ip为ipv4和ipv6. 因为是远程未知udp信息,所以肯定不可能是域名),2字节的port,然后后面直接接 该信息的承载内容。所以这个信息长度至少为8(7字节头部和至少一字节数据)

服务端对 udp 信息的转发 的行为会与正常 symmetric NAT 不同。并不是对不同的 请求的远程目标地址都要 新 dial一遍udp。因为每dial一遍,都会 新使用 服务端的一个 新的随机udp端口,而我们为了实现fullcone,在传输 到 Cmd_CRUMFURS 里出现过的 目标地址(A) 的数据时,还是要使用最初 代理服务端 接收 (A)发送来 数据 所使用的 的服务器udp端口

所以,服务端 要对每一个dial产生的 UDPConn进行保存;然后, 在接到 客户端的 CMD_UDP 请求时,首先查看 下列情况

  1. 以前是否向这个地址拨过号,如果拨过,直接从保存的列表中拿出来这个UDPConn,直接使用它继续发送数据
  2. 如果没拨过号,那这个远程地址是否曾向 保存的 UDPConn 中 其中一个 发送过数据,如果能找到这个UDPConn,就使用这个
  3. 如果自己没拨过号,也没有任何一个 UDPConn 接收 过 该远程地址的信息,则这个远程地址 属于新地址,我们就新Dial 一次。

所以,保存的机制就是一个map,以地址为 键,以 UDPConn为内容; 自己拨过的号所使用的地址(A)和该拨号产生的 UDPConn (U)作为一对 存入 map ;然后向(A)传来的任何其它未知地址(B)也和 (U)一起作为一对 存入 map

CRUMFURS 信道与普通UDP信道一样,要传 udp长度头。

流量特征的避免

不过,为了避免 【同时具有两个tcp链接,且一个tcp链接只有入方向,没有出方向】 的流量特征,我们实际上不能让crumfurs成为真正的单独通道,否则会被审查者轻易察觉。

所以上面讨论中 crumfurs 放在一个单独信道 实际上不切实际的。

我们 crumfurs信息的传输要隐藏在普通链接中。 所以我们udp数据头部除了长度头之外,还要有一字节的 crumfurs标识,如果为0 则意味着没有 crumfurs信息产生,而如果为1的话,后面会附带crumfurs信息。

这样的话,也不用单独一个 crumfurs命令, 也不用单独拨号crumfurs信道。

普通udp转发与本作新创建方式的对比

业界普通udp的fullcone转发是使用 socks5/trojan 方式的这种多路复用方式,建立一个udp信道,然后传输多种udp数据;

而本作提出的 crumfurs方式实际上 是开辟了一种新的 单路udp转发方式.

此时从socks5读出的每一个udp链接,视其raddr而定,如果raddr是没遇到过的地址,则我们会新建立一个新信道

连接池

我们可以通过使用新协议命令来做到 非mux的 连接池技术,可以在每个vless 头部添加 “是否关闭连接”的信息

如果准备关闭连接,则实际上不关闭连接,而是标记这个连接进行备用,然后等客户端下一次想要创建一个新连接时,直接使用这个现有的连接。

与 多路mux的不同是,多路mux是有同时连接的上限的,而连接池技术则没有,所以还是会比多路mux的方法速度更快

服务端主动返回ip地址

我们访问vless时,是可以访问域名的,而此时客户端是 不知道该域名是否会被墙的;

如果有一种方法,可以让客户端知道该域名的真实ip(不能客户端自己查dns,因为可能被污染),那就好了。

如果我们在 vless v1的 服务端返回的首包的信息头中,头部首先返回该 目标的 实际 ip,这样就相当于客户端免费做了一次dns查询。

比如,头部 : 如果不返回ip,则第一字节为0;如果返回ip,则第一字节为1,然后第二字节为 ip类型,标识是v4还是v6,然后后面是实际ip数据,最后跟着的才是首包的实际数据

感觉这个方法不错,使用这种方法,则只需一个geoip数据库就可以判断 分流,不再需要额外的 geosite

内层加密

vmess的加密方式过于繁琐,而且如果怕封未知流量的话还是要套tls

有些小伙伴怕纯tls 容易被中间人攻击,那么我们如果内层也加密的话就不怕了

可以考虑推出一种简化版的加密方式. 当然显然,这个加密是可选的,而且默认不配置的话肯定我们是不加密的,因为vlessv1外面必套tls

vmess我觉得最复杂的地方就是客户端和服务端每次都要同时生成一大串数,很麻烦啊!

传输网络层数据

在阅读一个issue时,偶然看到 【对ip协议的支持】的需求。

verysimple的架构是分层结构,不限定到底传输什么协议的底层数据,所以理论上是可以直接传输 ip协议的数据的

也许网络层的代理传输命令,可以加到 vless v1里,加一个CmdIP 即可

握手包长度混淆

v1 应该加一个 握手包长度混淆 功能, 与 naiveproxy的 padding 等价

因为虽然是在tls内部,tls record的长度还是能够暴露一些信息。如果每次tls握手的首个tls record的长度都不一样,那么能好一些。

目前认为, 不要太过随机,比如一个小时内 每个 tls握手的首个tls record 长度都不一样,这个又会导致熵太大。

目前认为,每一次启动 程序,都会随机选择一个 混淆填充长度,然后固定填充这个长度。这样短时间内首个tls record长度相同,又不会让审查着 获取到任何 与协议有关的 tls record 首包长度特征。

tls record 最大长度是16k,再长就要分片,而 一般http请求读到的 文件很有可能超过16k,一个html文件长达几百K是很正常的。

但是一个http响应不仅是返回文件内容,而还有http响应头。一般一个响应头不长,也就 几十 至 几百字节。

只要我们先伪装http响应头来完美模拟tls record首包,再做混淆填充 vless v1的握手包,就可以达到规避 流量 特征的目的

多路复用

应该全盘借鉴trojan-go,使用 smux+ simplesocks 作为 内层 多路复用的协议。