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
HeaderClientand write it intodest_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:
| Field | Type | Description |
|---|---|---|
name | string | User-chosen node name. Must be unique inside the config file. |
type | string | Must be exactly "HeaderServer". |
settings | object | Required. Must contain override. |
next | string | Required. The next layer-4 stream node receives the routed line. |
Required settings fields:
| Field | Type | Description |
|---|---|---|
override | number or string | Selects 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:
| Value | Meaning |
|---|---|
"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 65535 | Set 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:
| Header | Accepted form |
|---|---|
| PROXY v1 | PROXY TCP4 <source-ip> <dest-ip> <source-port> <dest-port>\r\n |
| PROXY v2 | version 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
| Direction | Delayed header modes |
|---|---|
upstream Init | Initialize local state, but delay next init until the configured header is decoded. |
| upstream payload before header | Buffer until the full configured header is available. |
| upstream payload after header | Forward to next. |
| upstream pause/resume before header | Ignored; there is no initialized next side yet. |
| upstream pause/resume after header | Forward to next. |
downstream Est / pause / resume before header | Ignored. |
| downstream payload before header | Recycled, then the previous side is closed. |
| downstream callbacks after header | Forward 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
| Value | Size |
|---|---|
| WaterWall port header | 2 bytes |
| WaterWall protocol header | 1 byte |
| minimum accepted WaterWall header port | 10 |
| PROXY protocol v1 header | up to 108 bytes |
| PROXY protocol v2 base header | 16 bytes |
| PROXY protocol v2 required TCP4 address block | 12 bytes |
required_padding_left | 0 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 rewritedest_ctx. - While waiting for a header,
HeaderServerhas not initialized the next tunnel yet. - It should not be used as packet-line shared state.
Node Metadata
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 |
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.