由于我的 Blog 原始文件托管在 GitHub pages 上,推送到个人仓库后上一篇文章立即被相关人士扩散并引发争议,并被指出内容存在错误。

事后,我与该作者进行了交流,也重新分析了相关协议,所以我在这里分享我的改正的对 VLESS 协议的新的理解。

由于有好事者盯着我的个人仓库看,我已经将其转为 private,且也不会删除上一篇文章,留作参考。

我上一篇文章写:

Xray 对 UUID 使用了 ReadFull,这意味着 VLESS 可能并没有与 Trojan 相同的探测保护,向其发送 1+15
字节后停止,服务器长时间等待将会成为特征。

但这是错误的,由于我简易检查时忽略了 Xray 代码的复杂度,没有发现外部再次嵌套了缓冲区再次进行了处理,这也是他在频道中唯一反驳我的。

关于 VLESS 的设计,我曾以为如此设计必然不会主动宣传,然而我确实在文档中发现了作者对此的解释。

version

“协议版本”不仅能起到“响应认证”的作用,还赋予了 VLESS 无痛升级协议结构的能力,带来无限的可能性。 “协议版本”在测试版本中均为 0,正式版本中为 1,以后若有不兼容的协议结构变更则应升级版本。

VLESS 服务端的设计是 switch version,即同时支持所有 VLESS 版本。若需要升级协议版本(可能到不了这一步),推荐的做法是服务端提前一个月支持,一个月后再改客户端。VMess 请求也有协议版本,但它的认证信息在外面,指令部分则高度耦合且有固定加密,导致里面的协议版本毫无意义,服务端也没有进行判断,响应则没有协议版本。Trojan 的协议结构中没有协议版本。

总结:VLESS 具有无痛升级协议结构的能力、无限的可能性、同时支持所有版本;VMess 请求也有协议版本,但毫无意义;Trojan 的协议结构中没有协议版本。

简单一个 version 字段说明即可看到作者的语文功力。然而,没有提到的是:

  • VMess 兼容至少三种版本的旧协议
  • Trojan 设计简单、设计此字段没有作用
  • VLESS 始终使用 0 作为版本号,从未有过 “正式版本”

而 VLESS 即使在 patch release 中 break 了 vision 子协议,也没有修改过该字段。

当我检查当时的版本时,发现 Release Notes 都已经被删除,而被编辑为一个 Telegram Channel 的中文 post,没有任何对协议修改的描述,剩下的更新内容也只有类似的宣传。

uuid

接下来是 UUID,我本来觉得 16 字节有点长,曾经考虑过缩短它,但后来看到 Trojan 用了 56 个可打印字符(56 字节),就彻底打消了这个念头。服务端每次都要验证 UUID,所以性能也很重要:VLESS 的 Validator 经历了多次重构/升级,相较于 VMess,它十分简洁且耗资源很少,可以同时支持非常多的用户,性能也十分强悍,验证速度极快(sync.Map)。API 动态增删用户则更高效顺滑。 https://github.com/XTLS/Xray-core/issues/158

总结:比 Trojan 更简单,相较于 VMess 更简洁、消耗资源更少,“可以支持非常多的用户,性能也十分强悍,验证速度极快,API 动态增删用户则更高效顺滑”。

没有说的:

Trojan 使用密码 + KDF 而不是 UUID,滥用 UUID 作为密码和 key 与错误的 UDP 设计一样继承自 VMess。从安全性上来讲,使用相同长度的密码,对 Trojan 进行暴力破解需要更多的尝试次数,且如果连接被 TLS 中间人解密,Trojan 也不会泄漏原始密码文本。

后半段是一种田忌赛马的对比法,VLESS 和 VMess 根本不是同类型的协议,而这些鲜艳的优点并不是因为 VLESS 作出了什么改进,而是因为 VMess 为了兼容性保留了大量历史遗留设计,即使这些优点对于的 Trojan 都不存在也要拿来宣传,可以一瞥作者的思维。

protobuf

引入 ProtoBuf 是一个创举,等下会详细讲解。“指令”到“地址”的结构目前与 VMess 完全相同,同样支持 Mux。

似乎只有 VLESS 可选内嵌 ProtoBuf,它是一种数据交换格式,信息被紧密编码成二进制,TLV 结构(Tag Length Value)。

总结第一段:

  • 使用 ProtoBuf 来序列化 string 是一个创举 (定义结论)
  • 似乎只有 VLESS 可选内嵌 ProtoBuf (暗示)
起因是我看到一篇文章称 SS 有一些缺点,如没有设计错误回报机制,客户端没办法根据不同的错误采取进一步的动作。 (但我并不认同所有错误都要回报,不然防不了主动探测。下一个测试版中,服务器可以返回一串自定义信息。) 于是想到一个可扩展的结构是很重要的,未来它也可以承载如动态端口指令。不止响应,请求也需要类似的结构。 本来打算自己设计 TLV,接着发觉 ProtoBuf 就是此结构、现成的轮子,完全适合用来做这件事,各语言支持等也不错。

目前“附加信息”只有 Scheduler 和 SchedulerV,它们是 MessName 和 MessSeed 的替代者,当你不需要它们时,“附加信息长度”为 0,也就不会有 ProtoBuf 序列化/反序列化的开销。其实我更愿意称这个过程为“拼接”,因为 pb 实际原理上也只是这么做而已,相关开销极小。拼接后的 bytes 十分紧凑,和 ALPHA 的方案相差无几,有兴趣的可以分别输出并对比。

