VlessClient
VlessClient is the client-side plain VLESS v0 tunnel for WaterWall. It writes a VLESS request header with a configured
UUID and target destination, then carries TCP stream data or UDP datagrams through the next stream-facing node.
This node implements only the base VLESS protocol. It does not create TLS, REALITY, XTLS Vision, flow addons, mux, XUDP, WebSocket, gRPC, or any other transport wrapper.
Typical Placement
TCP VLESS client:
TcpListener -> VlessClient -> TlsClient -> TcpConnector
UDP VLESS client:
UdpListener -> VlessClient -> TlsClient -> TcpConnector
Dynamic destination from a proxy/router:
Socks5Server -> VlessClient -> TlsClient -> TcpConnector
SniffRouter -> VlessClient -> TlsClient -> TcpConnector
VlessClient does not create TLS by itself. For normal VLESS deployments, put TlsClient after VlessClient, and make
the final TcpConnector connect to the VLESS server address and port. The real target service is encoded inside the
VLESS request header.
TCP Example
[
{
"name": "vless-client",
"type": "VlessClient",
"settings": {
"uuid": "5783a3e7-e373-51cd-8642-c83782b807c5",
"address": "example.com",
"port": 443,
"protocol": "tcp",
"domain-strategy": "do-not-resolve-domains",
"verbose": false
},
"next": "tls-client"
},
{
"name": "tls-client",
"type": "TlsClient",
"settings": {
"sni": "vless.example.net",
"verify": true
},
"next": "server-out"
},
{
"name": "server-out",
"type": "TcpConnector",
"settings": {
"address": "vless.example.net",
"port": 443,
"nodelay": true
}
}
]
Dynamic-Destination Example
{
"name": "vless-client",
"type": "VlessClient",
"settings": {
"id": "5783a3e7-e373-51cd-8642-c83782b807c5",
"address": "dest_context->address",
"port": "dest_context->port",
"protocol": "dest_context->protocol"
},
"next": "tls-client"
}
id and user-id are accepted aliases for uuid.
UDP Example
{
"name": "vless-client-udp",
"type": "VlessClient",
"settings": {
"uuid": "5783a3e7-e373-51cd-8642-c83782b807c5",
"address": "dest_context->address",
"port": "dest_context->port",
"protocol": "udp"
},
"next": "tls-client"
}
In UDP mode, the previous side remains UDP-facing. VlessClient creates one internal TCP carrier line for that UDP
application line, sends a VLESS UDP request over the carrier, and wraps each UDP payload as a 2-byte big-endian length
plus payload.
Required Fields
Top-level fields:
| Field | Type | Description |
|---|---|---|
name | string | User-chosen node name. Must be unique inside the config file. |
type | string | Must be exactly "VlessClient". |
settings | object | Non-empty VLESS client settings. |
next | string | The stream-facing node that carries VLESS bytes. Required in normal use. |
Required settings fields:
| Field | Accepted names | Description |
|---|---|---|
| UUID | uuid, id, user-id | VLESS user ID. The value can be dashed RFC4122 form or compact 32-character hex. It is sent as 16 raw bytes. |
| target address | target-address, address, target | Final target host or IP encoded in the VLESS request, or "dest_context->address". |
| target port | port | Final target port, or "dest_context->port". Constant numeric ports must be in 1..65535. |
The UUID is serialized in RFC4122 text byte order, not as ASCII and not as a platform GUID memory layout.
Optional Settings
| Field | Default | Description |
|---|---|---|
protocol | "tcp" | Selects VLESS command. Also accepts proto. |
first-payload-timeout-ms | 400 | Integer milliseconds in 0..4,294,967,295; zero disables deliberate waiting. |
verbose | false | Enables extra tunnel logging. |
domain-strategy | "do-not-resolve-domains" | Controls whether domain targets are resolved locally before the request is written. |
Supported protocol values:
| Value | Behavior |
|---|---|
"tcp" or "connect" | Send VLESS TCP command 0x01. |
"udp" or "udp-associate" | Send VLESS UDP command 0x02 and carry UDP datagrams through an internal TCP carrier line. |
"dest_context->protocol" or "line->dest_ctx->protocol" | Use the incoming destination context protocol. Exact TCP selects command 0x01; exact UDP selects command 0x02. |
If dest_context->protocol is selected but the incoming protocol is missing, ambiguous, or not TCP/UDP, the client logs
a warning and falls back to TCP.
Domain Strategy
domain-strategy controls the final target address encoded in the VLESS request.
| Value | Behavior |
|---|---|
"do-not-resolve-domains" | Keep domain targets as domains in the request. The VLESS server side resolves the target. |
"resolve-domains-and-accept-dns-returned-order" | Resolve locally and use the first usable DNS result. |
"resolve-domains-and-prefer-ipv4" | Resolve locally, prefer IPv4, and fall back to IPv6. |
"resolve-domains-and-prefer-ipv6" | Resolve locally, prefer IPv6, and fall back to IPv4. |
"resolve-domains-and-use-only-ipv4" | Resolve locally and require an IPv4 result. If none exists, close the line. |
"resolve-domains-and-use-only-ipv6" | Resolve locally and require an IPv6 result. If none exists, close the line. |
"resolve-domains-with-core-settings" | Resolve locally using the dns settings from core.json. |
When local resolution is enabled, VlessClient creates an internal DomainResolver whose prepare hook applies the
configured target before DNS resolution. Resolution applies when the final VLESS target address is a domain, including
both fixed JSON targets and "dest_context->address". Literal IP addresses are left unchanged.
Request And Response Format
Request header:
version: 00
uuid: 16 raw bytes
addons len: 00
command: 01 TCP or 02 UDP
destination: port first, then address
Destination format:
port: 2 bytes big-endian
address type: 01 IPv4, 02 domain, 03 IPv6
address body: IPv4 4 bytes, domain length + bytes, or IPv6 16 bytes
Response header:
00 00
The client accepts a non-empty response addons section for interoperability, logs a warning, skips those addon bytes, and then delivers body bytes. It does not act on any response addon.
First payload and transport establishment
Init prepares the destination and starts the next node without emitting a request.
The first eligible application payload may arrive before transport Est, during its
callback, or after Pause. It is sent immediately with the complete request
in one ordinary buffer, including one UDP frame header when applicable. Ordinary input
is reused in place when its headroom fits all headers. Otherwise a best-fit ordinary
buffer is allocated; any resident prefix and private-pipe body are copied in order.
Empty TCP input does not trigger a request. An empty UDP datagram emits a 00 00 length prefix and triggers the
initial request when it is the first application payload.
Every direct TCP transport Est reaches the live application, including repeats,
independently of protocol data and Pause. UDP carrier Est maps to its application
once. If the request remains unsent, its first-payload deadline starts at the first Est.
settings.first-payload-timeout-ms defaults to 400, accepts integers from 0 to
4,294,967,295, and rejects other types and ranges. Zero disables deliberate waiting.
Paused time counts. On direct TCP, expiry sends one header-only request even after
Pause; all Pause/Resume notifications pass through, including duplicates, without
retaining consumer-pause state. On an owned UDP carrier, the client is the exact-line
head and retains producer control: an idle header due during carrier Pause waits for
Resume. An eligible payload arriving first cancels the timer and sends the combined
output. Duplicate Est/Resume cannot repeat the request or reset its deadline. Finish
cancels the timer.
One ordinary output does not promise one syscall, TCP packet, or TLS record.
Direct TCP and per-datagram upstream forwarding do not wait for Est or Resume and do not keep application output queues. VLESS downstream parsing validates version 0 and consumes exactly the response header and its addons, at most 257 bytes. Nonempty addons are skipped with a warning. Outbound input never waits for this response.
Once the TCP request has been submitted, the complete response header has been consumed, and retained/reentrant input has drained, the node destroys its local line state and permanently bypasses itself for that line. Later callbacks pass directly between its neighbors. Completing only the request or response is insufficient.
UDP application lines remain borrowed UDP-facing lines. The client creates one dependent, owned TCP carrier on the application's worker and maps Est, Pause/Resume and Finish to that association. The request carries the fixed target; each frame has a two-byte big-endian length. The UDP decoder finishes every complete datagram in an admitted input even when an earlier delivery causes Pause. Nested input joins the guarded parser FIFO behind older input; only incomplete framing/order state remains. Finish, malformed input or receiver refusal stops delivery and closes the affected association. Only this node's owned carrier is destroyed here; the application owner drains it during shutdown.
Splice and retention
The first request plus payload is ordinary; the request itself is at most 278 bytes. Later opaque TCP preserves its original ordinary or splice representation. UDP sends prepend two bytes within the advertised 280-byte padding budget. Header parsing and exact body movement preserve boundaries and use complete ordinary fallback when padding or pipe capacity requires it. Receive extraction preserves full onward padding.
| Retained parser input | Inclusive logical limit |
|---|---|
| TCP body input retained only for parser reentry ordering | 2,097,152 bytes (2 MiB) |
| TCP response assembly, including active head and cached metadata | 2,097,409 bytes (2 MiB plus 257 bytes) |
| UDP wire assembly, including active head and cached headers | 2,162,689 bytes (2 MiB plus 65,537 bytes) |
| UDP body | 0 through 65,535 bytes |
Equality passes; overflow or parser queue refusal closes the association. There is no
retained decoded-output queue or 1,024-record limit on a synchronous batch. Fragment
counts do not consume output slots. These are logical protocol limits, independent
of buffer-pool size, RAM profile, and pipe capacity. Oversized local datagrams are
dropped; malformed received frames close the association. A received length of 0 consumes its two-byte prefix and
delivers exactly one empty UDP datagram. The association stays open and parsing continues with the next frame in order.
This differs from Xray's base VLESS implementation, whose writers drop empty datagrams and whose reader skips zero-length frames.
Empty-datagram delivery requires a peer that supports it; nonempty framing is unchanged.
Empty datagrams are valid at the UDP layer (RFC 8085 §5).
Splice requires misc.splice, a supported build and support from every node in the
final chain, including internal helpers. DomainResolver supports splice. TlsClient
still blocks splice for its entire chain. Keep TLS in ordinary deployment configurations;
splice capability does not guarantee zero-copy or a measured speed increase.
Finish Behavior
VlessClient destroys its own line state before propagating real Finish callbacks. It does not call lineDestroy() on
normal external lines.
In UDP mode, it owns the internal carrier line that it created. When the app line or carrier line closes, it closes the paired side and destroys the owned carrier line safely.
Padding
VlessClient reserves enough padding for its largest 278-byte request and the
first UDP datagram's two-byte length prefix:
required_padding_left = 280
Unsupported Features
This implementation does not support:
- TLS or REALITY creation inside the VLESS node
- XTLS Vision or flow addons
- VLESS mux
- reverse
- XUDP
- WebSocket, HTTP, gRPC, or other transport wrappers
Use separate WaterWall nodes such as TlsClient, HttpClient, MuxClient, or TcpConnector when you need those layers.
Common Mistakes
- Do not expect
VlessClientto connect to the VLESS server by itself; put a stream transport after it. - Do not omit
TlsClientfor public VLESS deployments unless you intentionally want plain VLESS on the wire. - Do not confuse the VLESS server address with the final target address.
TcpConnectorconnects to the server; the VLESS request header carries the final target. - Do not use UDP mode with a next node that is not stream-facing.
- Do not rely on unsupported VLESS features such as Vision, flow, mux, or XUDP.
Node Metadata
Source-backed metadata:
| Property | Value |
|---|---|
| node flags | kNodeFlagSupportsSplice |
can_have_prev | true |
can_have_next | true |
layer_group | kNodeLayer4 |
layer_group_prev_node | kNodeLayer4 |
layer_group_next_node | kNodeLayer4 |
required_padding_left | 280 bytes |