Skip to content

VLESS

VLESS is a lightweight proxy protocol from the V2Ray/Xray family. A client sends a short request header that carries a user UUID, a command and a target address, and after that the connection carries your traffic unchanged. etemenanki-app speaks VLESS as a server (protocol = "vless" in an [[inbound]]) and as a client (in an [[outbound]]), and it interoperates with Xray over TLS, WebSocket and gRPC.

This page covers both roles: every setting, how VLESS combines with a transport, UDP and mux support, the Xray features that are deliberately missing, and the errors you can hit. katana serves VLESS nodes with the same protocol code; there the panel supplies the users and the transport, so this page’s settings apply to etemenanki-app only.

Inbound (server) Outbound (client)
Credentials users = [{ id = "…" }], UUIDs only id = "…", one UUID
Transports tcp, tls, ws and grpc; ws and grpc with or without TLS Same
TCP Yes Yes
UDP Yes, always on Yes, one destination per UDP flow
mux.cool and XUDP Accepted automatically, no setting Not supported: every flow opens its own connection
XTLS flow (Vision) Not supported Not supported
REALITY Not supported Not supported

A server that accepts VLESS over WebSocket and TLS on port 443, and a client that offers a local SOCKS port and sends everything to that server. The certificate must be valid for proxy.example.com, because the client verifies it against that name.

