Skip to content

第27章 VMess:身份、受保护请求与数据分块 ​

27.1 同一个协议名称下,必须区分头部认证和负载加密 ​

VMess与VLESS都常用UUID配置用户,但它们对UUID的用途不同。VLESS基本请求直接传送原始用户标识,再依赖所选安全层保护;现代VMess把UUID作为长期共享秘密的来源,先构造受保护的身份信息,再加密认证请求指令,指令里还携带本次负载所用的随机密钥与IV。

因此,网络上看到的现代VMess开头不是明文UUID。服务器要根据已配置账号尝试识别AuthID,再派生该请求的头部密钥,验证长度和指令,之后才知道目标以及负载参数。身份识别、指令保护、数据保护彼此关联,但分别有自己的结构和状态。

VMess历史上采用过HMAC-MD5形式的身份值及AES-CFB指令保护,官方文档已将该认证方式弃用。旧配置中常见alterId等兼容概念,不能据此给现代请求再凭空添加一组旧字段。本章以Xray-core提交0888beb38a9ba45440557175e71ce21cf8ba3b13为实现基线;它所列encoding客户端直接调用SealVMessAEADHeader,服务端按现代AEAD验证路径处理,本章不把官方历史双路径描述冒充该提交仍执行的全部分支。

还有一组名称容易混淆:VMess AEAD认证格式指的是请求头使用认证加密;账号security所选AES-128-GCM或ChaCha20-Poly1305通常指的是后续数据部分。请求头采用AES-GCM,不等于负载只能用AES-GCM。反过来,历史负载曾使用AEAD,也不能自动证明历史指令头已经有同样的认证保护。

状态或材料主要职责是否逐块明文发送
用户UUID与CmdKey长期账号及头部派生来源否
AuthID识别账号、约束时间与请求重复请求起始16字节为受保护形式
connectionNonce区分本次头部派生上下文请求中发送8字节
请求Body Key/IV本次上行负载保护放在受保护指令头内
响应Body Key/IV本次下行负载及响应相关保护从本次请求材料派生
负载块计数与长度状态定界和逐块保护依选项处理;计数不等于明文流号

基础VMess并没有TLS1.3式临时ECDH或ML-KEM握手。客户端可以在连接可用后直接发送受保护请求,服务器识别后建立目标连接。但“没有先交换公钥”不能解释成“实现没有状态”:服务器保存账号和防重放信息,每条连接还维护Body密钥、Nonce计数、掩码与缓冲。任何后续数据均不依赖之前状态的说法,都不适合用来解释实际分块流。

同样,随机生成每次Body密钥不等于前向保密。它们通过可由长期账号秘密解开的请求头交给服务器;账号秘密之后泄漏,历史头中的随机材料也可能被恢复。外面若另有带PFS的TLS,必须分别分析其保护终点和密钥,而不是把外层性质归给基础VMess。

本章先拆AuthID及头部,再解释指令字段与负载,最后使用固定材料复核一份构造请求。历史说明可参照V2Fly VMess文档,实际现代路径以固定encoding/client.go、encoding/server.go及aead/encrypt.go为准。

27.2 UUID不在明文头里:CmdKey与AuthID ​

现代VMess先把UUID原始16字节与一段固定ASCII字符串连接,经MD5形成16字节CmdKey:

text
CmdKey = MD5(UUID原始16字节 || ASCII("c48619fe-8f02-49e0-b9e9-edf763e17e21"))

固定字符串包括连字符,共36字节,不应先当作UUID解析成另一份16字节。用户自己的UUID则相反,必须使用原始16字节,不能把36字符文本直接放进这个公式。CmdKey不是公开标识,它与UUID具有相同的长期秘密来源;MD5在这里也没有把可猜的短串变成高熵秘密。

接下来需要一种既可被服务器识别、又不会直接暴露UUID的起始字段。AuthID的明文正好16字节:

偏移字段长度和编码
0—7Unix秒时间戳8字节大端有符号整数
8—11随机材料4字节
12—15前12字节的CRC32-IEEE4字节大端

固定实现以KDF16(CmdKey, "AES Auth ID Encryption")得到AES-128密钥,再对这一个16字节块加密,产生网络上最初的16字节。这里是单块AES运算,不包含独立IV或GCM Tag。CRC帮助识别候选解密结果,不是一个带秘密的消息认证码;不能因为CRC通过,就跳过后续GCM头验证。

