VMess 协议
位置:核心协议层 | 难度:较难 | 前置:密码学基础、TLS 1.3 深入源码证据:
proxy/vmess/aead/encrypt.go(136行)、aead/authid.go(122行)、aead/kdf.go(34行)、encoding/client.go(341行) 外部参考:VMess 官方规范 (xtls.github.io)、GFW Report "V2Ray Weaknesses"
1. 协议定位
VMess 是 V2Ray 项目 2016 年设计的加密代理协议,Xray-core 继承并持续维护。与 VLESS 最本质的区别:VMess 自带完整的应用层加密和认证体系,不依赖外部 TLS。
这个设计在 2016 年是合理的——当时 TLS 1.3 尚未普及,代理工具需要在应用层提供自己的安全保证。但到 2022 年后,这个设计变成了弱点:应用层加密 + 外层 TLS = TLS-in-TLS 双层加密,被 GFW 的包大小和熵分析检测。据社区估算,VMess 在 GFW 中的检测率到 2025 年 9 月已超过 80%。
VMess 仍然重要的原因:作为 V2Ray 生态的原生协议,大量存量部署依赖它,理解其实现是理解 VLESS 为何简化的前提。
2. 整体流程
3. AEAD 认证头完整格式
VMess 客户端在 TCP 连接建立后发送的第一个数据块即为认证头。这是 GFW 检测 VMess 流量的主要依据——16 字节固定长度 AuthID + 18 字节固定长度 ALength,总是一个特定的包大小模式。
字节范围 长度 字段 说明
[0-15] 16B AuthID AES 加密的 (时间戳 + 随机数 + CRC32)
[16-33] 18B ALength AEAD 加密的 2B 载荷长度 + 16B GCM Tag
[34-41] 8B Nonce 随机连接 Nonce(每次连接不同)
[42-..] Y+16B AHeader AEAD 加密的指令部分 + 16B GCM Tag
[..] .. Data 根据 Security 类型加密的载荷数据3.1 为什么这个格式可被检测
USENIX Security '23 论文(Wu et al.)揭示了 GFW 的全加密流量检测算法(Algorithm 1):
Ex1(熵检测):popcount(pkt) / len(pkt) ≤ 3.4 或 ≥ 4.6 → 豁免
VMess AuthID 16B 是 AES 密文 → popcount/len ≈ 4.0
→ 落入 3.4-4.6 范围 → 不被豁免 → 被阻断VMess 的 AEAD 加密头不具备 TLS ClientHello 的 ASCII 明文特征(SNI、ALPN 等),因此 GFW 的 Ex2-Ex5 都不适用。这使 VMess 在没有外层 TLS 伪装的情况下极易被识别。
4. AuthID 创建与验证
4.1 CreateAuthID
源码位置:proxy/vmess/aead/authid.go 行 26-40
func CreateAuthID(cmdKey []byte, time int64) [16]byte {
buf := bytes.NewBuffer(nil)
common.Must(binary.Write(buf, binary.BigEndian, time)) // 8 bytes: Unix 时间戳
var zero uint32
common.Must2(io.CopyN(buf, rand3.Reader, 4)) // 4 bytes: 随机数
zero = crc32.ChecksumIEEE(buf.Bytes()) // 4 bytes: CRC32 校验值
common.Must(binary.Write(buf, binary.BigEndian, zero))
aesBlock := NewCipherFromKey(cmdKey) // KDF 派生的 AES 密钥
if buf.Len() != 16 {
panic("Size unexpected")
}
var result [16]byte
aesBlock.Encrypt(result[:], buf.Bytes()) // AES 加密整个 16B 块
return result
}逐阶段分析:
阶段 1:构造 16 字节明文块
[0-7] Unix 时间戳 (int64, BigEndian)
[8-11] 4 字节随机数
[12-15] CRC32-IEEE([0-11] 的 12 字节)CRC32 的作用是快速过滤无效 AuthID——服务端解密后先检查 CRC32,绝大多数随机连接在这一步就被过滤掉了,不需要做昂贵的时间戳比较。
CRC32 不是安全原语——攻击者可以构造碰撞——但它在这里只做初筛,真正的安全由 AES 加密和时间戳窗口保证。
阶段 2:AES 加密
密钥由 cmdKey 通过 KDF 派生:
func NewCipherFromKey(cmdKey []byte) cipher.Block {
aesBlock, err := aes.NewCipher(KDF16(cmdKey, KDFSaltConstAuthIDEncryptionKey))
// KDFSaltConstAuthIDEncryptionKey = "AES Auth ID Encryption"
}加密整个 16 字节块。输出是看起来完全随机的 16 字节——这是 GFW 熵检测(Algorithm 1 Ex1)将 VMess 识别为"全加密流量"的原因。
4.2 AuthID 解码与用户匹配
源码位置:proxy/vmess/aead/authid.go 行 58-68 + 行 99-121
func (aidd *AuthIDDecoder) Decode(data [16]byte) (int64, uint32, int32, []byte) {
aidd.s.Decrypt(data[:], data[:]) // AES 解密(Decrypt 因为是 ECB 模式等价于 Encrypt 的逆)
var t int64
var zero uint32
var rand int32
reader := bytes.NewReader(data[:])
common.Must(binary.Read(reader, binary.BigEndian, &t)) // 读取时间戳
common.Must(binary.Read(reader, binary.BigEndian, &rand)) // 读取随机数
common.Must(binary.Read(reader, binary.BigEndian, &zero)) // 读取 CRC32
return t, zero, rand, data[:]
}服务端的 AuthIDDecoderHolder.Match() 对每个已注册用户尝试解密 AuthID:
func (a *AuthIDDecoderHolder) Match(authID [16]byte) (interface{}, error) {
for _, v := range a.decoders {
t, z, _, d := v.dec.Decode(authID)
if z != crc32.ChecksumIEEE(d[:12]) {
continue // CRC32 不匹配 → 跳过此用户
}
if t < 0 {
return nil, ErrNeagtiveTime
}
if math.Abs(math.Abs(float64(t))-float64(time.Now().Unix())) > 120 {
return nil, ErrInvalidTime // 时间戳超出 ±120 秒窗口
}
if !a.filter.Check(authID) {
return nil, ErrReplay // 重复的 AuthID → 重放攻击
}
return v.ticket, nil // 匹配成功
}
return nil, ErrNotFound
}GFW 对 AuthID 的已知攻击(来源:GFW Report "V2Ray Weaknesses"):
- 时间戳重放:攻击者录制一个合法 AuthID,在 60-120 秒窗口内重放。如果服务器返回"载荷长度解密失败"而非"用户不存在",则行为差异确认了 VMess 服务。
- CRC32 碰撞:理论上可以构造不同的时间戳产生相同 CRC32,但受限于 AES 加密的混淆。
5. KDF 密钥派生链
源码位置:proxy/vmess/aead/kdf.go 行 13-33
VMess 从用户 UUID 派生出一棵密钥树,每个用途使用独立的派生密钥。这通过 HMAC-SHA256 的级联实现。
func KDF(key []byte, path ...string) []byte {
hmacf := hmac.New(sha256.New, []byte(KDFSaltConstVMessAEADKDF))
// KDFSaltConstVMessAEADKDF = "VMess AEAD KDF"
for _, v := range path {
first := true
hmacf = hmac.New(func() hash.Hash {
if first {
first = false
return hash2{hmacf} // 首次迭代使用外层 HMAC 的内部状态
}
return hmacf
}, []byte(v))
}
hmacf.Write(key)
return hmacf.Sum(nil)
}
func KDF16(key []byte, path ...string) []byte {
r := KDF(key, path...)
return r[:16]
}密钥树完整结构:
用户 UUID → cmdKey (16 bytes, UUID 的 CmdKey() 方法提取)
│
├── KDF16(cmdKey, "AES Auth ID Encryption")
│ → AuthID 加密密钥(用于 CreateAuthID 中的 AES 加密)
│
├── KDF16(cmdKey, "VMess Header AEAD Key_Length", authID, nonce)
│ → 载荷长度 AEAD 加密密钥
│
├── KDF(cmdKey, "VMess Header AEAD Nonce_Length", authID, nonce)[:12]
│ → 载荷长度 AEAD Nonce (12 bytes)
│
├── KDF16(cmdKey, "VMess Header AEAD Key", authID, nonce)
│ → 载荷 AEAD 加密密钥
│
├── KDF(cmdKey, "VMess Header AEAD Nonce", authID, nonce)[:12]
│ → 载荷 AEAD Nonce (12 bytes)
│
├── KDF16(cmdKey, "AEAD Resp Header Len Key")
│ → 响应长度 AEAD 加密密钥
│
├── KDF(cmdKey, "AEAD Resp Header Len IV")[:12]
│ → 响应长度 AEAD IV
│
├── KDF16(cmdKey, "AEAD Resp Header Key")
│ → 响应载荷 AEAD 加密密钥
│
└── KDF(cmdKey, "AEAD Resp Header IV")[:12]
→ 响应载荷 AEAD IV所有盐值常量定义在 proxy/vmess/aead/consts.go 行 4-14:
const (
KDFSaltConstAuthIDEncryptionKey = "AES Auth ID Encryption"
KDFSaltConstAEADRespHeaderLenKey = "AEAD Resp Header Len Key"
KDFSaltConstAEADRespHeaderLenIV = "AEAD Resp Header Len IV"
KDFSaltConstAEADRespHeaderPayloadKey = "AEAD Resp Header Key"
KDFSaltConstAEADRespHeaderPayloadIV = "AEAD Resp Header IV"
KDFSaltConstVMessAEADKDF = "VMess AEAD KDF"
KDFSaltConstVMessHeaderPayloadAEADKey = "VMess Header AEAD Key"
KDFSaltConstVMessHeaderPayloadAEADIV = "VMess Header AEAD Nonce"
KDFSaltConstVMessHeaderPayloadLengthAEADKey = "VMess Header AEAD Key_Length"
KDFSaltConstVMessHeaderPayloadLengthAEADIV = "VMess Header AEAD Nonce_Length"
)这些字符串常量实现域分离(Domain Separation):即使攻击者知道了某个派生密钥,也无法推导出其他用途的密钥——因为 KDF 的级联路径不同。
KDF 的 HMAC 级联设计:
// KDF(key, "A", "B", "C") 的计算过程:
// HMAC("VMess AEAD KDF", key) → h1
// HMAC(h1.inner, "A") → h2
// HMAC(h2, "B") → h3
// HMAC(h3, "C") → result每个字符串作为一个额外的 HMAC 输入层,使路径不可逆。
6. 双层 AEAD 加密头
源码位置:proxy/vmess/aead/encrypt.go 行 14-61
这是 VMess 协议最复杂的部分——对指令部分进行双层 AEAD 加密:外层加密载荷长度,内层加密载荷内容。
func SealVMessAEADHeader(key [16]byte, data []byte) []byte {
generatedAuthID := CreateAuthID(key[:], time.Now().Unix())
connectionNonce := make([]byte, 8)
io.ReadFull(rand.Reader, connectionNonce)
// ===== 第一层 AEAD:加密载荷长度 =====
headerPayloadDataLen := uint16(len(data)) // 载荷的字节长度
// 序列化为 2 字节 BigEndian
binary.Write(buf, binary.BigEndian, headerPayloadDataLen)
payloadHeaderLengthAEADKey := KDF16(key[:],
KDFSaltConstVMessHeaderPayloadLengthAEADKey, // "VMess Header AEAD Key_Length"
string(generatedAuthID[:]), string(connectionNonce))
payloadHeaderLengthAEADNonce := KDF(key[:],
KDFSaltConstVMessHeaderPayloadLengthAEADIV, // "VMess Header AEAD Nonce_Length"
string(generatedAuthID[:]), string(connectionNonce))[:12]
payloadHeaderAEAD := crypto.NewAesGcm(payloadHeaderLengthAEADKey)
payloadHeaderLengthAEADEncrypted = payloadHeaderAEAD.Seal(nil,
payloadHeaderLengthAEADNonce,
aeadPayloadLengthSerializedByte, // 明文:2B 长度
generatedAuthID[:]) // AD:AuthID
// ===== 第二层 AEAD:加密指令部分 =====
payloadHeaderAEADKey := KDF16(key[:],
KDFSaltConstVMessHeaderPayloadAEADKey, // "VMess Header AEAD Key"
string(generatedAuthID[:]), string(connectionNonce))
payloadHeaderAEADNonce := KDF(key[:],
KDFSaltConstVMessHeaderPayloadAEADIV, // "VMess Header AEAD Nonce"
string(generatedAuthID[:]), string(connectionNonce))[:12]
payloadHeaderAEAD := crypto.NewAesGcm(payloadHeaderAEADKey)
payloadHeaderAEADEncrypted = payloadHeaderAEAD.Seal(nil,
payloadHeaderAEADNonce,
data, // 明文:指令部分
generatedAuthID[:]) // AD:AuthID
// ===== 组装输出 =====
output := [AuthID 16B] [ALength 18B] [Nonce 8B] [AHeader Y+16B]
}为什么需要双层 AEAD:
在 VMess 的原始设计中,服务端需要先知道载荷长度才能分配正确的接收缓冲区。如果载荷长度本身不加密,审查方可以从长度推断协议类型。
双层设计使审查方既看不到载荷长度(第一层加密),也看不到载荷内容(第二层加密)。
双层 AEAD 与 GFW 检测的关系:
双层加密进一步增加了首个数据包的随机性——ALength(18B)和 AHeader 的开头都是 AEAD 密文,具有高熵特征。这使 VMess 流量在 GFW Algorithm 1 的 Ex1(熵检测)中几乎不可能被豁免。
7. 指令部分格式
源码位置:proxy/vmess/encoding/client.go 行 63-102
指令部分在被 AEAD 加密之前的明文结构:
偏移 长度 字段 说明
[0] 1B Version 始终为 0x01
[1] 16B Request Body IV 随机生成的请求体加密 IV
[17] 16B Request Body Key 随机生成的请求体加密密钥
[33] 1B Response Header 随机响应认证字节
[34] 1B Options 选项位掩码
[35] 1B Security+Padding 高 4 位=填充长度, 低 4 位=加密方式
[36] 1B Reserved 保留 (0x00)
[37] 1B Command 0x01=TCP, 0x02=UDP, 0x03=Mux
[38] 2B Port 目标端口 (BigEndian)
[40] 1B Address Type 0x01=IPv4, 0x02=域名, 0x03=IPv6
[41] 变长 Address 目标地址
[..] 0-15B Padding 随机填充 (长度由 Security 高 4 位指定)
[..] 4B FNV1a Checksum 指令明文的 FNV1a 哈希Options 位掩码:
Bit 0 (S): Stream 分块
Bit 1 (R): 不清楚 (文档未定义)
Bit 2 (M): Chunk Masking
Bit 3 (X): Global Padding
Bit 4-7: 保留Security 加密方式(低 4 位):
0x01= AES-128-GCM0x02= ChaCha20-Poly13050x03= None(无加密,仅用于测试)
FNV1a Checksum 位置:
fnv1a := fnv.New32a()
fnv1a.Write(buffer.Bytes()) // 哈希所有指令明文字节(除 checksum 自身)
hashBytes := buffer.Extend(fnv1a.Size()) // 在 buffer 末尾扩展 4 字节
fnv1a.Sum(hashBytes[:0]) // 将哈希值写入扩展位置FNV1a 不是安全 MAC——它不提供防篡改保护。真正的完整性由外层 AEAD 保证。FNV1a 的功能是在 AEAD 解密成功后做一次快速的完整性确认。
8. 请求体加密
指令部分之后的数据(请求体)使用在指令部分中协商的密钥进行加密。
源码位置:proxy/vmess/encoding/client.go 行 104-177
func (c *ClientSession) EncodeRequestBody(request *protocol.RequestHeader, writer io.Writer) (buf.Writer, error) {
switch request.Security {
case protocol.SecurityType_AES128_GCM:
aead := crypto.NewAesGcm(c.requestBodyKey[:]) // 16B 密钥来自指令部分
auth := &crypto.AEADAuthenticator{
AEAD: aead,
NonceGenerator: GenerateChunkNonce(c.requestBodyIV[:], uint32(aead.NonceSize())),
// Nonce = requestBodyIV + 递增计数器 → 每个 Chunk 的 Nonce 不同
}
// ...
case protocol.SecurityType_CHACHA20_POLY1305:
aead, _ := chacha20poly1305.New(GenerateChacha20Poly1305Key(c.requestBodyKey[:]))
// ...
case protocol.SecurityType_NONE:
// 无加密,直接透传
}
}每个 Chunk 使用独立的 Nonce(通过计数器递增),确保即使相同明文也不会产生相同密文。
Session 密钥的生成(NewClientSession,行 38-61):
func NewClientSession(ctx context.Context, behaviorSeed int64) *ClientSession {
randomBytes := make([]byte, 33) // 16(IV) + 16(Key) + 1(ResponseHeader)
rand.Read(randomBytes)
copy(session.requestBodyKey[:], randomBytes[:16])
copy(session.requestBodyIV[:], randomBytes[16:32])
session.responseHeader = randomBytes[32]
// 响应密钥从请求密钥派生:
// ResponseBodyKey = SHA256(RequestBodyKey)[:16]
// ResponseBodyIV = SHA256(RequestBodyIV)[:16]
BodyKey := sha256.Sum256(session.requestBodyKey[:])
copy(session.responseBodyKey[:], BodyKey[:16])
BodyIV := sha256.Sum256(session.requestBodyIV[:])
copy(session.responseBodyIV[:], BodyIV[:16])
}每次连接生成全新的随机密钥对,保证前向安全性。
9. 响应格式
服务端返回的响应头使用从请求密钥派生的密钥进行 AEAD 加密。
源码位置:proxy/vmess/encoding/client.go 行 179-253
服务端响应:
[加密的响应长度: 2B 长度 + 16B Tag] → 18 bytes AEAD
[加密的响应内容: Y bytes + 16B Tag]解密后:
[Response Auth: 1B] — 必须匹配客户端发送的 responseHeader
[Options: 1B] — 响应选项
[Command ID: 1B] — 如果有命令
[Command Data Length: 1B]
[Command Data: 变长]Response Auth 字节的验证是防止响应伪造的关键:
if buffer.Byte(0) != c.responseHeader {
return nil, errors.New("unexpected response header. Expecting ",
int(c.responseHeader), " but actually ", int(buffer.Byte(0)))
}10. GFW 攻击面总结
基于 GFW Report 的公开分析报告和 USENIX Security '23 论文,VMess 在 GFW 面前的主要弱点:
| 攻击手段 | 论文/报告来源 | VMess 受影响原因 |
|---|---|---|
| 被动熵检测 | USENIX '23 Algorithm 1 Ex1 | AuthID 16B 是 AES 密文,popcount/len ≈ 4.0 |
| 被动包长度检测 | USENIX '23 第 8 节 | 16+18+8 = 42B 固定前缀长度是 VMess 独有的特征 |
| AuthID 重放攻击 | GFW Report "V2Ray Weaknesses" | 60-120s 时间窗口内重放有效 AuthID 可触发服务器行为差异 |
| Padding 字段篡改 | GFW Report "V2Ray Weaknesses" | 修改 Margin P 使服务器读取异常长度 |
| TLS-in-TLS 检测 | nthLink White Paper | VMess+TLS 组合产生双层 TLS 嵌套特征 |
| 主动探测 | GFW Report IMC '20 | 7 种针对 VMess 的主动探测类型 |
**VMess 单独使用(无 TLS 外层)**面临的 GFW 检测路径:
这解释了为什么 VMess 在 2021 年 GFW 部署被动检测后的可用性急剧下降。
11. 源码文件索引
| 文件 | 核心内容 | 行数 |
|---|---|---|
proxy/vmess/vmess.go | 协议注册、Account 类型定义 | 50+ |
proxy/vmess/aead/consts.go | KDF 盐值常量 | 14 |
proxy/vmess/aead/kdf.go | KDF 密钥派生(HMAC 级联) | 34 |
proxy/vmess/aead/authid.go | AuthID 创建/解码/防重放 | 122 |
proxy/vmess/aead/encrypt.go | 双层 AEAD 加密/解密头 | 136 |
proxy/vmess/encoding/encoding.go | 协议版本、地址解析器 | 18 |
proxy/vmess/encoding/client.go | 客户端:请求编码、响应解码、Chunk 加密 | 341 |
proxy/vmess/encoding/server.go | 服务端:对称的解码和编码 | ~250 |
proxy/vmess/encoding/commands.go | 命令编解码 | ~50 |
proxy/vmess/validator.go | 用户管理和 AuthID 匹配入口 | ~100 |
proxy/vmess/inbound/inbound.go | 入站处理器:接收连接 → 验证 → Dispatcher | ~200 |
proxy/vmess/outbound/outbound.go | 出站处理器:建立连接 → 发送请求 → 中继 | ~200 |
12. 外部参考资料
资料:VMess Protocol — Project X 官方协议规范 类型:协议规范 链接:https://xtls.github.io/en/development/protocols/vmess.html 可信度:A 建议参考:AEAD 认证格式定义和指令部分字段说明
资料:Summary on Recently Discovered V2Ray Weaknesses 类型:技术报告(GFW Report) 链接:https://gfw.report/blog/v2ray_weaknesses/en 可信度:A 建议参考:AuthID 重放攻击和 Padding 篡改攻击的详细分析
资料:How the Great Firewall of China Detects and Blocks Fully Encrypted Traffic 类型:顶会论文(USENIX Security '23, Wu et al.) 链接:https://www.usenix.org/conference/usenixsecurity23/presentation/wu-mingshi 可信度:A 建议参考:Algorithm 1 的 5 条豁免规则,理解 GFW 如何检测 VMess
资料:nthLink White Paper: Are Shadowsocks and Trojan-go Still Relevant 类型:技术白皮书(Open Tech Fund) 链接:https://www.opentech.fund/wp-content/uploads/2023/11/nthLink_whitepaper_are_shadowshocks_and_trojan-go_still_relevant.pdf 可信度:B 建议参考:VMess 在 2022-2023 年的 GFW 阻断情况统计
读者自检
- VMess AuthID 的 16 字节是如何构造的(时间戳 + 随机数 + CRC32,AES 加密)
- 为什么 GFW Algorithm 1 的 Ex1(熵检测)能识别 VMess 流量
- 双层 AEAD 分别加密什么、为什么需要两层
- KDF 的 HMAC 级联设计如何实现域分离
- VMess 的 AuthID 重放窗口是多少秒