Portunus
Features

PROXY Protocol & Peek Histogram

Per-target PROXY v1/v2 prelude so backends see the original client IP, plus a SNI peek-duration histogram.

Available since v0.10.0. Two additions:

  1. Per-target PROXY-protocol prelude (v1 text or v2 binary).
  2. A SNI ClientHello peek-duration histogram for SNI-mode listeners.

PROXY protocol

Why it exists

Once traffic is relayed through a forwarder, the backend sees the TCP connection as originating from the forwarder's own IP, not the real client. The original (src, dst) tuple is lost at that hop, which breaks:

  • Audit / logging — access logs record the forwarder IP, so you can no longer trace a request back to the real visitor.
  • IP-based controls — rate limiting, WAF rules, geo-IP, and allow/deny lists all key off the client IP and silently misfire.

The PROXY protocol fixes this by sending the original address tuple to the backend before any real payload, so the backend can recover the client identity as if it were a direct connection.

How it works

When a backend opts in via proxy_protocol = "v1" or "v2", the client emits a PROXY header before any payload bytes from the original connection. The backend can then observe the original client's (src_addr, src_port, dst_addr, dst_port).

The backend must be configured to expect a PROXY header. Sending one to a backend that does not understand it makes the backend parse the header as application data and corrupts the connection. This is a both-ends agreement — only enable it per target when the backend is PROXY-aware. TCP only; UDP paths are unaffected.

Per-target opt-in means a single rule can mix PROXY-aware and PROXY-unaware backends. proxy_protocol is set per target in the targets[] array — via --targets-json (or POST /v1/rules), or in a standalone TOML target. The repeatable --target host:port[@priority] shorthand cannot carry it.

# backend-a gets the v2 binary header; backend-b gets the legacy byte stream
portunus-server push-rule edge-01 443 --targets-json '[
  {"host":"backend-a.local","port":8080,"priority":1,"proxy_protocol":"v2"},
  {"host":"backend-b.local","port":8080,"priority":2}
]'
# portunus-standalone: per-target in the rule's targets array
[[rule]]
name = "ha-https"
protocol = "tcp"
listen_port = 443
targets = [
  { host = "backend-a.local", port = 8080, priority = 1, proxy_protocol = "v2" },
  { host = "backend-b.local", port = 8080, priority = 2 },
]

UDP rules carrying proxy_protocol are rejected with validation.proxy_protocol_on_unsupported_rule. The accepted values are "v1" and "v2"; anything else fails validation.proxy_protocol_invalid.

Modes

ModeDescription
v1Human-readable text header PROXY TCP4 <src-ip> <dst-ip> <src-port> <dst-port>\r\n (e.g. PROXY TCP4 1.2.3.4 5.6.7.8 12345 80\r\n; TCP6 for IPv6)
v2Binary header — 16-byte signature/command prefix followed by the address block (28 bytes total for IPv4, 52 for IPv6) per the PROXY-protocol v2 spec
absent (default)No prelude — legacy byte stream

The choice is per-target, not per-rule, so multi-target rules can mix opt-in and opt-out backends.

Since v1.7.0 the prelude write is time-boxed: a slow or stuck upstream can no longer hang connection setup while the header is being written.

Peek-duration histogram (SNI-mode listeners only)

What it is for

In SNI dispatch mode the listener must peek at the inbound TLS ClientHello — without consuming it — to extract the SNI hostname before it can decide which backend the connection belongs to. That peek sits synchronously on the connection-setup path: it has to wait for enough of the ClientHello to arrive (possibly across several TCP segments) and then parse the TLS record. Slow clients, network jitter, or stalled handshakes make this step slow and drag out connection-establishment tail latency.

The histogram measures exactly that step, so you can:

  • Observe the cost of SNI routing — watch p50/p99 peek time to tell whether SNI dispatch is becoming a bottleneck.
  • Spot slow clients / abuse — a mass of long peeks can signal a slowloris-style slow-handshake attack or a network problem.
  • Tune capacity and timeouts — set peek timeouts from the real distribution rather than guessing.

Metrics

When a listener runs in SNI dispatch mode, Portunus records how long the ClientHello peek+parse takes:

portunus_tls_client_hello_peek_duration_seconds_bucket{...}
portunus_tls_client_hello_peek_duration_seconds_sum
portunus_tls_client_hello_peek_duration_seconds_count

Buckets follow Prometheus convention with finite boundaries up to 3 seconds; observations above the 3 s boundary increment _count and le="+Inf" without bumping le="3".

The histogram does not exist for legacy plain-TCP listeners — no peek happens there.

On this page