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 密钥派生方式:
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,密钥/随机数派生自:
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,密钥/随机数派生自:
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
| 字段 | 大小 | 描述 |
|---|---|---|
| Version | 1 字节 | 始终为 0x01 |
| Body IV | 16 字节 | 用于消息体加密的随机 IV |
| Body Key | 16 字节 | 用于消息体加密的随机密钥 |
| Response Header | 1 字节 | 随机字节,服务端必须回显 |
| Option | 1 字节 | 位掩码:ChunkStream(0x01)、ChunkMasking(0x04)、GlobalPadding(0x08)、AuthenticatedLength(0x10) |
| Security | 1 字节 | 高 4 位 = 填充长度(0-15),低 4 位 = 加密类型 |
| Reserved | 1 字节 | 始终为 0x00 |
| Command | 1 字节 | 0x01=TCP、0x02=UDP、0x03=Mux |
| Address | 可变长 | Port(2B, BE) + AddrType(1B) + Address |
| Padding | 0-15 字节 | 随机填充 |
| FNV1a | 4 字节 | 前述所有字段的 FNV-1a 散列(完整性校验) |
加密类型(低 4 位):
| 值 | 加密方式 |
|---|---|
| 0x00 | AUTO / UNKNOWN |
| 0x03 | AES-128-GCM |
| 0x04 | ChaCha20-Poly1305 |
| 0x05 | 无 |
源码:proxy/vmess/encoding/server.go:114-124
响应(服务端到客户端)
响应头同样使用 AEAD 加密:
+----------------------------+----------------------------+
| Encrypted Length (2+16B) | Encrypted Header (var+16B) |
+----------------------------+----------------------------+密钥从响应体密钥/IV 派生:
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-253、proxy/vmess/encoding/server.go:328-369
解密后的响应头:
+----------+--------+---------+---------+
| RespHdr | Option | CmdID | CmdLen | CmdData |
| 1B | 1B | 1B | 1B | var |
+----------+--------+---------+---------+RespHdr 字节必须与请求中的随机字节匹配。
消息体加密
消息体数据以认证分块方式传输:
graph LR
subgraph "每个分块"
A[长度 2B] --> B[载荷] --> C[认证标签]
end分块大小编码(带掩码): 当启用 ChunkMasking 时,SHAKE128 流对 2 字节长度字段进行异或:
// ShakeSizeParser
mask = SHAKE128(bodyIV).next_2_bytes()
wire_length = actual_length XOR mask源码:proxy/vmess/encoding/auth.go:51-87
消息体 AEAD 随机数生成:
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 字节:
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:
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 派生:
func NewID(uuid uuid.UUID) *ID {
// cmdKey = MD5(uuid.Bytes() + []byte("c48619fe-8f02-49e0-b9e9-edf763e17e21"))
}源码:common/protocol/id.go
TimedUserValidator
服务端维护一个 TimedUserValidator,它:
- 将所有用户的 CmdKey 存储在
AuthIDDecoderHolder中 - 对每个传入连接,尝试用每个用户的密钥进行 AES 解密 16 字节的 Auth ID
- 验证 CRC32 校验和、时间戳范围(±120 秒)和重放过滤器
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
入站处理器的工作流程:
- 根据策略设置握手读取超时
- 使用
TimedUserValidator创建ServerSession - 调用
DecodeRequestHeader()进行认证和解析 - 分发到路由分发器
- 将请求(从客户端读取,写入 link)和响应(从 link 读取,写入客户端)作为并行任务运行
关键代码行:proxy/vmess/inbound/inbound.go:226-319
出站处理器
文件:proxy/vmess/outbound/outbound.go
出站处理器的工作流程:
- 从配置中选取服务器和用户
- 从账户设置中选择加密类型
- 对 AEAD 加密方式自动启用
ChunkMasking和GlobalPadding - 支持
SecurityType_ZERO(无加密、无分块),用于 XTLS 场景 - 使用随机消息体密钥/IV 创建
ClientSession - 编码请求头 + 消息体,然后读取响应
关键代码行:proxy/vmess/outbound/outbound.go:57-225
实现说明
旧协议已移除:旧的非 AEAD VMess 认证方式(基于 MD5)已被完全移除。仅支持 AEAD。如果服务端无法通过 AEAD 解密匹配任何用户,将返回错误并进行确定性排空。
会话重放保护:两层保护——
AuthIDDecoderHolder.filter(基于 Auth ID 的 120 秒映射过滤器)和SessionHistory(3 分钟的{user, bodyKey, bodyIV}元组缓存)。填充:当启用
GlobalPadding时,ShakeSizeParser.NextPaddingLen()返回shake128(IV) % 64,为每个分块添加 0-63 字节的随机填充。认证长度实验:当通过
TestsEnabled: "AuthenticatedLength"启用时,分块大小本身使用从KDF16(bodyKey, "auth_len")派生的单独密钥进行 AEAD 加密。UDP over Mux:VMess 将 UDP 流量包装为到
v1.mux.cool:666的 Mux 连接,使用 XUDP 帧封装。地址格式:VMess 使用端口在前、地址在后的顺序(不同于 SOCKS5 的地址在前、端口在后)。地址类型字节:
0x01=IPv4、0x02=域名、0x03=IPv6。
源码:proxy/vmess/encoding/encoding.go:12-17