Skip to main content

HeaderServer

HeaderServer publishes its continuing-path state before calling the next Init, so a synchronous transport Est is forwarded promptly, including repeated notifications. Input submitted reentrantly from Init, Est or initial replay stays behind the original accepted bytes. This temporary ordering FIFO is bounded to 2 MiB / 1,024 buffers and is drained within the current synchronous dispatch; it is empty on the ordinary direct path. Overflow closes the borrowed line through its owner.

After next Init and the initial FIFO replay finish, the node destroys and zeros its line state and permanently bypasses itself. Constant-port mode bypasses immediately after applying the port, before forwarding Init. Pause/Resume are transparent and repeatable once the next side is initialized; they do not gate the initial replay.

HeaderServer is the server-side reader for small connection metadata headers. It can do four different jobs:

  • consume the WaterWall 2-byte port header from HeaderClient and write it into dest_context->port
  • consume the WaterWall 1-byte TCP/UDP header and replace only dest_context->protocol
  • consume HAProxy PROXY protocol v1 or v2 and write the advertised client IPv4 and port into src_context
  • set one constant destination port during upstream Init

It is a normal layer-4 stream tunnel. It does not create sockets, packet lines, or line_t objects.

Typical Placement​

WaterWall destination-port metadata:

client side: TcpListener -> HeaderClient -> TcpConnector
server side: TcpListener -> HeaderServer -> TcpConnector

PROXY protocol source metadata from a trusted proxy:

trusted PROXY sender -> TcpListener -> HeaderServer -> TcpConnector

Constant destination port:

TcpListener -> HeaderServer -> TcpConnector

Use PROXY mode only behind a trusted sender. A raw internet client can forge a PROXY header unless a proxy or a private transport boundary prevents that.

Practical Examples​

Restore the original listener port from HeaderClient​

This is the classic WaterWall pair. The first instance listens on multiple public ports but sends everything to one relay port. The relay restores the original accepted port before TcpConnector chooses the local backend.

{
"name": "header-server",
"type": "HeaderServer",
"settings": {
"override": "dest_context->port"
},
"next": "connector"
}

The matching client-side HeaderClient should use:

{
"name": "header-client",
"type": "HeaderClient",
"settings": {
"data": "src_context->port"
},
"next": "relay-connector"
}

Restore only TCP/UDP identity​

Use this when the sender provides only the protocol and the destination address and port are already known:

{
"name": "protocol-reader",
"type": "HeaderServer",
"settings": {
"override": "dest_context->protocol"
},
"next": "router-or-connector"
}

Pair it with HeaderClient.data = "dest_context->protocol" for the sender's intended destination transport, or "src_context->protocol" for its accepted source transport. The server applies the marker before initializing next, without changing any address, port, or source metadata.

UDP over a TCP carrier still requires separate datagram framing, such as the UdpOverTcp pair.

Read source IP and port from PROXY protocol​

Use this when the previous real peer sends HAProxy PROXY protocol and the next WaterWall nodes should see the original client as line->routing_context.src_ctx.

{
"name": "proxy-source-reader",
"type": "HeaderServer",
"settings": {
"override": "proxy-protocol->source-fields"
},
"next": "router-or-connector"
}

The sender may be HAProxy, nginx, Envoy, or HeaderClient configured with "proxy-protocol", "proxy-protocol-v1", or "proxy-protocol-v2".

Example WaterWall sender:

{
"name": "proxy-header",
"type": "HeaderClient",
"settings": {
"data": "proxy-protocol",
"frontend-ipv4": "203.0.113.10"
},
"next": "relay-connector"
}

In this mode, HeaderServer accepts only IPv4 TCP PROXY headers. It consumes the header, writes the source IP and source port into src_ctx, then initializes the next upstream node and forwards any payload that followed the header.

Force one backend port​

Use a numeric override when every line should use the same destination port and no header should be consumed.

{
"name": "force-https",
"type": "HeaderServer",
"settings": {
"override": 443
},
"next": "connector"
}

Do not place a header-writing HeaderClient before numeric mode unless you intentionally want that header to remain in the application payload.

Required Fields​

Top-level fields:

FieldTypeDescription
namestringUser-chosen node name. Must be unique inside the config file.
typestringMust be exactly "HeaderServer".
settingsobjectRequired. Must contain override.
nextstringRequired. The next layer-4 stream node receives the routed line.

Required settings fields:

FieldTypeDescription
overridenumber or stringSelects which field is overridden and which header, if any, is consumed.

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

settings.override​

Accepted values:

ValueMeaning
"dest_context->port"Read the 2-byte WaterWall upstream header and set dest_ctx.port.
"line->dest_ctx->port"Compatibility alias for "dest_context->port".
"dest_context->protocol"Read the 1-byte WaterWall TCP/UDP header and replace only dest_ctx.protocol.
"line->dest_ctx->protocol"Alias for "dest_context->protocol".
"proxy-protocol->source-fields"Read PROXY protocol v1 or v2, then set src_ctx.ip and src_ctx.port.
1 through 65535Set this constant destination port during upstream Init.

Startup fails if override is missing, is not one of the accepted strings, or is a number outside 1 through 65535.

WaterWall Port Header Mode​

