Skip to main content

ReverseServer

ReverseServer is the server-side peer for ReverseClient. It receives pre-opened reverse links from one side, receives real local/user traffic from the other side, and pairs one half from each side so traffic can flow through the reverse tunnel.

This node is usually placed on the reachable machine: the side that can accept connections from the remote ReverseClient and from the real users or local entrypoint you want to bridge.

Why Bridge Is Usually Needed​

ReverseServer has to join two different connection sources:

  • reverse-link connections created by ReverseClient
  • real user/local connections that should consume those reverse links

In practical WaterWall configs those two sources are usually attached with a Bridge pair:

The peer listener feeds reverse links into ReverseServer. The user listener feeds real traffic into the paired Bridge. ReverseServer then pairs one reverse-link half with one local/user half and forwards between them.

This Bridge-oriented explanation is important: without the Bridge pair it is easy to place ReverseServer as if it were a simple left-to-right tunnel, but the node is really a dynamic matcher between two connection sources.

ReverseServer must connect directly to the Bridge on its next side: ReverseServer -> Bridge, with no intervening node. The companion client-side rule is Bridge -> ReverseClient, also with no intervening node. These are chain-design requirements that users must follow; the code does not automatically enforce either adjacency rule.

What It Does​

  • Accepts reverse-link halves coming from ReverseClient.
  • Validates the internal reverse-link handshake.
  • Accepts local/user halves from the other side of the topology.
  • Buffers data temporarily while one half waits for its peer.
  • Pairs one waiting reverse half with one waiting local half.
  • Forwards payload, finish, pause, and resume between paired halves.
  • Drops invalid reverse-link handshakes.
  • Drops waiting halves that buffer too much data.
  • Can move a waiting reverse-link half to another worker to complete a pair.

ReverseServer is not a pure adapter and not a simple protocol wrapper. It is a middle tunnel that keeps per-line state while each half waits for a match.

Typical Placement​

Server-side shape:

peer listener -> ReverseServer -> Bridge(reverse side)
user listener -> Bridge(user side)

Example topology:

The peer listener is often restricted with a whitelist so only the expected ReverseClient machine can open reverse links.

Configuration Example​

{
"name": "reverse-server",
"type": "ReverseServer",
"settings": {
"reverse-secret-length": 640,
"reverse-secret": "shared-secret"
},
"next": "bridge_reverse"
}

Minimal Bridge-oriented server example:

{
"name": "users_inbound",
"type": "TcpListener",
"settings": {
"address": "0.0.0.0",
"port": 443,
"nodelay": true
},
"next": "bridge_users"
}
{
"name": "bridge_users",
"type": "Bridge",
"settings": {
"pair": "bridge_reverse"
}
}
{
"name": "bridge_reverse",
"type": "Bridge",
"settings": {
"pair": "bridge_users"
}
}
{
"name": "reverse_server",
"type": "ReverseServer",
"settings": {},
"next": "bridge_reverse"
}
{
"name": "reverse_peer_inbound",
"type": "TcpListener",
"settings": {
"address": "0.0.0.0",
"port": 8443,
"nodelay": true,
"whitelist": [
"203.0.113.10/32"
]
},
"next": "reverse_server"
}

In real deployments, the peer path can include TLS, HTTP, Mux, Reality, or other transport/security nodes before reaching ReverseServer.

Required Fields​

Top-level fields:

FieldTypeDescription
namestringUser-chosen node name. Must be unique inside the config file.
typestringMust be exactly "ReverseServer".
nextstringRequired in practical Bridge layouts. Must name the directly adjacent Bridge that leads to local/user traffic; no intervening node is allowed.

settings may be omitted or empty. If present, it must be an object.

Optional Settings​

SettingTypeDefaultDescription
activation-min-sizeinteger64Minimum total activation size, including its four-byte header; 4..65536.
activation-max-sizeinteger1024Maximum total activation size; 4..65536, and min <= max.
reverse-secret-lengthinteger640Length of the reverse-link handshake. Must be in 1..1024.
reverse-secretASCII stringunsetXORs the default expected handshake bytes with this secret, repeated over the handshake length. Must be non-empty ASCII when set.

ReverseServer, ReverseClient, and any SniffRouter reverse detector in front of them must use the same reverse-secret-length and reverse-secret.

The reverse-link side is recognized by an internal handshake from ReverseClient.

Default expected handshake:

640 bytes of 0xFF

When reverse-secret is configured, the same XOR transformation used by ReverseClient is applied before comparison.

ReverseServer compares handshake bytes incrementally, retaining only the matched offset until the full handshake is validated:

  • if the handshake matches, the handshake bytes are stripped
  • any payload after the handshake is kept as buffered data
  • if the handshake is wrong, that reverse-link half is dropped

Two-Sided Matching Model​

ReverseServer keeps two waiting lists per worker:

Waiting halfMeaning
reverse-link halfA connection from ReverseClient that passed the reverse handshake.
local/user halfA connection from the other side of the topology that needs a reverse link.

