Skip to main content

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.next is the peer path that eventually reaches ReverseServer
  • the local target path is attached behind ReverseClient through the Bridge pair
  • when a reverse link becomes active, ReverseClient initializes 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:

FieldTypeDescription
namestringUser-chosen node name. Must be unique inside the config file.
typestringMust be exactly "ReverseClient".
nextstringRequired 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​

SettingTypeDefaultDescription
minimum-unusedintegerworkers * 4Minimum spare reverse links to keep ready per tunnel. Must be greater than 0.
reverse-secret-lengthinteger640Length of the reverse-link handshake. Must be in 1..1024.
reverse-secretASCII stringunsetXORs 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.

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:

DirectionFlow
local side to remote sideprevious node -> ReverseClient -> next node
remote side to local sidenext 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:

CallbackBehavior
upstream InitDisabled; treated as a fatal misuse.
downstream InitDisabled; treated as a fatal misuse.
downstream Est from next sideMarks transport established; moves a still-connecting link to unused once.
downstream Payload from next sideActivates an unused reverse link or forwards to the paired previous-side line.
upstream Payload from previous sideForwards local payload to the paired outbound reverse link.
FinishCloses both internal lines for paired links; cleans counters and replaces capacity.

Practical Notes​

  • minimum-unused is 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 Bridge to 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:

PropertyValue
node flagskNodeFlagSupportsSplice
can_have_prevtrue
can_have_nexttrue
layer_groupkNodeLayer4
layer_group_prev_nodekNodeLayer4
layer_group_next_nodekNodeLayer4
required_padding_left0 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.