本章所写KDF是VMess专用的嵌套HMAC-SHA256构造,不是HKDF。其根使用VMess AEAD KDF作为HMAC密钥;每增加一个路径项,就以此前的带密钥散列构造为下一层HMAC的散列函数、以路径原始字节作该层密钥,最后把输入秘密写入最外层,得到32字节。KDF16截取前16字节。路径既可为固定ASCII,也可为AuthID或Nonce原始二进制,不能先转成十六进制文本。

更形式化地说,令H0(m)=HMAC-SHA256(ASCII("VMess AEAD KDF"),m);对路径p1到pn依次定义Hi(m)=HMAC的散列函数取Hi−1、密钥取pi、消息取m,最终KDF(k,p1,…,pn)=Hn(k)。这里Hi−1是一个完整的带密钥散列构造,不是只取它对某次输入的32字节输出再当作普通SHA256密钥。这个区别会改变所有派生结果。

服务端为账号保存AuthID解码器,依候选CmdKey导出的AES密钥尝试解密。若CRC不符,继续其他账号;匹配后再检查时间与重放。固定Match拒绝负时间,要求与当前Unix时间差不超过120秒,并通过持锁MapFilter查重、记录原始加密AuthID。这个窗口来自所列实现,不能拿旧MD5认证的时间说明或Shadowsocks 2022的±30秒规则替换。

随机4字节使相同账号在同一秒可以产生不同AuthID,但不消除重复的可能;服务端仍需状态检查。完整头派生还使用另外8字节connectionNonce,所以“AuthID重复检测”与“所有头部派生输入的随机性”也不能混为同一个条件。更多随机材料不替代检查,去重也不替代密钥/Nonce设计。

Match识别到账号并记录AuthID后,后续长度和指令GCM验证仍可能失败。因此一条“AuthID已见过”的记录不等于目标连接已经建立,账号候选成功也不等于整个请求被接受。重启、跨实例状态、时钟变化和缓存保留策略还会影响整体重放边界;本书这里只解释固定路径,没有声称它提供业务级恰好执行一次的保证。

CmdKey见固定common/protocol/id.go,AuthID布局、匹配与120秒检查见aead/authid.go,专用派生及标签见kdf.go与consts.go。

27.3 认证长度、connectionNonce与受保护指令 ​

账号被AuthID识别后,服务端还不知道后续指令有多长。现代请求把这一步拆成两次AES-128-GCM操作:一次保护2字节长度,一次保护长度所指向的完整指令。两者使用不同KDF标签,避免把同一份上下文误用于两个职责。

text
AuthID16 || 长度密文18 || connectionNonce8 || 指令密文N || 指令Tag16

注意connectionNonce在网络上位于长度密文之后。服务端先收齐18+8字节,再用两者所需上下文派生并打开长度;它不能见到18字节就立即解密而忽略尚未读到的8字节。connectionNonce不是GCM直接使用的12字节Nonce,而是派生输入。

令C为CmdKey,A为加密AuthID的16字节,R为connectionNonce的8字节,派生如下:

职责AES密钥取专用KDF前16字节GCM Nonce取专用KDF前12字节
长度KDF(C, "VMess Header AEAD Key_Length", A, R)KDF(C, "VMess Header AEAD Nonce_Length", A, R)
指令KDF(C, "VMess Header AEAD Key", A, R)KDF(C, "VMess Header AEAD Nonce", A, R)

两次GCM的AAD均为A本身。A既进入派生,也被直接绑定为认证附加数据;R也通过派生影响密钥和Nonce。把A或R改位后,不能继续用原来的Tag验证出同样结果。长度明文为2字节大端N,且不含指令Tag;接收端在长度认证成功后才按N+16读取指令。

请求包装比指令明文多58字节:AuthID16、认证长度18、connectionNonce8、指令Tag16。它与第26章Shadowsocks的“每块长度/负载对”不同:这里保护的是一次请求的指令头;接下来的Body有自己的分块、密钥和Nonce,不重复这58字节外壳。

