Skip to content

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

go
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 派生:

go
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

go
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:

go
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"):

  1. 时间戳重放:攻击者录制一个合法 AuthID,在 60-120 秒窗口内重放。如果服务器返回"载荷长度解密失败"而非"用户不存在",则行为差异确认了 VMess 服务。
  2. CRC32 碰撞:理论上可以构造不同的时间戳产生相同 CRC32,但受限于 AES 加密的混淆。

5. KDF 密钥派生链

源码位置:proxy/vmess/aead/kdf.go 行 13-33

VMess 从用户 UUID 派生出一棵密钥树,每个用途使用独立的派生密钥。这通过 HMAC-SHA256 的级联实现。

go
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:

go
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 级联设计

go
// 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 加密:外层加密载荷长度,内层加密载荷内容。

go
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-GCM
  • 0x02 = ChaCha20-Poly1305
  • 0x03 = None(无加密,仅用于测试)

FNV1a Checksum 位置:

go
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

go
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):

go
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 字节的验证是防止响应伪造的关键:

go
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 Ex1AuthID 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 PaperVMess+TLS 组合产生双层 TLS 嵌套特征
主动探测GFW Report IMC '207 种针对 VMess 的主动探测类型

**VMess 单独使用(无 TLS 外层)**面临的 GFW 检测路径:

这解释了为什么 VMess 在 2021 年 GFW 部署被动检测后的可用性急剧下降。


11. 源码文件索引

文件核心内容行数
proxy/vmess/vmess.go协议注册、Account 类型定义50+
proxy/vmess/aead/consts.goKDF 盐值常量14
proxy/vmess/aead/kdf.goKDF 密钥派生(HMAC 级联)34
proxy/vmess/aead/authid.goAuthID 创建/解码/防重放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 重放窗口是多少秒