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:
| Field | Type | Description |
|---|---|---|
name | string | User-chosen node name. Must be unique inside the config file. |
type | string | Must be exactly "ReverseServer". |
next | string | Required 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
| Setting | Type | Default | Description |
|---|---|---|---|
activation-min-size | integer | 64 | Minimum total activation size, including its four-byte header; 4..65536. |
activation-max-size | integer | 1024 | Maximum total activation size; 4..65536, and min <= max. |
reverse-secret-length | integer | 640 | Length of the reverse-link handshake. Must be in 1..1024. |
reverse-secret | ASCII string | unset | XORs 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.
Reverse-Link Handshake
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 half | Meaning |
|---|---|
| reverse-link half | A connection from ReverseClient that passed the reverse handshake. |
| local/user half | A connection from the other side of the topology that needs a reverse link. |
When payload arrives on an unpaired reverse-link half:
- handshake bytes are compared incrementally
- a complete valid handshake admits the half to the reverse waiting list
- any following application data enters the bounded FIFO before callbacks
- 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 side | Forwarding behavior |
|---|---|
| reverse-link payload | forwarded toward the local/user side with upstream payload |
| local/user payload | forwarded toward the reverse-link side with downstream payload |
| finish from one side | closes the peer half |
| pause/resume | reflected 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:
| Callback | Behavior |
|---|---|
upstream Init | Initializes a reverse-link half and immediately reports downstream establishment to the previous side. |
upstream Payload | Processes the ReverseClient handshake before pairing; after pairing forwards to the local/user side with tunnelNextUpStreamPayload(). |
downstream Init | Initializes and registers the local/user half, reports upstream establishment, and attempts pairing without payload. |
downstream Payload | Waits for or pairs with a reverse-link half; after pairing forwards to the reverse side with tunnelPrevDownStreamPayload(). |
Pause / Resume | Forwarded only after a pair exists. |
Finish | Removes 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 theReverseClientmachine. - Use
Bridgeto connect the user/local traffic side cleanly. - Invalid reverse-link handshakes are dropped.
- Large unpaired payloads are dropped once the buffering limit is exceeded.
ReverseServeris tightly coupled toReverseClient; 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:
| 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 |