指令中包含随机生成的上行Body Key16、Body IV16和响应认证字节V。客户端不会等待服务器先发另一套Body密钥;服务端解开指令后直接取得这些材料。现代路径把SHA256(请求Body Key)的前16字节作为响应Body Key,SHA256(请求Body IV)的前16字节作为响应Body IV,区分两个方向。

响应头仍使用认证长度和认证内容,但没有再发送请求式AuthID与8字节connectionNonce。其长度密钥从响应Body Key按标签AEAD Resp Header Len Key派生,长度Nonce从响应Body IV按AEAD Resp Header Len IV派生;内容相应使用AEAD Resp Header Key与AEAD Resp Header IV。两次GCM的AAD为空。不能把请求头的A/R派生公式直接复制到响应方向。

通常无额外命令的响应头明文由V、选项、命令00、长度00组成4字节,因此认证长度18加内容4及Tag16,总计38字节。客户端解密后还会检查V与本次请求保存的随机字节一致。这一字节是会话关联检查的一部分,不是单独提供128位或更高安全强度的秘密MAC;其外面的GCM验证不可省略。

整体密码依赖仍指向长期UUID。持有它的人能得到CmdKey,对截获请求解开指令,再恢复随机Body材料及派生响应材料。因此基础VMess没有因“每次随机Body Key”而获得PFS,也没有因为响应不重复AuthID就成为与请求无关的独立流。

长度、指令及AAD见固定aead/encrypt.go,准确标签见consts.go,Body随机生成、响应派生及V检查分别见encoding/client.go与server.go。

27.4 指令头:把随机Body参数与目标放在同一个认证范围 ​

打开AHeader后,服务端首先读取38字节固定部分,再按命令读取目标、头部随机Padding和4字节FNV校验。本节偏移均从解密指令明文起点算起,不包含外面的16字节AuthID、18字节长度密文或8字节connectionNonce。

偏移字段长度与说明
0版本客户端写01
1—16请求Body IV16字节随机值
17—32请求Body Key16字节随机值
33响应认证V随机1字节,响应应匹配
34Opt1字节选项位
35高4位头Padding长度P0到15
35低4位Body加密方式Sec如03为AES-128-GCM,04为ChaCha20-Poly1305
36保留客户端写00
37CmdTCP01、UDP02、Mux03等按具体处理支持
38起目标TCP/UDP为端口2大端、地址类型1、地址
目标之后头PaddingP字节随机材料
最后4字节FNV-1a校验对前面全部指令明文计算,结果大端

域名目标为01 bb、类型02、长度0b及example.com。IPv4类型01,IPv6类型03。此处沿用VMess/VLESS共用地址规则,不能把Shadowsocks/SOCKS5的域名03与IPv6 04照搬过来。Mux路径不读取同样的目标字段,而使用内部v1.mux.cool标识,由下一层复用格式承载目标语义。

example.com的目标部分长15字节:端口2、类型1、域名长度1及域名11。加固定38、末尾FNV4,指令明文共57+P字节;现代认证包装后为115+P字节。IPv4、IPv6的指令明文分别为49+P、61+P。这里还没有任何网站应用负载,也没有TCP/IP头或外层HTTP/TLS。

Opt中0x01是分块标志,0x04是长度掩码,0x08是全局负载Padding,0x10是认证长度实验选项。头Padding的P与全局负载Padding不是一个字段:前者长度在35字节高4位,改变本次AHeader;后者使之后每个Body块可能增加不同数量的尾部Padding。

固定正常出站在AES-GCM或ChaCha20-Poly1305下设置分块、掩码和全局Padding,因此常见Opt为0d。实验开关可再加0x10。account的auto会按硬件能力解析成具体Body方法,客户端把具体Sec写入头;它不是在网络上先发auto字符串等待服务端测速。所读服务端解析后也拒绝UNKNOWN或AUTO结果。

响应关联V只占1字节,FNV只占4字节,这并不是VMess把安全强度降成8位或32位的意思:它们位于经过AES-GCM认证的指令结构内。反过来,也不能因为FNV被函数命名为Authenticate,就把它当作独立密码MAC。FNV没有秘密,任何人知道明文就可重算;现代完整性依赖外部GCM密钥、Nonce和Tag。

