Skip to content

VMess

VMess is the V2Ray protocol that authenticates each connection with a user UUID and a timestamp, then encrypts the request header and the body itself. etemenanki-app speaks the modern AEAD form of VMess in both directions: as an inbound it serves Xray and V2Ray clients, and as an outbound it connects to any VMess server that accepts AEAD.

Read this page when you set up a VMess server or client, when you move a VMess configuration over from Xray, or when clients fail to connect with an authentication error. Two properties shape almost everything below: VMess is AEAD only, and it depends on both clocks being right.

Property etemenanki-app
Header format AEAD only. This is what Xray calls alterId = 0; there is no alterId key.
Body ciphers aes-128-gcm, chacha20-poly1305
Authentication User UUID plus a timestamp that must be within 120 seconds of the server clock
Replay protection Each authentication ID is accepted once
Transports TCP, TLS, WebSocket, gRPC (see Transports)
UDP Yes, carried inside the TCP connection
mux.cool and XUDP Served by the inbound automatically; the outbound does not multiplex

VMess encrypts its own payload, so it works over plain TCP. Over the open internet, you usually put it inside TLS, WebSocket over TLS, or gRPC over TLS so the traffic looks like ordinary HTTPS. The VMess over gRPC and TLS recipe walks through a complete deployment.

The server accepts two users over plain TCP and sends everything out directly. The client exposes a local SOCKS port and forwards every flow to the server.

server.toml
[[inbound]]
tag = "vmess-in"
protocol = "vmess"
listen = "0.0.0.0"
port = 10086
[inbound.settings]
users = [
{ id = "11111111-2222-3333-4444-555555555555" },
{ id = "11111111-2222-3333-4444-666666666666" },
]
[[outbound]]
tag = "direct"
protocol = "freedom"
[route]
default = "direct"

Check each file with etemenanki-app --test -c <file> before you start it. Generate a real UUID for every user with uuidgen or cat /proc/sys/kernel/random/uuid; the UUID is the user’s only credential.

The VMess inbound takes the common inbound keys (tag, listen, port, stream, address_family, sniffing), described in Inbounds, plus this [inbound.settings] table:

KeyTypeRequiredDefaultDescription
usersarray of tablesno[]The accounts allowed to connect, one table per user. Every user has the same access. An empty list is accepted, but then every connection fails authentication. The Xray key clients is rejected as an unknown field.
users[].idstringyes—The user's UUID. Accepts the hyphenated form, 32 hex digits, {…} braces or a urn:uuid: prefix, in any letter case. Any other string fails with inbound <tag>: invalid uuid "…". This is the only key a user table accepts, so alterId, email and level are errors.

Each entry in users is a table with exactly one key, id. The parser rejects every other key, so a user copied from Xray with alterId, email or level stops the configuration:

configuration invalid: inbound vmess-in: invalid settings: unknown field `alterId`, expected `id`
in `users`

Remove the extra keys. alterId = 0 needs no replacement, because AEAD is the only mode.

The id must be a real UUID. These forms all parse, in upper or lower case:

Form Example
Hyphenated 11111111-2222-3333-4444-555555555555
Plain hex 11111111222233334444555555555555
Braced {11111111-2222-3333-4444-555555555555}
URN urn:uuid:11111111-2222-3333-4444-555555555555

Xray also accepts an arbitrary short string as an ID and maps it to a UUID. etemenanki-app does not: id = "my-custom-id" fails with inbound vmess-in: invalid uuid "my-custom-id": invalid character: found `m` at 0. Give such users a UUID.

All users in the list have the same access. etemenanki-app has no per-user route rule or traffic counter; katana, the panel node agent built on the same kernel, adds per-user accounting on top of VMess.

An empty or missing users list passes --test, but then no client can authenticate.

The server checks a new connection against the users one by one, so handshake cost grows with the number of users. In very large lists, users who connect often are moved toward the front of the scan automatically, so you do not need to order the list.

The client picks the body cipher and states it in the request header. The inbound has no setting for it and accepts these two:

Client security (Xray naming) Result on the etemenanki-app inbound
aes-128-gcm Accepted
chacha20-poly1305 Accepted
auto Accepted: the client resolves auto to one of the two ciphers above before it connects
none, zero, or any other cipher Refused: vmess: unsupported security type N, where N is the cipher number the client sent (5 for none)

