Skip to content

VMess 协议

VMess 是 V2Ray/Xray 原始的加密代理协议。它提供多种加密选项的认证加密、基于时间的防重放保护,以及可选的填充/掩码以抵抗流量分析。

概述

  • 方向:入站 + 出站
  • 传输:TCP、UNIX 套接字
  • 加密:AES-128-GCM、ChaCha20-Poly1305、无
  • 认证:基于 UUID 的 AEAD 头部加密
  • 多路复用:通过 v1.mux.cool 虚拟域名和 XUDP 支持

线路格式

请求(客户端到服务端)

VMess AEAD 请求由两层组成:外层 AEAD 加密的头部信封,以及内层命令头。

外层信封(AEAD 头部)

+----------+---------------------------+--------------+---------------------------+
| Auth ID  | Encrypted Payload Length  | Conn Nonce   | Encrypted Payload         |
| 16 bytes | 2 + 16 bytes (GCM tag)    | 8 bytes      | variable + 16 bytes (tag) |
+----------+---------------------------+--------------+---------------------------+

源码:proxy/vmess/aead/encrypt.go:14-61

Auth ID(16 字节):AES-ECB 加密的数据块,包含:

+---------------+-----------+----------+
| Timestamp     | Random    | CRC32    |
| 8 bytes (BE)  | 4 bytes   | 4 bytes  |
+---------------+-----------+----------+

Auth ID 加密的 AES 密钥派生方式:

go
aesKey = KDF16(cmdKey, "AES Auth ID Encryption")

源码:proxy/vmess/aead/authid.go:26-40

时间验证:服务端对每个已知用户解密 Auth ID,校验 CRC32,并验证时间戳与服务器时间的偏差在 120 秒以内。

源码:proxy/vmess/aead/authid.go:99-121

载荷长度加密:AES-128-GCM,密钥/随机数派生自:

go
key   = KDF16(cmdKey, "VMess Header AEAD Key_Length", authID, connNonce)
nonce = KDF(cmdKey, "VMess Header AEAD Nonce_Length", authID, connNonce)[:12]
// Additional data = authID

载荷加密:AES-128-GCM,密钥/随机数派生自:

go
key   = KDF16(cmdKey, "VMess Header AEAD Key", authID, connNonce)
nonce = KDF(cmdKey, "VMess Header AEAD Nonce", authID, connNonce)[:12]
// Additional data = authID

源码:proxy/vmess/aead/encrypt.go:30-51

内层命令头(解密后的载荷)

+-----+--------+--------+---------+--------+----------+-----+---------+---------+-------+
| Ver | BodyIV | BodyKey | RespHdr | Option | Security | Rsv | Command | Address | Pad   | FNV1a |
| 1B  | 16B    | 16B    | 1B      | 1B     | 1B       | 1B  | 1B      | var     | 0-15B | 4B    |
+-----+--------+--------+---------+--------+----------+-----+---------+---------+-------+

源码:proxy/vmess/encoding/client.go:63-101

字段大小描述
Version1 字节始终为 0x01
Body IV16 字节用于消息体加密的随机 IV
Body Key16 字节用于消息体加密的随机密钥
Response Header1 字节随机字节,服务端必须回显
Option1 字节位掩码:ChunkStream(0x01)、ChunkMasking(0x04)、GlobalPadding(0x08)、AuthenticatedLength(0x10)
Security1 字节高 4 位 = 填充长度(0-15),低 4 位 = 加密类型
Reserved1 字节始终为 0x00
Command1 字节0x01=TCP、0x02=UDP、0x03=Mux
Address可变长Port(2B, BE) + AddrType(1B) + Address
Padding0-15 字节随机填充
FNV1a4 字节前述所有字段的 FNV-1a 散列(完整性校验)

加密类型(低 4 位):

加密方式
0x00AUTO / UNKNOWN
0x03AES-128-GCM
0x04ChaCha20-Poly1305
0x05

源码:proxy/vmess/encoding/server.go:114-124