When override is "dest_context->port" or "line->dest_ctx->port", HeaderServer waits for the first 2 upstream bytes. Those bytes are decoded as a big-endian port and written to:

line->routing_context.dest_ctx.port

Upstream Init is intentionally delayed until the header is decoded. Payload bytes are staged in a buffer_stream_t, so the 2-byte header may arrive split across payload callbacks.

Decoded ports below 10 are rejected to preserve the tunnel's existing conservative close behavior.

WaterWall Protocol Header Mode​

When override is "dest_context->protocol" or "line->dest_ctx->protocol", HeaderServer waits for one upstream byte. It consumes 6 as TCP or 17 as UDP and replaces only:

line->routing_context.dest_ctx.protocol

The marker is read once per normal layer-4 line. All source metadata and the destination address and port are preserved. The server applies the protocol before calling next Init, then forwards any remaining payload in order. Empty payload callbacks do not complete the header. Any other byte closes the line without initializing next.

This is internal routing metadata, with no IP address, port, or PROXY protocol fields. It does not encode UDP datagram boundaries.

PROXY Protocol Source-Fields Mode​

When override is "proxy-protocol->source-fields", the first upstream bytes must be a valid HAProxy PROXY protocol v1 or v2 header. If the header is missing, malformed, not IPv4, or does not carry source fields, HeaderServer logs an error and closes the line.

Accepted headers:

HeaderAccepted form
PROXY v1PROXY TCP4 <source-ip> <dest-ip> <source-port> <dest-port>\r\n
PROXY v2version 2, PROXY command, TCP4 family/transport

Rejected headers include:

  • PROXY v1 UNKNOWN
  • PROXY v1 TCP6
  • PROXY v2 LOCAL
  • PROXY v2 UNSPEC, TCP6, UDP4, UDP6, or any unsupported family
  • malformed or oversized headers

Only these fields are applied:

line->routing_context.src_ctx.ip_address = PROXY source IPv4
line->routing_context.src_ctx.port = PROXY source port
line->routing_context.src_ctx.protocol = TCP

Destination fields, TLVs, and every other PROXY field are consumed but ignored. IPv6 is not supported in this mode. peer_source_port is not rewritten.

PROXY v1 is capped at 108 bytes. PROXY v2 consumes the full v2 record length, including any TLVs, but requires the fixed TCP4 address block to be present.

Constant Override Mode​

When override is a number, HeaderServer does not read or strip any payload header.

During upstream Init, it sets:

line->routing_context.dest_ctx.port = configured port

Then it immediately forwards upstream Init to next. All upstream payload is forwarded unchanged.

Direction Behavior​

DirectionDelayed header modes
upstream InitInitialize local state, but delay next init until the configured header is decoded.
upstream payload before headerBuffer until the full configured header is available.
upstream payload after headerForward to next.
upstream pause/resume before headerIgnored; there is no initialized next side yet.
upstream pause/resume after headerForward to next.
downstream Est / pause / resume before headerIgnored.
downstream payload before headerRecycled, then the previous side is closed.
downstream callbacks after headerForward to the previous node.

In constant mode, the line starts established during upstream Init, so normal upstream and downstream callbacks are forwarded directionally.

UpStreamEst and DownStreamInit are disabled callbacks for this node.

Finish Behavior​

After bypass, Finish travels directly to the next active node in its direction. The following cleanup applies while header routing or initial replay is active.

On upstream Finish, HeaderServer destroys local line state. It forwards the finish to next only if the line had already completed header routing and initialized the next side.

On downstream Finish, it destroys local line state and forwards the finish to the previous node.

On protocol error, it destroys local state, closes next if next had already been initialized, and closes the previous side.

The tunnel does not call lineDestroy() because it does not create the line.

Limits and Padding​

ValueSize
WaterWall port header2 bytes
WaterWall protocol header1 byte
minimum accepted WaterWall header port10
PROXY protocol v1 headerup to 108 bytes
PROXY protocol v2 base header16 bytes
PROXY protocol v2 required TCP4 address block12 bytes
required_padding_left0 bytes

The server only consumes headers. It does not prepend bytes and therefore does not require left padding.

Notes and Caveats​

  • Use WaterWall port-header mode with HeaderClient.data = "src_context->port".
  • Use protocol-header mode with HeaderClient.data = "src_context->protocol" or "dest_context->protocol"; UDP over TCP needs separate datagram framing.
  • Use PROXY source-fields mode with a trusted PROXY sender.
  • PROXY source-fields mode is IPv4-only.
  • PROXY source-fields mode updates only src_ctx; it does not rewrite dest_ctx.
  • While waiting for a header, HeaderServer has not initialized the next tunnel yet.
  • It should not be used as packet-line shared state.

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 Support​

HeaderServer advertises kNodeFlagSupportsSplice. While waiting for a WaterWall port/protocol header or a PROXY protocol header, it materializes the entire received payload with sbufEnsureOrdinary() before passing it to BufferStream. This also preserves any application bytes coalesced with the header. Once header routing is complete, both directions and the bounded initial-reentry queue preserve opaque splice buffers. Constant-port mode needs no payload inspection.

It does not request ordinary reads during temporary header parsing: read preferences last for the whole line and would also disable later splice reads.