Current Xray releases no longer offer none or zero and treat an unknown value as auto; the refusal matters for older Xray and V2Ray clients that still send them.

A client that still uses the legacy header (alterId greater than 0) never gets that far. Its first bytes are not an AEAD authentication ID, so the server refuses it as an unknown user.

Every VMess connection starts with a 16-byte authentication ID. The client builds it by encrypting the current Unix time, a random value and a checksum with a key derived from the user’s UUID. The server tries each user’s key until one decrypts to a valid checksum, then compares the embedded time with its own clock:

  • The time must be within 120 seconds of the server clock, in either direction. The limit is inclusive: a difference of exactly 120 seconds passes, 121 seconds fails.
  • The server remembers each accepted ID for 240 seconds (twice the window) and refuses a connection that reuses one. Normal clients create a fresh ID for every connection, so this only stops replayed handshakes.

When the client’s clock and the server’s clock differ by more than 120 seconds, the connection fails exactly like a wrong UUID: vmess: unknown user or invalid auth id. Time zones do not matter, because both sides use Unix time; only the absolute clock does.

If clients report authentication failures and you are sure the UUID is right, check the clocks:

  1. On the server, confirm the clock is synchronized. With systemd, timedatectl status should show System clock synchronized: yes. With chrony, chronyc tracking shows the current offset.

  2. If it is not synchronized, turn NTP on, for example with sudo timedatectl set-ntp true, or start the chrony or ntpd service your distribution uses.

  3. Compare the client’s clock with the server’s by running date -u on both. They must agree to well within 120 seconds.

  4. Retry the connection. No restart of etemenanki-app is needed; the server reads the system clock for every handshake.

The VMess outbound needs the common outbound keys server and port (a missing one fails with outbound <tag>: missing server or missing port) and may use stream and address_family, described in Outbounds. Its own [outbound.settings] table has two keys:

KeyTypeRequiredDefaultDescription
idstringyes—The UUID of the account on the server, in any form the inbound accepts. Leaving it out fails with a missing field error; a malformed value fails with outbound <tag>: invalid uuid "…".
securitystring (enum)no"aes-128-gcm"The body cipher. aes-128-gcm and auto select AES-128-GCM, as does leaving the key out. chacha20-poly1305 selects ChaCha20-Poly1305. Matching ignores letter case. Anything else, including none and zero, fails with unknown vmess security "…". Not to be confused with [outbound.stream].security, which turns on TLS.

security accepts these values, in any letter case:

Value Cipher
key left out AES-128-GCM
auto AES-128-GCM
aes-128-gcm AES-128-GCM
chacha20-poly1305 ChaCha20-Poly1305
anything else Configuration error

In Xray, auto picks AES-128-GCM when the CPU has AES hardware support and ChaCha20-Poly1305 otherwise; here it always means AES-128-GCM. Choose chacha20-poly1305 explicitly when the client runs on hardware without AES instructions. none and zero are refused; the inbound refuses clients that use them too:

configuration invalid: unknown vmess security "none"

The error shows the value in lower case and does not name the outbound, so search the file for every VMess security key when you see it.

The outbound always requests chunk masking and global padding, which is also what Xray’s AEAD client does by default. There is no key to turn them off. It opens one upstream connection per TCP flow and does not multiplex several flows over one connection.

alterId in the outbound settings is an error too: outbound <tag>: invalid settings: unknown field `alterId`, expected `id` or `security` .

sequenceDiagram
    participant C as Client
    participant S as etemenanki-app inbound
    participant T as Target
    C->>S: auth ID (16 bytes)
    Note over S: find the user, check the 120 s window and replay set
    C->>S: sealed request header (command, target, cipher)
    alt TCP command
        S->>T: connect (route decides the outbound)
        T-->>S: connected
        S-->>C: sealed response header
    else UDP or mux command
        S-->>C: sealed response header at once
    end
    C->>S: encrypted body chunks
    S->>T: plaintext
    T-->>S: reply
    S-->>C: encrypted body chunks

A few details follow from this order:

  • For a TCP request, the server sends its response header only once the outbound has connected. If the connection to the target fails, the server closes the client connection without a response.
  • When the TCP target is an IP address and sniffing is on (the default), the server reads the first body chunks, waiting at most 300 milliseconds, to recover a domain name for routing before it opens the outbound. A target that is already a domain name is not sniffed. See Routing.
  • A client that sends nothing is dropped after 10 seconds. Once its first bytes arrive, it has 10 seconds to complete the authentication ID and header. An established connection closes after 300 seconds without traffic in either direction. Limits lists these and the connection caps.

