Skip to main content

HalfDuplexServer

HalfDuplexServer is the server-side peer for HalfDuplexClient. It receives two inbound half-connections, matches them by the internal intro sent by the client, and reconstructs one normal logical line toward the next node.

Use it when the remote side has split one full-duplex stream into separate upload and download transport connections.

Typical Placement​

Direct in-process pair:

TesterClient -> HalfDuplexClient -> HalfDuplexServer -> TesterServer

Across a TCP transport:

client side: TcpListener -> HalfDuplexClient -> TcpConnector
server side: TcpListener -> HalfDuplexServer -> TcpConnector

HalfDuplexServer should sit on the opposite side of the transport path from HalfDuplexClient.

What It Does​

  • Accepts inbound half-connections from the previous node.
  • Reads the complete 17-byte intro: one exact role byte and one 128-bit pair ID.
  • Matches upload and download lines that belong to the same logical connection.
  • Creates one main line toward next after both halves are available.
  • Forwards upload-side payload to the next node.
  • Sends replies from the next node back through the download line.
  • Uses an internal pipe wrapper so a later half-connection can move to the worker that already owns its partner.

HalfDuplexServer is a middle stream tunnel. It does not create sockets itself, and it does not create packet lines.

Configuration Example​

{
"name": "halfduplex-server",
"type": "HalfDuplexServer",
"settings": {},
"next": "service-node"
}

Required Fields​

FieldTypeDescription
namestringUser-chosen node name. Must be unique inside the config file.
typestringMust be exactly "HalfDuplexServer".
nextstringRequired. The next stream node receives the reconstructed logical line.

HalfDuplexServer must also have a previous node in the chain. It is not a chain head or chain end.

Settings​

There are no required or optional tunnel-specific settings in the current implementation.

Matching Model​

Every incoming transport line starts in an unknown state. The server buffers until all 17 bytes are available, then consumes the intro sent by HalfDuplexClient.

The intro tells the server:

  • which logical connection this half belongs to
  • whether this half is the upload side or download side

The server keeps two internal maps:

MapPurpose
upload-line mapStores upload halves that arrived before their download partner.
download-line mapStores download halves that arrived before their upload partner.

If the matching partner is already waiting, the two halves are paired immediately. If not, the current half is stored until the peer half arrives.

Main Line Creation​

When both halves are available, HalfDuplexServer:

  1. creates one new main line on the upload line's worker
  2. links the main line with both transport-side half-lines
  3. initializes the next node with the main line
  4. strips the complete 17-byte upload intro from the first upload payload
  5. forwards any remaining upload payload to next

After this point, downstream nodes see a normal single WaterWall connection. They do not need to know that the transport side used two connections.

The download intro carries no user payload and is consumed only for matching. The pair ID is routing metadata, not authentication or transport protection; configure security nodes when those properties are required.

Worker Handoff​

The server is created through WaterWall's pipe-tunnel wrapper. If the two halves arrive on different workers, the later half is piped to the worker that already owns the earlier half, and pairing continues there.

This makes the pair robust when upload and download TCP connections are accepted by different workers.

Direction Behavior​

DirectionBehavior
upstream payload on upload lineForward to the reconstructed main line after matching.
upstream payload on download lineConsumed or recycled; download is not the client-to-server payload path.
downstream payload from next nodeForward to the previous side through the download line.
upstream pause/resume on download lineForward to the main line toward next.
downstream pause/resume from next nodeForward to the upload line toward the previous side.

This mirrors the traffic ownership of the split:

  • upload line carries client-originated payload into the service side
  • download line carries service replies back to the client side

The server remains an endpoint for each exact line: it borrows the two transports and owns the reconstructed main line. It must stay active to map payload, pressure and Finish between them. Its startup replay still respects next-side Pause; the ordinary middle-node transparency rule does not remove this producer control.

Temporary Buffering​

If an upload half arrives before its matching download half, the server buffers the upload data until the pair is complete.

ValueSize
maximum waiting upload buffer131070 * max(1, ceil(L / 32768)) bytes
internal intro17 bytes (one role byte plus a fresh 128-bit pair ID)
required_padding_left0 bytes

