Documentation
¶
Overview ¶
Package proxyproto is a small, dependency-free reader for the HAProxy PROXY protocol (v1 text + v2 binary) headers, used by cadish's opt-in PROXY-protocol listener (internal/server) to recover the real client address when cadish sits behind an L4/TCP-passthrough load balancer (HAProxy send-proxy, AWS NLB, GCP TCP LB) that prepends the original client/server tuple before the TLS/HTTP bytes.
Scope (owner decision 2026-06-24: in-tree, NO external dependency):
- v1: the text line "PROXY <proto> <src> <dst> <sport> <dport>\r\n" where <proto> is TCP4 | TCP6 | UNKNOWN. UNKNOWN (the LB could not determine the tuple) maps to a LOCAL header — the caller falls back to the socket peer.
- v2: the 12-byte signature, a version/command byte, a family/protocol byte, and a 2-byte address-block length, followed by the address block. We parse the PROXY command's AF_INET / AF_INET6 source+dest tuples; AF_UNIX and the LOCAL command map to a LOCAL header. Any address-block bytes beyond the fixed tuple are TLVs, which we deliberately SKIP (we only need the source address).
We intentionally do NOT fall back to the raw stream on a malformed header — the listener treats a parse failure from a trusted peer as a rejection (a downgrade an attacker could otherwise force). Trust gating lives in the listener, not here.
Index ¶
Constants ¶
const DefaultReadTimeout = 5 * time.Second
DefaultReadTimeout bounds how long the wrapper waits for the PROXY header on a freshly accepted connection, so a slow/garbage peer cannot pin an accept goroutine.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Config ¶
type Config struct {
// Trust is the set of trusted PROXY-header source CIDRs (typically the LB's
// addresses). REQUIRED and non-empty.
Trust []netip.Prefix
// ReadTimeout bounds the header read per connection. Zero uses DefaultReadTimeout.
ReadTimeout time.Duration
// OnReject, when non-nil, is called with the reason each time a connection is
// dropped by the REQUIRE-from-trusted policy (untrusted peer, missing or malformed
// header). It is an observability seam for the server's logger/metrics; Accept
// never surfaces a per-connection rejection as a fatal error.
OnReject func(error)
}
Config configures the PROXY-protocol listener wrapper.
SECURITY (load-bearing): Trust is the set of CIDRs whose connections may carry an authoritative PROXY header. A PROXY header is honored ONLY when the immediate TCP peer (the socket RemoteAddr) is inside this set. A header from a peer OUTSIDE the set is REJECTED (the connection is closed) — it is never parsed, so a spoofed PROXY header from an untrusted peer can NEVER forge the client IP. The trust set MUST be non-empty (NewListener errors otherwise): an empty set would mean "trust everyone", i.e. anyone could forge their source address — the forgery hole this whole feature exists to prevent.
type Header ¶
Header is a parsed PROXY header. When Local is true the header carried no usable client tuple (v1 UNKNOWN, v2 LOCAL command, or a non-TCP family) and the caller should use the real socket peer; Source/Dest are then zero.
func ReadHeader ¶
ReadHeader reads and parses exactly one PROXY header (v1 or v2) from br, leaving br positioned at the first application byte after the header. It distinguishes the versions by peeking the leading bytes: the v2 binary signature, else the v1 text prefix. A header that is neither, or is malformed, returns an error (the caller must NOT fall back to the raw stream — that would be an attacker-forceable downgrade).
func (*Header) SourceTCPAddr ¶
SourceTCPAddr returns the recovered source as a *net.TCPAddr (the type the listener wrapper reports as RemoteAddr), or nil for a Local header.
type Listener ¶
type Listener struct {
// contains filtered or unexported fields
}
Listener wraps a net.Listener so each accepted connection first reads a PROXY v1/v2 header (under the REQUIRE-from-trusted policy) and reports the recovered client as its RemoteAddr, BEFORE the HTTP/TLS server sees it. It is installed ONLY when the operator enables the PROXY-protocol listener; the default path is the bare listener, untouched (zero cost when off).
func NewListener ¶
NewListener wraps inner. It returns an error if the trust set is empty — an enabled PROXY listener with no trusted sources is a configuration error (it would let any peer forge its client IP). inner may be nil only in tests that inject the inner listener directly.
func (*Listener) Accept ¶
Accept accepts the next connection, performs the cheap, NON-BLOCKING trust check, and returns a net.Conn whose PROXY header is read LAZILY on first use (R34). The blocking header read is deferred off the single accept loop onto the connection's own goroutine, so a slow trusted peer trickling header bytes can no longer serially starve every other connection from being accepted.
SECURITY: the trust check (immediate-peer-in-trust-set) is synchronous here and parses NO peer bytes, so an UNTRUSTED peer is still rejected at Accept before any header is read — the load-bearing anti-forgery property is unchanged. The REQUIRE policy (a trusted peer MUST send a valid header) is still enforced, just at first use: a missing or malformed header surfaces as a Read/RemoteAddr error and the server closes the connection. RemoteAddr triggers the same lazy read, so no caller ever observes an unvalidated/forged client address (net/http reads RemoteAddr before serving).