VMess carries UDP inside the same TCP (or TLS, WebSocket, gRPC) connection; the inbound opens no UDP socket.

  • Inbound. A request with the UDP command names one target in its header. Each body chunk the client sends is one datagram to that target, and each reply datagram goes back as one chunk. The server answers the header at once, without waiting for the first packet.
  • Outbound. When a route sends a UDP flow to a VMess outbound, the outbound opens a VMess connection with the UDP command toward the flow’s destination and exchanges one chunk per datagram. Every packet on that connection goes to the target named in its header, and replies are attributed to that target.

For UDP to many peers over a single connection, Xray clients use XUDP, which the inbound supports (next section).

The VMess inbound understands mux.cool, the multiplexing that Xray clients turn on with "mux": { "enabled": true }. There is no key for it: a request with the mux command is recognized and served automatically.

  • Each sub-flow inside the carrier connection is routed and dialed on its own, with its own destination. The carrier’s own destination is the placeholder v1.mux.cool:0, which is never routed or dialed.
  • A sub-flow’s data may be split across VMess chunks and mux frames in any way. The server reassembles a mux frame that spans several VMess chunks, including chunks that arrive together in one read, and forwards each sub-flow’s bytes complete and in order. This is tested against an Xray mux client with a 64 KiB upload.
  • One carrier holds at most 256 sub-flows. The server declines a new sub-flow beyond that (or one that reuses a live session ID) by answering with an end frame and discarding its data; the carrier and its other sub-flows keep running.
  • XUDP (UDP sub-flows whose frames carry a per-packet address) works: one UDP sub-flow can exchange packets with several peers, and replies are attributed to the right peer. A UDP sub-flow lasts until the client ends it or the carrier connection closes. The server reads the XUDP global ID but does not use it to resume a session on a new connection.
  • A reply datagram larger than 8 KiB does not fit in one mux frame and is dropped.
  • Sniffing a sub-flow looks only at the data in its opening frame, so one sub-flow never holds up the others.

The VMess outbound has no mux client. Mux is a server-side feature here, for compatibility with Xray clients.

The VMess inbound and outbound can run over every stream transport:

[stream].network [stream].security Result
tcp (default) none VMess directly over TCP
tls none or tls VMess over TLS
ws none or tls VMess over WebSocket, optionally inside TLS
grpc none or tls VMess over gRPC, optionally inside TLS

network = "tcp" with security = "tls" is refused; write network = "tls" for TLS over TCP. An inbound that listens on a Unix socket accepts plain VMess only; any other network or security is refused, for example with inbound <tag>: protocol vmess over a unix socket does not support stream security "tls". Transports describes every stream key, and VLESS over WebSocket and TLS shows a WebSocket deployment that carries over to VMess by changing the protocol and its settings.

Configuration errors appear when you run --test, at startup and on reload. Connection errors are logged at debug level, for example vmess connection from Some(203.0.113.7) ended: proxy core: vmess: unknown user or invalid auth id; set level = "debug" in the [log] table to see them. The table below lists the message after the proxy core: prefix.

Message Cause Fix
invalid settings: unknown field `alterId`, expected `id` Xray-style user with alterId, email or level Keep only id in each user
invalid settings: unknown field `clients`, expected `users` Xray’s clients list name Rename it to users
invalid uuid "…" The ID is not a UUID Use a UUID generated with uuidgen
unknown vmess security "…" Outbound security is none, zero or misspelled Use aes-128-gcm, chacha20-poly1305 or auto, or leave it out
outbound <tag>: missing server or missing port The outbound has no upstream address Add server and port
vmess: unknown user or invalid auth id Wrong UUID, clocks more than 120 s apart, a replayed handshake, or a legacy alterId client Check the UUID, then the clocks on both sides
vmess: unsupported security type N The client uses none, zero or another unsupported cipher Set the client to aes-128-gcm, chacha20-poly1305 or auto
client did not complete its request in time The client started its handshake but did not finish it within 10 seconds Check the network path and the client’s transport settings
inbound handshake timed out after 10s (no proxy core: prefix) The client connected but sent nothing for 10 seconds Check the client’s configuration and the network path