Skip to content

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.cool virtual 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:

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

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

Payload Encryption: AES-128-GCM with key/nonce derived from:

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

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

FieldSizeDescription
Version1 byteAlways 0x01
Body IV16 bytesRandom IV for body encryption
Body Key16 bytesRandom key for body encryption
Response Header1 byteRandom byte, server must echo it back
Option1 byteBitmask: ChunkStream(0x01), ChunkMasking(0x04), GlobalPadding(0x08), AuthenticatedLength(0x10)
Security1 byteUpper 4 bits = padding length (0-15), lower 4 bits = cipher type
Reserved1 byteAlways 0x00
Command1 byte0x01=TCP, 0x02=UDP, 0x03=Mux
AddressvariablePort(2B, BE) + AddrType(1B) + Address
Padding0-15 bytesRandom padding
FNV1a4 bytesFNV-1a hash of all preceding fields (integrity)

Security types (lower 4 bits):

ValueCipher
0x00AUTO / UNKNOWN
0x03AES-128-GCM
0x04ChaCha20-Poly1305
0x05None

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:

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]

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:

mermaid
graph LR
    subgraph "Each Chunk"
        A[Length 2B] --> B[Payload] --> C[Auth Tag]
    end

Chunk size encoding (with masking): When ChunkMasking is enabled, a SHAKE128 stream XORs the 2-byte length field:

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

Source: proxy/vmess/encoding/auth.go:51-87

Nonce generation for body 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]
    }
}

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:

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
}

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:

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)
}

Source: proxy/vmess/aead/kdf.go:13-28

KDF Salt Constants:

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

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

  1. Stores all user CmdKeys in an AuthIDDecoderHolder
  2. On each incoming connection, tries to AES-decrypt the 16-byte Auth ID with each user's key
  3. Validates CRC32 checksum, timestamp range (+-120s), and replay filter
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
}

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:

  1. Sets handshake read deadline from policy
  2. Creates a ServerSession with the TimedUserValidator
  3. Calls DecodeRequestHeader() to authenticate and parse
  4. Dispatches to the routing dispatcher
  5. 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:

  1. Picks the server and user from config
  2. Selects security type from account settings
  3. Automatically enables ChunkMasking and GlobalPadding for AEAD ciphers
  4. Supports SecurityType_ZERO (no encryption, no chunking) for XTLS use cases
  5. Creates a ClientSession with random body key/IV
  6. Encodes request header + body, then reads response

Key lines: proxy/vmess/outbound/outbound.go:57-225

Implementation Notes

  1. 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.

  2. Session replay protection: Two layers -- AuthIDDecoderHolder.filter (120-second map filter on Auth IDs) and SessionHistory (3-minute cache of {user, bodyKey, bodyIV} tuples).

  3. Padding: When GlobalPadding is enabled, ShakeSizeParser.NextPaddingLen() returns shake128(IV) % 64, adding 0-63 bytes of random padding per chunk.

  4. Authenticated Length experiment: When enabled via TestsEnabled: "AuthenticatedLength", chunk sizes themselves are AEAD-encrypted using a separate key derived from KDF16(bodyKey, "auth_len").

  5. UDP over Mux: VMess wraps UDP traffic as Mux connections to v1.mux.cool:666, using XUDP framing.

  6. 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

Technical analysis for re-implementation purposes.