server.toml
[[inbound]]
tag = "vless-in"
protocol = "vless"
listen = "0.0.0.0" # the default is 127.0.0.1
port = 443
[inbound.stream]
network = "ws"
security = "tls"
[inbound.stream.ws]
path = "/vless"
[inbound.stream.tls]
cert_file = "/etc/etemenanki/fullchain.pem"
key_file = "/etc/etemenanki/privkey.pem"
[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 server.toml before you start it. The client needs no [outbound.stream.tls] table here: the TLS server name and the WebSocket Host both fall back to server. For a complete walkthrough with certificates and a test request, follow the VLESS over WebSocket and TLS recipe.

Generate a real UUID for each user with uuidgen or cat /proc/sys/kernel/random/uuid.

These keys go in [inbound.settings] of an inbound with protocol = "vless". The common inbound keys (tag, listen, port, sniffing, stream) are described on the inbounds page.

KeyTypeRequiredDefaultDescription
usersarray of tablesno[]The users allowed to connect, each identified by a UUID. An empty or missing list still builds, but then every client is refused. Xray's clients, decryption and fallbacks keys do not exist here and fail the config as unknown fields.
users[].idstringyes—The user's UUID. Accepted forms: hyphenated (11111111-2222-3333-4444-555555555555), 32 hex digits without hyphens, or the hyphenated form wrapped in braces or prefixed with a lowercase urn:uuid:; hex digits in either case. Anything else fails with invalid uuid. It is the only key of an entry: flow, email and level are rejected.

A bad UUID stops the whole configuration, naming the inbound and the offending value:

configuration invalid: inbound vless-in: invalid uuid "not-a-uuid": invalid character: found `n` at 0

How the server matches users:

  • The server looks the UUID up among users and closes the connection if it is not there. There is no fallback to another service: an unknown client gets nothing back.
  • Like Xray, the server ignores the third group of the UUID when it compares ids (3333 in 11111111-2222-3333-4444-555555555555). Two entries that differ only in that group are the same user, and the later one wins.
  • Entries carry no email or other label. Every user gets the same treatment; there is no per-user routing.

These keys go in [outbound.settings] of an outbound with protocol = "vless". The outbound also needs the common keys server and port, the address of the VLESS server. Without them the build fails with outbound proxy: missing server or outbound proxy: missing port; with a tls, ws or grpc network and no server, it fails earlier on the missing TLS name or host. address_family decides how the server’s name is resolved. See the outbounds page for these keys.

KeyTypeRequiredDefaultDescription
idstringyes—The UUID to authenticate with; it must match a users[].id on the server. Same accepted forms as the inbound: hyphenated, 32 hex digits, or the hyphenated form braced or prefixed with a lowercase urn:uuid:. Leaving it out fails with invalid settings: missing field, a malformed value with invalid uuid. It is the only key: Xray's flow, encryption and level are rejected, and there is no mux setting.

The outbound does not multiplex. Every TCP flow and every UDP flow it carries opens its own connection to the server: a TCP connection, then TLS, then the WebSocket upgrade or HTTP/2 connection when the transport has one. Because it has a server and port, a VLESS outbound can be a member of a balancer.

VLESS runs over any stream transport in [inbound.stream] or [outbound.stream]. Both sides must use the same network, the same WebSocket path or gRPC service_name, and both must agree on TLS. The keys themselves are described on the transports page.

network security What goes over the wire Xray equivalent
"tls" absent, "none" or "tls" VLESS in TLS over TCP "network": "tcp", "security": "tls"
"ws" "tls" VLESS in WebSocket in TLS "network": "ws", "security": "tls"
"grpc" "tls" VLESS in gRPC (HTTP/2) in TLS "network": "grpc", "security": "tls"
"tcp" (the default) absent or "none" Plain VLESS: the UUID is readable "network": "tcp"
"ws" or "grpc" absent or "none" Plain WebSocket or gRPC: the UUID is readable Same, without security

The plaintext rows are for setups where something else already provides TLS, for example a reverse proxy on the same host that terminates TLS and forwards WebSocket to a loopback port. A VLESS inbound that listens on a Unix socket (a listen path) always runs plain. It refuses any network other than "tcp" and any security other than "none", for example inbound vless-in: protocol vless over a unix socket does not support stream network "ws".

sequenceDiagram
    participant C as Client
    participant S as VLESS inbound
    participant T as Target
    C->>S: TLS and WebSocket handshakes
    C->>S: Request header with UUID, command and target, then payload
    Note over S: Look up the UUID, close the connection if unknown
    S->>T: Connect through the outbound the router picks
    T-->>S: Connected
    S-->>C: Response header, 2 bytes
    S->>T: Payload
    T-->>S: Reply
    S-->>C: Reply

The client does not wait for the response header: it sends its first payload right behind the request, and the server holds it until the target is connected. The request header is:

Field Size Accepted values
Version 1 byte 0 only
User id 16 bytes A UUID from users
Addons length 1 byte 0 only. A non-zero value means an XTLS flow, which is refused
Command 1 byte 1 TCP, 2 UDP, 3 mux
Target variable Port (2 bytes), then address type (1 IPv4, 2 domain, 3 IPv6) and address. Absent for the mux command

The server checks every fixed field and closes the connection on anything else. It answers with a two-byte response header (version 0, no addons):

  • for a TCP request, only once the target is connected. If the connection to the target fails, the server closes the client’s connection without sending a response;
  • for a UDP or mux request, right after the user is authenticated.

A client has 10 seconds to complete its VLESS request, and an established connection that carries nothing in either direction for 300 seconds is closed. These limits are shared by all protocols; see Limits.

When the inbound’s sniffing is on (the default) and a TCP request names an IP address instead of a domain, the server holds the first bytes of the payload, for at most 300 ms and 4 KiB, to recover a TLS server name or HTTP Host for routing. UDP requests are not sniffed.

VLESS carries UDP inside the same stream connection. The request header names one target, and every packet in both directions is framed as a 2-byte big-endian length followed by the payload. There is no per-packet address: all packets on that connection go to the target in the header, and the client takes every reply as coming from it.

  • Inbound. UDP is always on; there is no setting to turn it off. Each UDP request is routed like any other flow, so a [[route.rule]] with network = "udp" applies to it.
  • Outbound. The outbound carries a UDP flow as one VLESS UDP request, aimed at the first destination the flow sends to through this outbound. The request header fixes the target, so every later packet of that flow that is routed to this outbound goes to that first destination, whatever address it was meant for, and every reply is reported as coming from it. A SOCKS UDP association that talks to several peers through a VLESS outbound therefore reaches only the first one. Xray avoids this with XUDP, which the outbound does not speak.

Xray clients often enable mux, which sends many flows over one VLESS connection. The VLESS inbound accepts this automatically; there is no key to enable or disable it.

  • A mux connection is a VLESS request with command 3. It has no target of its own: its pseudo-destination v1.mux.cool:0 is never dialled.
  • Each sub-flow inside it names its own destination and is routed separately, as if it had arrived on its own connection. With sniffing on, a sub-flow to an IP address is sniffed only from the payload that arrives with its opening frame; the carrier never waits for more. Sub-flows can be TCP streams or UDP associations; XUDP sub-flows carry an address with every packet, so one association can reach several peers, and each reply is attributed to the peer that sent it.
  • One mux connection carries at most 256 sub-flows at a time. A client that opens more, or reuses the id of a sub-flow that is still open, gets that sub-flow refused with an End frame, and the mux connection itself stays up.
  • Closing the mux connection ends all of its sub-flows.

The outbound never sends mux. If you set Xray-style [outbound.mux], the configuration fails with unknown field `mux`.

These Xray features do not exist in etemenanki-app. Each fails loudly rather than being ignored.

Xray feature What happens here
XTLS flow, including xtls-rprx-vision The flow key is an unknown field on both sides. A client that sends a flow anyway is disconnected with vless addons (xtls flow) are not supported.
REALITY security = "reality" fails with unknown stream security. Use a real certificate and TLS.
fallbacks Unknown field in [inbound.settings]. Unauthenticated connections are closed, never forwarded.
decryption / encryption Unknown fields. Only plain VLESS, which Xray calls "none", is spoken.
Free-form id strings Xray derives a UUID from a short text id; here the id must be a UUID, or the build fails with invalid uuid.
email and level on users Unknown fields.
Outbound mux Unknown field. The outbound opens one connection per flow.
Xray JSON etemenanki-app TOML
Inbound settings.clients[].id [inbound.settings] users[].id
Inbound settings.decryption = "none" Leave it out
Outbound settings.vnext[0].address and .port server and port on the [[outbound]] itself
Outbound vnext[0].users[0].id [outbound.settings] id
Outbound users[0].encryption = "none" Leave it out
streamSettings.network = "tcp" with security = "tls" [.stream] network = "tls"
wsSettings.path [.stream.ws] path
grpcSettings.serviceName [.stream.grpc] service_name
tlsSettings.serverName [.stream.tls] server_name
tlsSettings.certificates[0] [.stream.tls] cert_file and key_file

The Xray migration page covers the rest of the configuration.

The integration tests run etemenanki-app against a real xray-core binary, pass traffic through, and compare the bytes. With VLESS, they cover:

Transport etemenanki-app as client, Xray as server Xray as client, etemenanki-app as server
TLS over TCP (network = "tls", Xray tcp + tls) Tested Tested
WebSocket, plain and with TLS Tested Tested
gRPC, plain and with TLS Tested Tested
Xray client with mux enabled: one stream, several concurrent streams, and over WebSocket with TLS Not applicable Tested
XUDP (UDP over mux): one peer, and two peers in one association, each reply attributed to the right peer Not applicable Tested

Configuration errors appear when you run --test or start the program, and when a hot reload is refused:

Error Cause Fix
inbound vless-in: invalid uuid "…": … A users[].id is not a UUID Use a real UUID; see the accepted forms above
outbound proxy: invalid uuid "…": … The outbound id is not a UUID Same
invalid settings: unknown field `flow`, expected `id` flow, email or level in a user entry, or in the outbound settings Remove the key; XTLS is not supported
invalid settings: unknown field `decryption`, expected `users` An Xray-only key in [inbound.settings], also clients or fallbacks Remove it, or rename clients to users
outbound proxy: invalid settings: missing field `id` No id in [outbound.settings] Add the UUID
outbound proxy: missing server / missing port The outbound has no upstream address Add server and port
outbound proxy: ws stream needs ws.host or server (or tls stream needs tls.server_name or server) No server on an outbound whose transport needs a host or TLS name; this check runs before the missing server one Add server and port
security = "tls" is not valid with network = "tcp"; … Xray’s spelling of TLS over TCP Use network = "tls"
unknown stream security "reality" (expected "tls" or "none") REALITY, XTLS or a typo in security Use "tls"
inbound vless-in: tls stream needs tls.cert_file (or tls.key_file) TLS on the inbound without a certificate or private key Add cert_file and key_file under [inbound.stream.tls]
grpc stream needs grpc.service_name network = "grpc" without a service name Set [.stream.grpc] service_name on both sides

A client that fails at the protocol level is disconnected. With [log] level = "debug", the server logs one line per failed connection, in the form vless connection from … ended: …. Protocol errors carry the prefix proxy core: followed by one of these reasons:

Reason Meaning
invalid vless request user id The UUID is not in users. Check that client and server use the same id.
vless addons (xtls flow) are not supported The client has flow set, usually xtls-rprx-vision. Remove it on the client.
invalid vless request version: … The client is not speaking VLESS, or TLS or the transport does not match on the two sides.
invalid vless command: … A command other than TCP, UDP or mux. The client uses a VLESS feature this server does not implement.
client did not complete its request in time The client started a request and did not finish it within 10 seconds. A client that sends nothing at all is logged as inbound handshake timed out after 10s, without the prefix.

If the client reports a TLS or WebSocket error instead, the transport settings differ between the two sides: check network, security, the WebSocket path and the certificate name.