为了指示对附加信息(Addons,也可以理解成插件,以后可以有很多个插件)的不同支持程度,下个测试版会在“附加信息长度”前新增“附加信息版本”。256 - 1 = 255 字节是够用且合理的(65535 就太多了,还可能有人恶意填充),现有的只用了十分之一,以后也不会同时有那么多附加信息,且大多数情况下是完全没有附加信息的。真不够用的话,可以升级 VLESS 版本。

为了减少逻辑判断等开销,暂定 Addons 不使用多级结构。一个月前出现过“可变协议格式”的想法,pb 是可以做到打乱顺序,但没必要,因为现代加密的设计不会让旁观者看出两次传输的头部相同。

下面介绍 Schedulers 和 Encryption 的构想,它们都是可选的,一个应对流量时序特征问题,一个应对密码学上的问题。

这是展示其设计水平的关键段落,我将其分为两个部分:

  1. 认为 “一个可扩展的结构” 对于错误回报机制是很重要的。

实际上还是从 VMess 继承的思维,认为协议与 “传输层” 和 “XTLS” 等大量子协议组合分离、随意更改是没有问题的。随之而来的,既然要应对其他协议的更新,就需要允许可变甚至存在未知数据的协议格式。

这种思维的语言腐败遗毒深远,导致大多数不明所以的人认为 V2Ray 发明的私有 “传输层” 不是使用这些协议名字的私有未定义格式,而是是一种通用的协议,且与非 V2Ray 系协议的组合是没有问题的,而不是一种未定义行为。

  1. 认为对于序列化非定长的数据结构,更应该使用 ProtoBuf,原因包括各语言支持都不错

这是其设计的最核心问题,我想用一些例子解释。

对于其实际传输的子协议名称 flow,最少的数据格式可以为:

Flow
uint8

这完全足够,在可预见的未来内, VLESS 不会有超过 256 个子协议。

如果考虑到要像 TLS extension 一样支持扩展,可以设计为:

LengthFlow
uint8uint8

仍然完全足够,使用 uint8 作为长度,是因为作者称 255 字节是够用且合理的。

但 VLESS 使用的实际格式为:

ProtoBuf LengthProtoBuf HeaderFlow LengthFlowSeed LengthSeed
uint8byteuvariantstringuvariantstring

不仅将子协议常量作为字符串传输,还在总长限定为 255 以内的数据内对其字段使用了 uvariant,即一种允许长度大于 255 的格式。

从 ”当你不需要它们时,也就不会有 ProtoBuf 序列化/反序列化的开销” 中也可以看出作者实际上并不熟悉数据格式,才对于一到二个字节可以传输的结构,使用了与 JSON 同类的的二进制应用层协议 ProtoBuf v2 message 来代替编写二进制协议。

不仅如此,VLESS 的服务器回应直接使用了请求中的 ProtoBuf 格式作为回应内容;在实际使用中,这些字段从未赋值,也没有实现上文提到的 ”错误指示“ 功能。

UDP 问题

VLESS 继承了与 VMess 一样,但又不存在于其他所有代理协议的设计问题。鉴于很多人对此不了解,我想对相关介绍作出一些简单解释:

UDP 是基于数据包而不是流的协议,因此,在应用传输中,每一个数据包都带有目标地址;但 V2Ray 由于早期开发者设计水平问题,在架构中将 UDP 数据包 DNAT 后为作为连接与 TCP 一同作为流来中继。

而作者的修复方式是发明一个新的子协议,在其中添加 UDP 地址以达到修复的目的,这也显现出了作者在协议设计上的不专业。

而关于作者对这个新的子协议的描述作为结尾:

#237 已阐明 V 系协议的 UoT 结构无法实现 FullCone,然而 Xray-core v1.3.0+ 却神奇地做到了本被认为是不可能的事情:

至此,Xray-core 实现了全协议、全出入站、全传输方式完美支持 FullCone 🎉

最后,仓库右上角点个 Star,谢谢!

尾声

与 VMess 的高度耦合不同,VLESS 的服务端、客户端不久后可以提前约定好加密方式,仅在外面套一层加密。这有点类似于使用 TLS,不影响承载的任何数据,也可以理解成底层就是从 TLS 换成预设约定加密。相对于高度耦合,这种方式更合理且灵活:一种加密方式出了安全性问题,直接扔掉并换用其它的就行了,十分方便。VLESS 服务端还会允许不同的加密方式共存。

对比 VMess,VLESS 相当于把 security 换成 encryption,把 disableInsecureEncryption 换成 decryption,就解决了所有问题。目前 encryption 和 decryption 只接受 "none" 且不能留空(即使以后有连接安全性检查),详见 VLESS 配置文档

VLESS 作为一个在基本只会在 TLS 内传输的协议,宣传是对 VMess 这样的全加密协议的改进,处处拿 VMess 论证先进性;而实际上继承了 VMess 的大量缺点,就连目标地址格式也照搬 VMess,而不是使用被 SOCKS、Shadowsocks、Trojan 等协议广泛使用的 socksaddr。

关于这个协议的总结:整体设计起源于 Trojan、包含了大量 VMess 的缺点、实现了 Xray 需要在不修改协议的情况下修改协议(附加子协议) 的需求。