服务端取得Body Key与IV时,还以用户UUID、这份Key及IV组成内部sessionID,加入SessionHistory。这个内部结构不在头中增加另一份48字节字段,也不是QUIC式可见连接ID;它是额外的会话重复检测状态。其具体保存和清理边界将在27.8节与AuthID窗口一起比较。

指令的最终目标、Body参数和选项必须作为一个整体解释。即使地址解析成功,未知Sec或不兼容选项也可能使Body阶段失败;即使GCM Tag通过,也仍需执行结构、长度、命令与策略检查。认证证明字节在密钥下有效,不等于每种有效编码都满足代理业务规则。

本节字段与写读顺序见固定encoding/client.go、server.go;位值和auto硬件选择见common/protocol/headers.go,默认选项见outbound.go。

27.5 Body分块:掩码、认证与Padding各做什么 ​

默认AEAD Body并没有沿用请求头那套“认证长度18字节”的格式。普通路径先用2字节长度字段描述随后的密文、Tag及Padding;若启用掩码,它只对这个长度做异或。负载内容另外由AES-128-GCM或ChaCha20-Poly1305认证加密。

text
长度字段2(可掩码) || 负载密文L || Tag16 || 尾部Padding Q

长度解码后的值为L+16+Q,不含前面的2字节。接收端依据相同状态得到Q,读取相应字节数,去掉尾部Q字节,再对密文与Tag执行AEAD验证。固定实现的Padding放在Tag之后,用安全随机源填充,不属于这次负载AEAD的明文或AAD。它不能当作经过同一Tag保护的内容。

AES路径直接使用请求Body Key16。ChaCha20-Poly1305需要32字节密钥,本实现把MD5(Body Key16)与MD5(前一个16字节MD5结果)连接得到32字节。这个扩展不能增加原始随机秘密的熵,也不是把请求头加密算法改成ChaCha。

负载Nonce从相应方向Body IV复制而来,前2字节替换为大端16位块计数,第一次为00 00,然后00 01;AEAD使用前12字节,所以余下10字节来自Body IV的索引2—11。它与Shadowsocks的96位小端计数器不同,也不把一个32字节公开Salt写在Body开头。

需要准确记录一个长连接边界:固定GenerateChunkNonce的计数变量是uint16,生成器本身没有在回绕前拒绝或换钥的分支。若同一实例在同一Body密钥下被调用65537次,第65537次会再次生成计数0所对应的Nonce。其他路径是否提前结束连接需要另查,不能把这个局部生成器描述成无限唯一的Nonce来源。本书未运行超长连接验证,也不从这个局部事实编造一个已复现攻击;但同密钥Nonce唯一性必须作为安全分析的条件,不能省略。

长度掩码来自SHAKE128。两端以该方向Body IV初始化同样的SHAKE输出流,每次取2字节大端数作为掩码M,线上长度为M异或真实长度。若启用全局Padding,则先从同一个SHAKE流再取2字节并对64取模,得到Q=0到63;随后才取下一个2字节作长度掩码。读取顺序一旦错位,后续所有长度都会错。

操作消耗的SHAKE输出用途
计算本块Q(启用Padding时)下2字节,解释为大端后mod 64决定尾部Padding数
计算长度掩码(普通掩码路径)再下2字节异或L+16+Q
形成负载Nonce不消耗SHAKE16位块计数与IV固定片段

异或掩码不是长度的密码认证。改变长度可能导致读错边界或后续Tag失败,不能把这些结果说成字段本身具有MAC。固定源码另外提供AuthenticatedLength实验:以auth_len标签派生专用密钥,用AEAD保护2字节长度,长度编码区变为18字节,且不会继续按普通SHAKE异或长度路径解释它。

这个实验路径还复用通用AEADChunkSizeParser语义:其Encode先从传入的总后续长度减去16字节认证开销,写成2字节明文;Decode验证后再加回16。不能看到参数名size,就把加密前的长度一律写成L+16+Q。Padding生成仍可能使用原SHAKE状态,但长度解析器已经替换,双方必须遵循完全相同的选项和派生。

例如普通掩码路径传hello5字节,假设本块Q=7,则解码长度为28,线上块总长30。Q=7只是构造条件,真实值由IV和SHAKE位置确定,不能随意改Padding长度又期望接收端自行猜中。若不计Padding,AEAD负载块的固定开销为2+16=18;启用认证长度时变为18+16=34,再另加实际Q。

