Skip to main content

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:

FieldTypeDescription
namestringUser-chosen node name. Must be unique inside the config file.
typestringMust be exactly "VlessClient".
settingsobjectNon-empty VLESS client settings.
nextstringThe stream-facing node that carries VLESS bytes. Required in normal use.

Required settings fields:

FieldAccepted namesDescription
UUIDuuid, id, user-idVLESS user ID. The value can be dashed RFC4122 form or compact 32-character hex. It is sent as 16 raw bytes.
target addresstarget-address, address, targetFinal target host or IP encoded in the VLESS request, or "dest_context->address".
target portportFinal 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​

FieldDefaultDescription
protocol"tcp"Selects VLESS command. Also accepts proto.
first-payload-timeout-ms400Integer milliseconds in 0..4,294,967,295; zero disables deliberate waiting.
verbosefalseEnables extra tunnel logging.
domain-strategy"do-not-resolve-domains"Controls whether domain targets are resolved locally before the request is written.

Supported protocol values:

ValueBehavior
"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.

ValueBehavior
"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 inputInclusive logical limit
TCP body input retained only for parser reentry ordering2,097,152 bytes (2 MiB)
TCP response assembly, including active head and cached metadata2,097,409 bytes (2 MiB plus 257 bytes)
UDP wire assembly, including active head and cached headers2,162,689 bytes (2 MiB plus 65,537 bytes)
UDP body0 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 VlessClient to connect to the VLESS server by itself; put a stream transport after it.
  • Do not omit TlsClient for public VLESS deployments unless you intentionally want plain VLESS on the wire.
  • Do not confuse the VLESS server address with the final target address. TcpConnector connects 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:

PropertyValue
node flagskNodeFlagSupportsSplice
can_have_prevtrue
can_have_nexttrue
layer_groupkNodeLayer4
layer_group_prev_nodekNodeLayer4
layer_group_next_nodekNodeLayer4
required_padding_left280 bytes