L is the line pool waiting-budget basis (64 KiB in S1/S2, 1 MiB in higher profiles), independent of ordinary buffer sizes and the splice limit. The limit is 262,140 bytes in S1/S2 and 4,194,240 bytes in higher profiles. It applies to initial publication and later appends; already-paired data forwards directly.

If the waiting upload buffer reaches the limit before the matching download half arrives, the upload half is removed from the map and closed.

Download halves are not buffered the same way. They are mainly stored as waiting map entries until the upload side arrives.

Lifecycle Behavior​

On upstream Init for an inbound half-line, the server initializes local line state and immediately reports Est back toward the previous node for that transport half. The reconstructed main line is not created until both halves are matched.

If either half-line closes before pairing, the server removes it from the waiting map and cleans its line state. If a waiting upload had buffered data, that buffer is recycled.

If either half-line closes after pairing, the server finishes and destroys the main line and schedules the other half-line to close toward the previous side.

If the downstream side closes the reconstructed main line, the server finishes the download line immediately and schedules the upload line to close.

UpStreamEst and DownStreamInit are disabled callbacks for this node.

Notes and Caveats​

  • Use it with a matching HalfDuplexClient.
  • The tunnel depends on the internal 17-byte intro generated by HalfDuplexClient.
  • Duplicate waiting upload or download pair IDs are rejected and the duplicate half-line is closed.
  • Waiting upload halves can consume memory until they are paired or reach the buffer limit.
  • This node has no separate settings for pairing timeout or buffer size today.

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_left0 bytes

Splice and setup storage​

Any delivery used to parse an intro or extend a waiting upload is materialized completely, including body bytes accompanying the intro. Retained setup buffers are ordinary. Pairing strips exactly 17 bytes and replays the retained body before later direct traffic. After startup replay completes, upload and return traffic preserve ordinary or splice wrappers, including across workers. Download-side input after its intro is discarded without materializing its body.

The waiting-upload limit remains 131070 * max(1, ceil(basis / 32768)), including the retained intro: close at or above 262,140 bytes in S1/S2 or 4,194,240 bytes in higher profiles. An available peer pairs before this waiting-limit check. These are logical waiting limits, not a cap on ready deliveries or total physical memory. Fresh materialization and grown waiting buffers retain onward padding.

Both nodes advertise kNodeFlagSupportsSplice. The client requires 17 bytes of left padding for its upload intro; the server requires zero. Actual splice reads require platform/build support, misc.splice enabled, and support from every node in the expanded chain, including the server's PipeTunnel wrapper and all neighbors. Ordinary buffers remain valid in every state. No new settings, wire fields, or read-preference requests are introduced.

Ordering during main-line startup​

Pairing publishes a temporary startup barrier before invoking next Init. The server-owned main line owns the initial ordinary body separately from later upload input. Input received reentrantly during Init or initial replay cannot overtake that body. An empty initial body still preserves the Init barrier.

Later input retained by this barrier has independent inclusive limits of 2 MiB logical bytes, 2 MiB canonical allocation charge, and 1,024 entries. Empty entries consume charge and an entry; allocation capacity may exhaust the charge limit before the byte limit. Refusal closes the association rather than dropping TCP bytes. The initial body is outside this new budget: its existing admission and waiting-upload limits remain unchanged, including immediately pairable large inputs.

All startup retention is ordinary. Splice deliveries joining the FIFO are fully materialized with onward padding. Replay sends the initial body first, then hands off the queued tail in order, coalescing multiple entries into one checked ordinary buffer without callbacks during preparation. Temporary materialization/coalescing storage is bounded scratch, not a promise about aggregate physical memory.

Replay waits for next-side permission. A Resume during Init only records that permission; source Resume follows older startup output and is withheld while next is paused. The barrier ends immediately before the final tail handoff, allowing later reentrant data to follow all older bytes. Ready forwarding remains queue-free and preserves ordinary/splice wrappers. Every close path releases main-owned startup data and its budget before notifying adjacent sides.