固定写流路径依据buf.Size=8192,并预留Tag16、普通长度2及最大Padding保留64,得到每次切分负载上限8110字节;实际Q最大63,所以“保留64”与“会发64字节Padding”不是同一件事。认证长度路径还需多保留16字节。协议字段容量、这个写入切分上限和TCP段长仍分别属于不同层。

结束标志也应按代码而非名字理解。默认出站可在正常发送结束后写一个空内容块;接收通用AuthenticationReader在解码长度等于Tag开销加Padding时,就置done返回EOF,先于读取和验证该空块正文。因此不能把这个路径宣称成TLS close_notify那样已验证的关闭记录,尤其普通长度掩码自身没有认证。NoTerminationSignal开关还能禁用发送该标志。连接完整性需要结合所选长度保护、实际结束路径和上层应用定界判断。

UDP命令则把数据报作为Body包块转发到目标,但这些块在基础VMess承载连接中仍受有序流运输约束。它不等于Shadowsocks那种各包独立UDP保护,也不会因为内部标记为UDP就摆脱外层TCP丢包造成的等待。

以上细节见固定encoding/auth.go、client.go、common/crypto/auth.go及chunk.go。本节分别标出算法意图、具体分支和局部计数边界,不把它们合并成无条件安全保证。

27.6 固定材料下,从AuthID算到第一个Body块 ​

本例为独立计算的现代VMess请求,使用公开教学UUID00112233-4455-4677-8899-aabbccddeeff、目标example.com:443和应用内容hello。所有随机位置改为明确的构造值,便于复算;它不是真实抓包或官方测试向量,也不应作为部署秘密。

取请求Body Key为00到0f,Body IV为10到1f,响应字节V为aa,Opt为0d,Sec为03,头Padding P为0。AuthID时间取1700000000秒,随机4字节为01 02 03 04,connectionNonce取20到27。这个旧时间在当前真实服务端应被拒绝;本例只验证编码和密码计算,不声称它是可接受的当前连接。

CmdKey及AuthID计算结果如下:

text
CmdKey:8bc17118994a028971369f225ac29eed
AuthID明文:000000006553f10001020304b85575f7
AuthID AES密钥:14640ca879cfc19c8ed953de0c4ed45d
线上AuthID:fa278674547afb8f9d692442d43e70f1
connectionNonce:2021222324252627

AuthID末尾b8 55 75 f7是前12字节CRC32-IEEE的大端表示。指令明文按27.4节字段构成57字节,最后4字节为FNV-1a结果9c fb da 17:

text
01
10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f
00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f
aa 0d 03 00 01
01 bb 02 0b 65 78 61 6d 70 6c 65 2e 63 6f 6d
9c fb da 17

注意第三行是Body Key,第二行是Body IV,顺序与27.2节CmdKey输入完全不同。aa是本次响应关联V,不是某种固定魔数。0d开启分块、掩码与全局Padding,但头P仍为0。

专用KDF得到两次头部GCM参数:

用途Key16Nonce12
长度4722c865d25bf7a926ab86f71c293458d0ab82710b2723517c1cdaf6
指令af7b2ed08c4450b71ab902714d5f266049f7474b01ba1c479e5d9a72

长度明文为00 39,即57字节。两次AAD均为线上AuthID原始16字节。长度密文含Tag共18字节,指令密文含Tag共73字节:

text
长度密文:
5095dc7d4309898d85d4c33953effee74d38

指令密文:
1a95c77a7adca35b4fbdbf634c0f3c3243d5358540a9520a780e6a4d640190414d140f195e0055fe0f806eecf566ce0d598a97f9c96ee72ca7a30251cd6847ac3cf404c72b9a7b4e4d

按AuthID16、长度密文18、connectionNonce8、指令密文73连接,即得到完整115字节请求头。不要把connectionNonce放在AuthID之后、长度之前;密码输入相同也不代表网络字段可以任意换位。

