VMess Protocol
VMess is the original V2Ray/Xray encrypted proxy protocol. It provides authenticated encryption with multiple cipher options, time-based anti-replay protection, and optional padding/masking to resist traffic analysis.
Overview
- Direction: Inbound + Outbound
- Transport: TCP, UNIX socket
- Encryption: AES-128-GCM, ChaCha20-Poly1305, None
- Authentication: UUID-based with AEAD header encryption
- Mux: Supported via
v1.mux.coolvirtual domain and XUDP
Wire Format
Request (Client to Server)
The VMess AEAD request consists of two layers: an outer AEAD-encrypted header envelope, and the inner command header.
Outer Envelope (AEAD Header)
+----------+---------------------------+--------------+---------------------------+
| Auth ID | Encrypted Payload Length | Conn Nonce | Encrypted Payload |
| 16 bytes | 2 + 16 bytes (GCM tag) | 8 bytes | variable + 16 bytes (tag) |
+----------+---------------------------+--------------+---------------------------+Source: proxy/vmess/aead/encrypt.go:14-61
Auth ID (16 bytes): AES-ECB encrypted block containing:
+---------------+-----------+----------+
| Timestamp | Random | CRC32 |
| 8 bytes (BE) | 4 bytes | 4 bytes |
+---------------+-----------+----------+The AES key for Auth ID encryption is derived via:
aesKey = KDF16(cmdKey, "AES Auth ID Encryption")Source: proxy/vmess/aead/authid.go:26-40
Time Validation: The server decrypts Auth ID for each known user, checks the CRC32, and validates that the timestamp is within 120 seconds of server time.
Source: proxy/vmess/aead/authid.go:99-121
Payload Length Encryption: AES-128-GCM with key/nonce derived from:
key = KDF16(cmdKey, "VMess Header AEAD Key_Length", authID, connNonce)
nonce = KDF(cmdKey, "VMess Header AEAD Nonce_Length", authID, connNonce)[:12]
// Additional data = authIDPayload Encryption: AES-128-GCM with key/nonce derived from:
key = KDF16(cmdKey, "VMess Header AEAD Key", authID, connNonce)
nonce = KDF(cmdKey, "VMess Header AEAD Nonce", authID, connNonce)[:12]
// Additional data = authIDSource: proxy/vmess/aead/encrypt.go:30-51
Inner Command Header (Decrypted Payload)
+-----+--------+--------+---------+--------+----------+-----+---------+---------+-------+
| Ver | BodyIV | BodyKey | RespHdr | Option | Security | Rsv | Command | Address | Pad | FNV1a |
| 1B | 16B | 16B | 1B | 1B | 1B | 1B | 1B | var | 0-15B | 4B |
+-----+--------+--------+---------+--------+----------+-----+---------+---------+-------+Source: proxy/vmess/encoding/client.go:63-101
| Field | Size | Description |
|---|---|---|
| Version | 1 byte | Always 0x01 |
| Body IV | 16 bytes | Random IV for body encryption |
| Body Key | 16 bytes | Random key for body encryption |
| Response Header | 1 byte | Random byte, server must echo it back |
| Option | 1 byte | Bitmask: ChunkStream(0x01), ChunkMasking(0x04), GlobalPadding(0x08), AuthenticatedLength(0x10) |
| Security | 1 byte | Upper 4 bits = padding length (0-15), lower 4 bits = cipher type |
| Reserved | 1 byte | Always 0x00 |
| Command | 1 byte | 0x01=TCP, 0x02=UDP, 0x03=Mux |
| Address | variable | Port(2B, BE) + AddrType(1B) + Address |
| Padding | 0-15 bytes | Random padding |
| FNV1a | 4 bytes | FNV-1a hash of all preceding fields (integrity) |
Security types (lower 4 bits):
| Value | Cipher |
|---|---|
| 0x00 | AUTO / UNKNOWN |
| 0x03 | AES-128-GCM |
| 0x04 | ChaCha20-Poly1305 |
| 0x05 | None |
Source: proxy/vmess/encoding/server.go:114-124
Response (Server to Client)
The response header is also AEAD encrypted:
+----------------------------+----------------------------+
| Encrypted Length (2+16B) | Encrypted Header (var+16B) |
+----------------------------+----------------------------+Keys derived from response body key/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]Source: proxy/vmess/encoding/client.go:179-253, proxy/vmess/encoding/server.go:328-369
Decrypted response header:
+----------+--------+---------+---------+
| RespHdr | Option | CmdID | CmdLen | CmdData |
| 1B | 1B | 1B | 1B | var |
+----------+--------+---------+---------+The RespHdr byte must match the random byte from the request.
Body Encryption
Body data is transmitted as authenticated chunks:
graph LR
subgraph "Each Chunk"
A[Length 2B] --> B[Payload] --> C[Auth Tag]
endChunk size encoding (with masking): When ChunkMasking is enabled, a SHAKE128 stream XORs the 2-byte length field:
// ShakeSizeParser
mask = SHAKE128(bodyIV).next_2_bytes()
wire_length = actual_length XOR maskSource: proxy/vmess/encoding/auth.go:51-87
Nonce generation for body 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]
}
}The nonce is the body IV with the first 2 bytes replaced by an incrementing counter.
Source: proxy/vmess/encoding/client.go:332-340
ChaCha20-Poly1305 key derivation: The 16-byte body key is expanded to 32 bytes:
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
}Source: proxy/vmess/encoding/auth.go:42-49
Termination Signal: When NoTerminationSignal is not set, an empty chunk (length=0) signals the end of the stream.
Source: proxy/vmess/outbound/outbound.go:185-189
KDF (Key Derivation Function)
VMess uses a nested 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)
}Source: proxy/vmess/aead/kdf.go:13-28
KDF Salt Constants:
| Constant | Value |
|---|---|
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" |
Source: proxy/vmess/aead/consts.go:1-14
User Authentication
CmdKey
The CmdKey is derived from the user's UUID:
func NewID(uuid uuid.UUID) *ID {
// cmdKey = MD5(uuid.Bytes() + []byte("c48619fe-8f02-49e0-b9e9-edf763e17e21"))
}Source: common/protocol/id.go
TimedUserValidator
The server maintains a TimedUserValidator that:
- Stores all user CmdKeys in an
AuthIDDecoderHolder - On each incoming connection, tries to AES-decrypt the 16-byte Auth ID with each user's key
- Validates CRC32 checksum, timestamp range (+-120s), and replay filter
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
}Source: proxy/vmess/aead/authid.go:99-121
Behavior Seed
A deterministic "behavior seed" is computed from all user IDs using HMAC-SHA256 + CRC64. This seed controls the drainer pattern (random read lengths before closing invalid connections) to prevent probing attacks.
Source: proxy/vmess/validator.go:114-123
Inbound Handler
File: proxy/vmess/inbound/inbound.go
The inbound handler:
- Sets handshake read deadline from policy
- Creates a
ServerSessionwith theTimedUserValidator - Calls
DecodeRequestHeader()to authenticate and parse - Dispatches to the routing dispatcher
- Runs request (read from client, write to link) and response (read from link, write to client) as parallel tasks
Key lines: proxy/vmess/inbound/inbound.go:226-319
Outbound Handler
File: proxy/vmess/outbound/outbound.go
The outbound handler:
- Picks the server and user from config
- Selects security type from account settings
- Automatically enables
ChunkMaskingandGlobalPaddingfor AEAD ciphers - Supports
SecurityType_ZERO(no encryption, no chunking) for XTLS use cases - Creates a
ClientSessionwith random body key/IV - Encodes request header + body, then reads response
Key lines: proxy/vmess/outbound/outbound.go:57-225
Implementation Notes
Legacy protocol removed: The old non-AEAD VMess authentication (MD5-based) has been completely removed. Only AEAD is supported. If the server cannot match any user via AEAD decryption, it returns an error with deterministic draining.
Session replay protection: Two layers --
AuthIDDecoderHolder.filter(120-second map filter on Auth IDs) andSessionHistory(3-minute cache of{user, bodyKey, bodyIV}tuples).Padding: When
GlobalPaddingis enabled,ShakeSizeParser.NextPaddingLen()returnsshake128(IV) % 64, adding 0-63 bytes of random padding per chunk.Authenticated Length experiment: When enabled via
TestsEnabled: "AuthenticatedLength", chunk sizes themselves are AEAD-encrypted using a separate key derived fromKDF16(bodyKey, "auth_len").UDP over Mux: VMess wraps UDP traffic as Mux connections to
v1.mux.cool:666, using XUDP framing.Address format: VMess uses port-then-address ordering (unlike SOCKS5 which is address-then-port). Address type bytes:
0x01=IPv4,0x02=Domain,0x03=IPv6.
Source: proxy/vmess/encoding/encoding.go:12-17