响应(服务端到客户端)

响应头同样使用 AEAD 加密:

+----------------------------+----------------------------+
| Encrypted Length (2+16B)   | Encrypted Header (var+16B) |
+----------------------------+----------------------------+

密钥从响应体密钥/IV 派生:

go
responseBodyKey = SHA256(requestBodyKey)[:16]
responseBodyIV  = SHA256(requestBodyIV)[:16]

lengthKey   = KDF16(responseBodyKey, "AEAD Resp Header Len Key")
lengthNonce = KDF(responseBodyIV, "AEAD Resp Header Len IV")[:12]
payloadKey  = KDF16(responseBodyKey, "AEAD Resp Header Key")
payloadNonce = KDF(responseBodyIV, "AEAD Resp Header IV")[:12]

源码:proxy/vmess/encoding/client.go:179-253proxy/vmess/encoding/server.go:328-369

解密后的响应头:

+----------+--------+---------+---------+
| RespHdr  | Option | CmdID   | CmdLen  | CmdData |
| 1B       | 1B     | 1B      | 1B      | var     |
+----------+--------+---------+---------+

RespHdr 字节必须与请求中的随机字节匹配。

消息体加密

消息体数据以认证分块方式传输:

mermaid
graph LR
    subgraph "每个分块"
        A[长度 2B] --> B[载荷] --> C[认证标签]
    end

分块大小编码(带掩码): 当启用 ChunkMasking 时,SHAKE128 流对 2 字节长度字段进行异或:

go
// ShakeSizeParser
mask = SHAKE128(bodyIV).next_2_bytes()
wire_length = actual_length XOR mask

源码:proxy/vmess/encoding/auth.go:51-87

消息体 AEAD 随机数生成

go
func GenerateChunkNonce(nonce []byte, size uint32) BytesGenerator {
    c := copy(nonce)
    count := uint16(0)
    return func() []byte {
        binary.BigEndian.PutUint16(c, count)
        count++
        return c[:size]
    }
}

随机数基于消息体 IV,前 2 字节被递增计数器替换。

源码:proxy/vmess/encoding/client.go:332-340

ChaCha20-Poly1305 密钥派生:16 字节的消息体密钥扩展为 32 字节:

go
func GenerateChacha20Poly1305Key(b []byte) []byte {
    key := make([]byte, 32)
    t := md5.Sum(b)
    copy(key, t[:])
    t = md5.Sum(key[:16])
    copy(key[16:], t[:])
    return key
}

源码:proxy/vmess/encoding/auth.go:42-49

终止信号:当未设置 NoTerminationSignal 时,空分块(长度=0)标志着流的结束。

源码:proxy/vmess/outbound/outbound.go:185-189

KDF(密钥派生函数)

VMess 使用嵌套的 HMAC-SHA256 KDF:

go
func KDF(key []byte, path ...string) []byte {
    hmacf := hmac.New(sha256.New, []byte("VMess AEAD KDF"))
    for _, v := range path {
        hmacf = hmac.New(func() hash.Hash {
            // uses previous hmac as inner hash
            return hmacf
        }, []byte(v))
    }
    hmacf.Write(key)
    return hmacf.Sum(nil)
}

源码:proxy/vmess/aead/kdf.go:13-28

KDF 盐值常量

常量
KDFSaltConstVMessAEADKDF"VMess AEAD KDF"
KDFSaltConstAuthIDEncryptionKey"AES Auth ID Encryption"
KDFSaltConstVMessHeaderPayloadAEADKey"VMess Header AEAD Key"
KDFSaltConstVMessHeaderPayloadAEADIV"VMess Header AEAD Nonce"
KDFSaltConstVMessHeaderPayloadLengthAEADKey"VMess Header AEAD Key_Length"
KDFSaltConstVMessHeaderPayloadLengthAEADIV"VMess Header AEAD Nonce_Length"
KDFSaltConstAEADRespHeaderLenKey"AEAD Resp Header Len Key"
KDFSaltConstAEADRespHeaderLenIV"AEAD Resp Header Len IV"
KDFSaltConstAEADRespHeaderPayloadKey"AEAD Resp Header Key"
KDFSaltConstAEADRespHeaderPayloadIV"AEAD Resp Header IV"