接下来编码hello。SHAKE128以Body IV初始化,前4字节输出为fc 91 8e 43。先取fc91对64取模得Q=17,再取8e43作为长度掩码。负载AEAD的Nonce为000012131415161718191a1b,密钥为前面公开的Body Key16,AAD为空,得到21字节密文含Tag:

text
a316c4d586054e854b4a74fc9040de5bd5eb74d177

后续真实长度为hello5+Tag16+Padding17=38,即0026;0026异或8e43得到线上长度8e65。本例人为取Padding字节为00到10,符合其内容不由SHAKE决定、长度由状态决定的区别。完整Body块为:

text
8e65
 a316c4d586054e854b4a74fc9040de5bd5eb74d177
 000102030405060708090a0b0c0d0e0f10

三行依次长2、21、17,共40字节。与请求头合计155字节,不含结束标志、响应、TCP/IP或任何额外承载。更改Padding字节不会被这个负载Tag直接检查;更改密文或Tag则应使AEAD验证失败,改变长度还会影响解析路径。不能笼统说“这40字节全部由同一Tag认证”。

本书内部以Python构造CRC、FNV、嵌套HMAC、AES单块、SHAKE和GCM;另用Node独立实现嵌套HMAC,核对五个KDF输出与两块GCM头密文一致,并检查三处Tag改位拒绝。这里仍未编译运行Xray或完成两端网络互通;跨语言复算只支持所列构造的字节正确性,不覆盖时间窗口、重放并发和超长Body状态。

构造对应固定aead/authid.go、encrypt.go、encoding/client.go及auth.go。

27.7 VMess与VLESS:相同UUID表示,不同秘密用途 ​

把VMess节点改成VLESS节点时,只改协议名并保留其他参数并不能保证语义相同。VMess的UUID进入CmdKey派生,AuthID、指令头和Body材料据此形成;VLESS的基本请求直接表达用户标识、命令与目标,安全层另外选择。它们共享某些地址编码约定,却没有共享同一套认证报文。

维度本章现代VMess基础VLESS及其组合
用户标识上线方式经CmdKey构造AuthID,不直接发送明文UUID基础请求携带原始16字节UUID
请求头保密完整性专用派生及AES-GCM由所选TLS/REALITY/Encryption等保护
负载密钥随机Body材料装入加密指令头基础VLESS不另定义Body AEAD密钥
基础前向保密不提供取决于实际安全组合及其状态
初始头AuthID、认证长度、Nonce与受保护指令简短请求头及可选Addon
数据处理VMess负载分块、掩码、Padding及选项基础流或相应UDP结构;Vision可另改处理
主要互操作约束认证代际、KDF、Body方法与选项状态VLESS版本、命令/Addon、承载和安全组合

第27.6节的VMess目标example.com请求头为115字节;相同目标的VLESS无Addon头为34字节,带Vision Flow为52字节。这个比较只数各自明确的代理头。VMess已经在115字节中包含请求头保护及Body密钥传递,而VLESS的34或52没有包含TLS、REALITY或Encryption握手与记录开销;不能用两个数字相减就宣称某完整方案节省多少握手流量。

负载成本也如此。现代VMess普通块有长度、Tag和可变Padding,并持续进行Body AEAD;VLESS如果套TLS,同样有TLS记录开销;如果使用Vision并在满足条件后绕过某一层,处理路径又会变化。只有在相同应用数据、明确记录切分、相同承载和相同安全保证下,开销数字才可直接比较。省掉密码处理但同时失去所需安全性质,不能算作同等条件下的性能优势。

长期秘密泄漏的后果要分别追踪。基础VMess的UUID足以导出历史CmdKey并恢复截获头中的Body材料;实际VLESS+临时交换TLS或REALITY,UUID泄漏并不直接给出过去TLS临时秘密。VLESS Encryption又有自己的静态材料、缓存PFS状态及Vision绕过边界,不能把它们一概归成“UUID相同所以都一样”。

反过来,VLESS UUID若在无保护链路直接发送,其用户标识和目标会暴露,而VMess头并非这样明文发送。比较应覆盖真实配置。把未保护的基础格式与另一方完整加密格式相对照,再给整个协议家族排名,会掩盖实际威胁模型。

