Contents

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.

proxy/vmess/encoding/client.go proxy/vmess/encoding/auth.go

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.

The AuthID authenticates by time and key derivation; replays and skewed clocks are rejected at the door

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

proxy/vmess/aead/authid.go proxy/vmess/aead/encrypt.go proxy/vmess/aead/kdf.go

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.

Don't

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.

Do

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

proxy/vmess/validator.go proxy/vmess/encoding/server.go

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.

VMessVLESS
ConfidentialityBuilt-in AEAD (AES-GCM / ChaCha20)None — requires TLS/REALITY
User lookupTrial-decrypt AuthID per userUUID sent, hash-map lookup
Clock sensitivity±120 s or auth failsNone
Overhead per chunk16-byte tag + masked lengthZero

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