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.
At a glance
Section titled “At a glance”| 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 |
Minimal example
Section titled “Minimal example”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.
[[inbound]]tag = "vless-in"protocol = "vless"listen = "0.0.0.0" # the default is 127.0.0.1port = 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"[[inbound]]tag = "socks-in"protocol = "socks"port = 1080 # listens on 127.0.0.1
[[outbound]]tag = "proxy"protocol = "vless"server = "proxy.example.com"port = 443
[outbound.stream]network = "ws"security = "tls"
[outbound.stream.ws]path = "/vless" # must match the server
[outbound.settings]id = "11111111-2222-3333-4444-555555555555"
[route]default = "proxy"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.
Inbound settings
Section titled “Inbound settings”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.
| Key | Type | Required | Default | Description |
|---|---|---|---|---|
users | array of tables | no | [] | 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[].id | string | yes | — | 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 0How the server matches users:
- The server looks the UUID up among
usersand 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 (
3333in11111111-2222-3333-4444-555555555555). Two entries that differ only in that group are the same user, and the later one wins. - Entries carry no
emailor other label. Every user gets the same treatment; there is no per-user routing.
Outbound settings
Section titled “Outbound settings”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.
| Key | Type | Required | Default | Description |
|---|---|---|---|---|
id | string | yes | — | 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.
Transports
Section titled “Transports”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".
How a connection works
Section titled “How a connection works”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]]withnetwork = "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.
Mux.cool and XUDP
Section titled “Mux.cool and XUDP”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-destinationv1.mux.cool:0is 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
sniffingon, 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
Endframe, 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`.
Not supported
Section titled “Not supported”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. |
Coming from Xray
Section titled “Coming from Xray”| 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.
Tested interoperability with Xray
Section titled “Tested interoperability with Xray”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 |
Common errors
Section titled “Common errors”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.