When payload arrives on an unpaired reverse-link half:

  1. handshake bytes are compared incrementally
  2. a complete valid handshake admits the half to the reverse waiting list
  3. any following application data enters the bounded FIFO before callbacks
  4. the source is held while unpaired and local or cross-worker matching is attempted

A local/user half is registered during Init, which attempts pairing immediately. It does not wait for a first payload. Early application bytes are buffered while the peer is unavailable. An existing application backlog remains queued after pairing while its consumer is paused. After Est, an unpaired user source is paused. A fully authenticated reverse source is also held while unpaired. Pairing sends activation and drains eligible older bytes before releasing these holds. Consumer Pause keeps its source held; clearing a waiting hold never overrides it. Holds and consumer pressure survive migration.

After pairing:

Incoming sideForwarding behavior
reverse-link payloadforwarded toward the local/user side with upstream payload
local/user payloadforwarded toward the reverse-link side with downstream payload
finish from one sidecloses the peer half
pause/resumereflected to the paired peer half

Variable-Size Activation​

A pair sends exactly one activation prefix before any user application bytes:

4-byte big-endian total length | random padding | direct application stream

The total length includes the four-byte header and is in 4..65536. Each pair chooses a fresh random length between activation-min-size (default 64) and activation-max-size (default 1024), inclusive. Both settings belong only to ReverseServer; ReverseClient reads the advertised length without knowing the configured range. Values must be integers in 4..65536, with min <= max.

ReverseClient consumes the complete prefix, then opens its destination line. No user payload is required, so a destination greeting can reach a silent user. The prefix is also sent for users that send immediately; it always precedes application data and never reaches the destination. There are no later activation headers. Fragmentation and a prefix coalesced with application data are supported.

ReverseServer hands off activation once even if the reverse side is paused, in a separate buffer containing no application data. Buffered application bytes still wait for Resume. The receiving transport may queue activation until it can write. FIFO ordering holds through startup callbacks; ready payloads use direct forwarding.

Random lengths avoid one fixed activation size. The plaintext length field and traffic timing remain visible without an enclosing encrypted transport; this is not a guarantee against traffic fingerprinting.

Direction Model​

Source-backed callback behavior:

CallbackBehavior
upstream InitInitializes a reverse-link half and immediately reports downstream establishment to the previous side.
upstream PayloadProcesses the ReverseClient handshake before pairing; after pairing forwards to the local/user side with tunnelNextUpStreamPayload().
downstream InitInitializes and registers the local/user half, reports upstream establishment, and attempts pairing without payload.
downstream PayloadWaits for or pairs with a reverse-link half; after pairing forwards to the reverse side with tunnelPrevDownStreamPayload().
Pause / ResumeForwarded only after a pair exists.
FinishRemoves unpaired waiting halves or finishes the paired peer half.

The immediate establishment callback means the half has been accepted by ReverseServer; it does not mean the reverse tunnel is already paired and forwarding useful payload.

Cross-Worker Pairing​

A new user can notify the owner worker of an idle reverse peer. Either arrival order therefore initiates pairing without further payload. Notifications carry no foreign line pointer.

Waiting lists are per worker. If a reverse-link half arrives on one worker and a local/user half is waiting on another worker, ReverseServer can move the reverse-link half through an internal pipe tunnel layer and then complete the pair on the target worker.

This is why a reverse setup can work even when the two halves are not accepted by the same worker thread.

Buffering Limits​

Each half has one application FIFO with inclusive limits of 2 MiB logical bytes and 1,024 buffers, independent of the RAM profile. Waiting data and retained startup replay share that budget. Empty TCP input consumes no entries; handshake comparison state and the separate activation prefix are protocol metadata.

A refused admission closes the waiting half, or both halves if already paired. Ready direct payloads do not acquire a backlog or inherit its size limit.

Practical Notes​

  • Keep reverse secret settings identical on both peers.
  • Put access control, such as TcpListener.whitelist, on the listener that accepts reverse links from the ReverseClient machine.
  • Use Bridge to connect the user/local traffic side cleanly.
  • Invalid reverse-link handshakes are dropped.
  • Large unpaired payloads are dropped once the buffering limit is exceeded.
  • ReverseServer is tightly coupled to ReverseClient; do not pair it with a generic client that does not send the expected reverse handshake.

Splice Support​

ReverseServer materializes each complete splice payload into ordinary memory in both directions while the half is unpaired. This covers handshake validation, handshake removal, waiting FIFOs and replay when pairing moves to another worker. Handshake completion alone does not end materialization: waiting storage remains ordinary until pairing completes. Initial replay and input queued behind startup backlog also use ordinary storage. Ready paired payloads pass through unchanged in both directions.

The internal PipeTunnel shares ReverseServer's splice capability and transfers opaque payload buffers across workers. Existing waiting limits, callback mapping and direct ReverseServer -> Bridge placement still apply. Splice activation requires an eligible finalized chain and misc.splice.

Node Metadata​

ReverseServer does not prepend protocol bytes after handshake handling and 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