二者都可能借助RAW、WebSocket或其他实现支持的承载。承载负责把代理字节送到对端,不自动理解VMess指令或VLESS用户。HTTP CDN若终结外层TLS,只能直接得到承载内的代理字节;能否进一步读懂受保护头,取决于是否掌握相应秘密。添加承载也不会消除代理服务器对最终目标的可见性。

VMess把更多会话与密码组织放在协议本身,VLESS把身份命令保持较小并与安全层组合。这是设计职责差别,不是“功能多必然更安全”或“代码少必然更快”的证明。选择时应先明确需要的安全终点、PFS、承载与客户端实现能力,再对具体路径分析。

VMess头与负载见固定encoding/client.go,VLESS基础头见encoding.go,Vision层次变化见proxy.go。

27.8 状态与版本:可接受的头还要接上相同的数据处理 ​

“客户端无需先等服务器握手”只说明VMess可以直接发起受保护请求,并不意味着服务器或连接没有历史。固定实现至少维护三类状态:按账号解AuthID的材料与去重集合、按用户和Body材料记录的SessionHistory,以及每个方向持续推进的负载Nonce、SHAKE和缓冲。

SessionHistory键由用户UUID16、请求Body Key16、请求Body IV16组成。插入时设置当前时间后3分钟到期;周期任务每30秒移除已过期项,重复判断本身也检查保存的到期时间。因此“最多每30秒清理一次”和“旧会话在到期后还必然继续被拒绝30秒”不是相同结论。它与AuthID的120秒时间检查及双代MapFilter分别作用于不同对象。

该会话记录在读取指令固定38字节、取得Body材料后加入,早于后续全部目标、Padding和FNV检查完成。于是缓存中的存在记录不能当作成功建立了目标连接的证明。反过来,替换一个新的AuthID也不一定绕过重复Body材料检查;两套判定不能互相省略。

这些集合通常属于相应处理器的内存。重启会改变状态存续,多个入口实例是否共享账号和历史也须另查。时间检查降低过旧报文的接受范围,有限集合约束重复,但本章没有证明所有故障、重启、并发及集群条件下都能实现业务级唯一执行。重发一段已发送的应用数据,还可能在目标协议中产生重复副作用,与代理入口是否重复又是另一层问题。

Body状态要求双方逐次对齐。丢失一个TCP段由TCP补齐,不能由VMess接收端跳过缺口继续读下一个掩码;错误消费一次SHAKE输出,会使后续Padding和长度同时错位。数据块读取可以分很多次Read,内部缓冲保留不完整结构;一次读取超时也不意味着可以把当前Nonce计数清零重新开始。

版本还影响配置含义。固定VMess配置Build只显式列出aes-128-gcm、chacha20-poly1305和auto,其他字符串落入AUTO分支。故仅因输入JSON写了一个历史方法名,不能认定运行时确实采用它,更不能根据一个none字符串就断言此版本Body不加密。应检查构建后的账号Security及实际编码分支;所列Body编码器只对其支持的AEAD路径返回有效处理器。

请求头Sec、Opt及高位P已经决定接收端下一步如何处理。对端须支持相同认证代际、准确KDF标签及二进制上下文、地址值、Body算法和长度/Padding语义。AuthenticatedLength是特定实验路径,不应因为对端都叫VMess就假定支持;NoTerminationSignal也会改变正常结束时发送的结构。这些选项无法由一个通用的“已导入节点”提示证明互通。

阅读源码时还要辨别残留实现与实际路径。所列现代响应函数里仍能看到某些AES流对象的构造,但响应头实际写入另一个缓冲并经AEAD输出,Body又交给对应编码器;不能搜到CFB或MD5字样就宣布整条现代路径仍使用旧指令认证,也不能只看函数注释忽略实际Writer替换。

本章对长连接Nonce回绕、结束标志验证时机等局部边界采用同一原则:把源码事实写出,说明它限制了什么结论,保留没有运行验证的范围。随机外观、更多校验字段或“AEAD”标签,都不能替代对密钥唯一性、状态同步和错误行为的逐项分析。接下来的Trojan把安全承载直接交给TLS,代理头反而更短,其认证凭据与回退边界又有所不同。

SessionHistory及现代解析顺序见固定encoding/server.go,账号构建见infra/conf/vmess.go,选项与发送结束路径见outbound.go。

从字节与状态机出发,理解每一层的职责。