ReverseClient
ReverseClient keeps a pool of pre-opened outbound reverse connections toward a
remote ReverseServer. When the server side needs a real connection, one of
those waiting reverse links is consumed, paired with a local-facing line, and
used as a tunnel back through the client-side machine.
This is the client-side node for reverse tunnel layouts: it is usually placed on the machine that can dial out but cannot accept direct inbound connections.
Why This Layout Uses Bridge
Most WaterWall chains read naturally from left to right:
ReverseClient has a different shape. It must create outbound idle links to
the remote ReverseServer, but once the remote side assigns one of those links,
the client still needs to expose the connection to a local target such as
xray-core, a local SOCKS service, or another local TCP endpoint.
WaterWall keeps a single next field per node. Instead of giving
ReverseClient special custom fields such as next-peer and next-core, the
reverse layout uses a Bridge pair:
In this layout:
ReverseClient.nextis the peer path that eventually reachesReverseServer- the local target path is attached behind
ReverseClientthrough the Bridge pair - when a reverse link becomes active,
ReverseClientinitializes the local side toward the previous node
Read the Bridge page before designing a reverse topology. The Reverse nodes
depend on that direction reversal and the required adjacency described below.
What It Does
- Creates outbound reverse links through the next node.
- Sends a private reverse-link handshake on every spare connection.
- Keeps a minimum number of unused, established reverse links per worker.
- Tracks connecting links separately from established-but-unused links.
- Waits for the remote side to send real payload on a spare reverse link.
- When a spare link is consumed, initializes the paired local-facing line toward the previous node.
- Forwards payload, finish, pause, and resume across the paired lines.
- Replenishes the spare pool whenever a link is consumed or closed.
- Closes and replaces idle spare links that remain unused for too long.
ReverseClient is not a normal listener and not a normal connector. It creates
internal lines itself and uses the ordinary next/previous WaterWall callbacks to
join the reverse peer path to the local target path.
Required Chain Adjacency
Place Bridge immediately before ReverseClient: Bridge -> ReverseClient.
Do not put any node between them. On the server side, place Bridge immediately
after ReverseServer: ReverseServer -> Bridge, also with no intervening nodes.
These are chain-design rules that users must follow. The code does not
automatically enforce either adjacency requirement. Transport or processing nodes
must be placed outside these two gaps; the outbound path after ReverseClient
may still contain the transport nodes described below.
Typical Placement
Client-side shape:
Bridge(local target side) <-> Bridge(to ReverseClient) -> ReverseClient -> outbound peer path
A simplified full shape:
The outbound peer path can be plain TCP, or it can include TLS, HTTP, Reality,
Mux, or other transport nodes as long as it eventually reaches the matching
ReverseServer.
Configuration Example
{
"name": "reverse-client",
"type": "ReverseClient",
"settings": {
"minimum-unused": 16,
"reverse-secret-length": 640,
"reverse-secret": "shared-secret"
},
"next": "outbound-to-reverse-server"
}
Minimal Bridge-oriented example:
{
"name": "outbound_to_core",
"type": "TcpConnector",
"settings": {
"address": "127.0.0.1",
"port": 443,
"nodelay": true
}
}
{
"name": "bridge_local",
"type": "Bridge",
"settings": {
"pair": "bridge_reverse"
},
"next": "outbound_to_core"
}
{
"name": "bridge_reverse",
"type": "Bridge",
"settings": {
"pair": "bridge_local"
},
"next": "reverse_client"
}
{
"name": "reverse_client",
"type": "ReverseClient",
"settings": {
"minimum-unused": 4
},
"next": "outbound_to_peer"
}
{
"name": "outbound_to_peer",
"type": "TcpConnector",
"settings": {
"address": "203.0.113.10",
"port": 443,
"nodelay": true
}
}
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 "ReverseClient". |
next | string | Required in practical configs. This is the outbound peer path toward ReverseServer. |
settings may be omitted or empty. If present, it must be an object.
Optional Settings
| Setting | Type | Default | Description |
|---|---|---|---|
minimum-unused | integer | workers * 4 | Minimum spare reverse links to keep ready per tunnel. Must be greater than 0. |
reverse-secret-length | integer | 640 | Length of the reverse-link handshake. Must be in 1..1024. |
reverse-secret | ASCII string | unset | XORs the default handshake bytes with this secret, repeated over the handshake length. Must be non-empty ASCII when set. |
ReverseClient, ReverseServer, and any SniffRouter reverse detector in
front of them must use the same reverse-secret-length and reverse-secret.
Older documentation said changing the reverse handshake required editing the
source. That is no longer accurate: the current implementation exposes
reverse-secret-length and reverse-secret in node settings.
Reverse-Link Handshake
Each spare reverse connection starts by sending an internal handshake.
Default handshake:
640 bytes of 0xFF
When reverse-secret is configured, each default byte is XORed with the
corresponding byte of the ASCII secret, repeated until reverse-secret-length
bytes have been produced.
This handshake is not user payload. ReverseServer uses it to distinguish
pre-opened reverse links from the local/user side of the reverse topology.
Startup And Pool Behavior
At startup, ReverseClient asks every worker to create outbound reverse links
until the spare target is met.
For each worker it tracks:
- connecting reverse links
- established but still unused reverse links
If the sum is below minimum-unused, the node schedules more outbound reverse
connections on that worker.
After the next side reports downstream establishment for a reverse link, that link becomes part of the unused pool if it is still connecting. Activation may also arrive after transport Init but before Est; completing it moves the link directly from connecting to active. Late or repeated Est marks transport readiness without changing an active pair's counters or creating another replacement spare.
Activation Flow
A spare reverse link remains unused until the complete one-time activation
prefix from ReverseServer has arrived. Its four-byte big-endian total length
includes the header and must be in 4..65536. Random padding is discarded
incrementally with a four-byte header cache and 256-byte scratch, without
allocating the advertised total length. Split headers/padding and a prefix
coalesced with application bytes are accepted. Malformed lengths close the pair;
the existing 30-second idle deadline also covers incomplete activation.
After consuming the prefix, the client decrements the connecting or unused count, removes the idle entry, increments the active count, schedules a replacement spare, and initializes the local paired line toward the previous node. A destination greeting can flow immediately, even when the user has sent nothing. The activation never reaches the destination. Remaining application bytes are forwarded in order, and all later deliveries pass through directly. There is no further activation framing and no client setting for the server's size range. Nested application input during destination Init/initial handoff stays behind older bytes in a FIFO. The original initial body and queued input share a 2 MiB logical-byte / 1,024-buffer budget. Empty input consumes no entries; overflow closes the pair. Ready direct payloads do not use this backlog.
After that, payload flows both ways:
| Direction | Flow |
|---|---|
| local side to remote side | previous node -> ReverseClient -> next node |
| remote side to local side | next node -> ReverseClient -> previous node |
Pause and Resume are forwarded across the paired lines, including repeats. Before destination Init, the latest transport Pause is remembered; Resume cancels it. An outstanding Pause is relayed after Init returns, before the initial body. Already admitted application bytes still drain in FIFO order through Pause. The node remains active because every delivery maps between two different lines.
Finish And Replacement Behavior
If an active paired link closes from either side:
- both internal line states are destroyed
- the peer side is finished
- the internal lines are destroyed by
ReverseClient, because it created them - the active counter is decremented
- a replacement spare link is scheduled
If an unused reverse link closes before pairing:
- connecting or unused counters are updated
- its idle-table entry is removed
- both internal lines are destroyed
- a replacement is scheduled
If a spare link remains unused for about 30 seconds, the idle timeout closes
and replaces it.
Direction And Lifecycle Notes
ReverseClient creates its own internal line pairs. Direct external upstream or
downstream init into this node is not part of its normal contract.
Source-backed callback behavior:
| Callback | Behavior |
|---|---|
upstream Init | Disabled; treated as a fatal misuse. |
downstream Init | Disabled; treated as a fatal misuse. |
downstream Est from next side | Marks transport established; moves a still-connecting link to unused once. |
downstream Payload from next side | Activates an unused reverse link or forwards to the paired previous-side line. |
upstream Payload from previous side | Forwards local payload to the paired outbound reverse link. |
Finish | Closes both internal lines for paired links; cleans counters and replaces capacity. |
Practical Notes
minimum-unusedis a latency/capacity tradeoff. Higher values keep more idle outbound connections ready.- Idle pre-opened links are intentional. Do not treat them as leaked connections.
- Keep the reverse secret settings identical on both peers.
- Use
Bridgeto attach the local target side cleanly without inventing custom chain fields. - The outbound peer path may include other transport/security nodes, but it must
preserve byte-stream connectivity to the matching
ReverseServer.
Node Metadata
ReverseClient does not frame payload itself after the handshake, so it does not need left padding.
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 | 0 bytes |
Splice support
The reverse handshake is generated in ordinary memory. Activation parsing consumes only the prefix bytes through representation-aware helpers; an accompanying application body can retain its private pipe. Ready application payloads pass through unchanged in both directions. Nested startup input retains its ordinary or private-pipe representation in FIFO order. Closing during destination Init settles retained application bytes exactly once. Both reverse nodes support eligible splice chains; keep the direct Bridge -> ReverseClient adjacency.
Ordinary fallback buffers remain accepted. Splice requires a supported Linux build, misc.splice enabled, no packet nodes, and support from every node in the finalized expanded/merged chain. The capability flag does not bypass these checks. No performance improvement is promised without measurement.