源码:proxy/vmess/aead/consts.go:1-14

用户认证

CmdKey

CmdKey 从用户的 UUID 派生:

go
func NewID(uuid uuid.UUID) *ID {
    // cmdKey = MD5(uuid.Bytes() + []byte("c48619fe-8f02-49e0-b9e9-edf763e17e21"))
}

源码:common/protocol/id.go

TimedUserValidator

服务端维护一个 TimedUserValidator,它:

  1. 将所有用户的 CmdKey 存储在 AuthIDDecoderHolder
  2. 对每个传入连接,尝试用每个用户的密钥进行 AES 解密 16 字节的 Auth ID
  3. 验证 CRC32 校验和、时间戳范围(±120 秒)和重放过滤器
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 }
        if math.Abs(float64(t) - float64(time.Now().Unix())) > 120 { return nil, ErrInvalidTime }
        if !a.filter.Check(authID) { return nil, ErrReplay }
        return v.ticket, nil
    }
    return nil, ErrNotFound
}

源码:proxy/vmess/aead/authid.go:99-121

行为种子

基于所有用户 ID 使用 HMAC-SHA256 + CRC64 计算出确定性的"行为种子"。该种子控制排空模式(在关闭无效连接前读取随机长度的数据),以防止探测攻击。

源码:proxy/vmess/validator.go:114-123

入站处理器

文件:proxy/vmess/inbound/inbound.go

入站处理器的工作流程:

  1. 根据策略设置握手读取超时
  2. 使用 TimedUserValidator 创建 ServerSession
  3. 调用 DecodeRequestHeader() 进行认证和解析
  4. 分发到路由分发器
  5. 将请求(从客户端读取,写入 link)和响应(从 link 读取,写入客户端)作为并行任务运行

关键代码行:proxy/vmess/inbound/inbound.go:226-319

出站处理器

文件:proxy/vmess/outbound/outbound.go

出站处理器的工作流程:

  1. 从配置中选取服务器和用户
  2. 从账户设置中选择加密类型
  3. 对 AEAD 加密方式自动启用 ChunkMaskingGlobalPadding
  4. 支持 SecurityType_ZERO(无加密、无分块),用于 XTLS 场景
  5. 使用随机消息体密钥/IV 创建 ClientSession
  6. 编码请求头 + 消息体,然后读取响应

关键代码行:proxy/vmess/outbound/outbound.go:57-225

实现说明

  1. 旧协议已移除:旧的非 AEAD VMess 认证方式(基于 MD5)已被完全移除。仅支持 AEAD。如果服务端无法通过 AEAD 解密匹配任何用户,将返回错误并进行确定性排空。

  2. 会话重放保护:两层保护——AuthIDDecoderHolder.filter(基于 Auth ID 的 120 秒映射过滤器)和 SessionHistory(3 分钟的 {user, bodyKey, bodyIV} 元组缓存)。

  3. 填充:当启用 GlobalPadding 时,ShakeSizeParser.NextPaddingLen() 返回 shake128(IV) % 64,为每个分块添加 0-63 字节的随机填充。

  4. 认证长度实验:当通过 TestsEnabled: "AuthenticatedLength" 启用时,分块大小本身使用从 KDF16(bodyKey, "auth_len") 派生的单独密钥进行 AEAD 加密。

  5. UDP over Mux:VMess 将 UDP 流量包装为到 v1.mux.cool:666 的 Mux 连接,使用 XUDP 帧封装。

  6. 地址格式:VMess 使用端口在前、地址在后的顺序(不同于 SOCKS5 的地址在前、端口在后)。地址类型字节:0x01=IPv4、0x02=域名、0x03=IPv6。

源码:proxy/vmess/encoding/encoding.go:12-17

用于重新实现目的的技术分析。