VMess
The original protocol: self-encrypting, with AEAD headers and time-based auth.
VMess is the protocol Xray inherited from V2Ray, designed for a world where the tunnel itself had to be the encryption layer. Where VLESS ships bytes in the clear and delegates confidentiality to TLS, VMess assumes nothing about the transport and brings its own AEAD ciphers, its own key exchange, and its own replay defense.
VMess encrypts itself — TLS is optional, not assumed
Every VMess session negotiates fresh symmetric keys inside the request header, so the
protocol is confidential even over raw TCP. The client draws 33 random bytes per connection —
a 16-byte body key, a 16-byte body IV, and a response-check byte (NewClientSession) — and
smuggles them to the server inside the encrypted header. The response direction never sends
keys at all: both sides independently derive them as SHA-256(requestKey)[:16] and SHA-256(requestIV)[:16].
Body traffic is then chunked, and each chunk is sealed with the cipher you pick per user:
{
"protocol": "vmess",
"settings": {
"vnext": [
{
"address": "example.com",
"port": 443,
"users": [{ "id": "27848739-7e62-4138-9fd3-098a63964b6b", "security": "aes-128-gcm" }]
}
]
}
} aes-128-gcm and chacha20-poly1305 are real AEAD (the ChaCha20 key is the 16-byte body key
stretched to 32 bytes with two MD5 rounds — GenerateChacha20Poly1305Key). Chunk nonces are
the body IV with a 2-byte big-endian counter stamped on top (GenerateChunkNonce), and chunk
lengths are XOR-masked with a SHAKE128 stream so even the sizes don’t sit on the wire as
plaintext.
The first 16 bytes are a puzzle only the server can solve
A VMess request identifies its user without ever naming it. The client packs an 8-byte
unix timestamp, 4 random bytes, and a CRC32 into one AES block and encrypts it with a key
derived from the account UUID (CreateAuthID in aead/authid.go). The cmdKey under that
derivation is itself MD5(uuid ‖ "c48619fe-8f02-49e0-b9e9-edf763e17e21") — a magic constant
baked into protocol.NewID. On the wire those 16 bytes are indistinguishable from random.
Once the AuthID matches, the rest of the header follows as two AES-GCM boxes: an 18-byte
sealed length, an 8-byte connection nonce, then the sealed header payload — with per-message
keys derived by a recursive HMAC-SHA256 KDF rooted at the salt string "VMess AEAD KDF", and
the AuthID mixed in as associated data (SealVMessAEADHeader / OpenVMessAEADHeader).
Your clock is part of the protocol
If client and server disagree on the time by more than 120 seconds, authentication fails — the server compares the decrypted timestamp against time.Now().Unix() and returns ErrInvalidTime, whose message in the source literally reads “invalid timestamp, perhaps
unsynchronized time”.
This is the classic VMess failure mode: config is correct, TLS handshake succeeds, and the
connection still dies because a VPS drifted three minutes.
Debug 'invalid user' errors by rewriting configs
A skewed clock produces the same symptom as a wrong UUID: the header never authenticates. No amount of config editing fixes a drifted system time.
Sync both ends with NTP first
Check date -u on client and server before anything else. VLESS has no timestamp check —
if switching protocols “fixes” VMess, your clock was the bug.
Replay is blocked twice behind that time check. First, every accepted AuthID goes into a
120-second replay filter (antireplay.NewMapFilter[[16]byte](120)) — matching the ±120 s
validity window, so a captured AuthID is rejected by the filter while fresh and by the
timestamp after that. Second, SessionHistory remembers each (user, bodyKey, bodyIV) triple for 3 minutes and kills duplicates with “duplicated session id, possibly under replay
attack, but this is a AEAD request”.
AEAD is why alterId died
The original VMess header was authenticated by a bare HMAC-MD5 of the timestamp, keyed by
the user ID — no ciphertext integrity, which left it open to active probing of the header
decryption. The alterId workaround (each user pretending to be dozens of derived IDs) only
spread the problem around. The AEAD header shown above replaced it, and Xray’s implementation
removed the legacy path entirely: MemoryAccount in account.go has no alterId field, and
the AEAD decoder holder is the only authentication path left in TimedUserValidator — an alterId in your config is dead weight.
| VMess | VLESS | |
|---|---|---|
| Confidentiality | Built-in AEAD (AES-GCM / ChaCha20) | None — requires TLS/REALITY |
| User lookup | Trial-decrypt AuthID per user | UUID sent, hash-map lookup |
| Clock sensitivity | ±120 s or auth fails | None |
| Overhead per chunk | 16-byte tag + masked length | Zero |
The honest framing: VMess pays double-encryption tax under TLS but survives without it; VLESS is leaner but is only as private as the layer under it.
proxy/vmess/account.go proxy/vmess/vmess.go