flare_mlake · API · http

flare.http.proxy_protocol

PROXY protocol v1 + v2 parser (HAProxy spec).

The PROXY protocol is the de-facto wire shape for forwarding the original client TCP endpoint through a load balancer (HAProxy, AWS NLB, GCP TCP LB, Cloudflare Spectrum, ...) to the origin server. Without it the origin only sees the LB's IP — every access log, every per-IP rate limiter, every geo decision is wrong.

The protocol lives in front of the application data: the LB writes a small header right after the TCP accept on the origin's listener, then forwards the raw TLS / HTTP bytes unchanged. The origin sniffs the first few bytes, parses the header, populates Request.peer with the original client endpoint, and then proceeds with its normal handshake / parse on the bytes that follow the header.

flare ships strict, fuzz-clean parsers for both wire shapes:

  • v1 (text) — "PROXY TCP4 src dst sport dport " (or TCP6 / UNKNOWN). Variable length, terminated by CRLF, spec-mandated maximum 107 bytes.
  • v2 (binary) — 12-byte signature ``

QUIT followed by 1-byte version+command, 1-byte family+protocol, 2-byte big-endian payload length, then a fixed-shape address block plus optional TLVs. Total length is always 16 + payload_length`` bytes.

Public surface:

  • :class:ProxyHeader — the parsed header (source SocketAddr + destination SocketAddr + consumed: Int byte count to skip before reading application data).
  • :class:ProxyParseError — typed error raised on malformed input. Carries version (1, 2, or 0 for the version-detecting wrapper), position (byte offset where the error was detected; -1 if not applicable), and what (a stable human-readable phrase for greppable logs). Defensive: a hostile peer that thinks it's behind a PROXY-protocol-aware LB but actually isn't must not be able to inject log entries or spoof source IPs, so the parser refuses any non-conforming input rather than guessing.
  • :func:parse_proxy_v1 / :func:parse_proxy_v2 — strict parsers that fail closed on any spec violation.
  • :func:parse_proxy_protocol — version-detecting wrapper. Sniffs the first 12 bytes (or fewer) and dispatches to the right parser. Returns None on a buffer that's too short to decide yet (keep reading).

The reactor integration (ServerConfig.trust_proxy_protocol) opt-in is a follow-up; this module ships the parsers

  • tests + fuzz harness independently so the wire shapes can be audited and reused.

References:

Structs

ProxyParseError

struct ProxyParseError

Typed parse error raised by :func:parse_proxy_v1 / :func:parse_proxy_v2 / :func:parse_proxy_protocol.

Fields:

  • version — wire-shape version this error applies to: 1 (v1 text), 2 (v2 binary), or 0 (version- detecting wrapper rejected the buffer before committing to either shape).
  • position — byte offset in the input buffer where the violation was detected, or -1 if not byte-positional (e.g. token-level v1 errors).
  • what — a stable human-readable phrase that callers can grep on. Stable across patch versions so log regressions are obvious.

Implements Copyable, Movable, Writable

Fields
version: Int
position: Int
what: String

write_to

def write_to[W: Writer](self, mut writer: W)

ProxyHeader

struct ProxyHeader

A parsed PROXY protocol header.

Fields: source: The original client endpoint (the IP + port the LB saw on its public-facing socket). destination: The endpoint the client thought it was connecting to (the LB's listener address). Useful for origin servers that multiplex on the LB's port. consumed: Byte count to skip past in the inbound stream before resuming application-protocol read. Always > 0 when this header was successfully parsed. is_local: True for v2 PROXY-LOCAL frames (LB health checks); the source / destination fields are zero-initialised in this case and the application should fall back to getpeername(2) for the peer address. Always False for v1.

Implements Copyable, Movable

Fields
source: SocketAddr
destination: SocketAddr
consumed: Int
is_local: Bool

Functions

parse_proxy_v1

def parse_proxy_v1(buf: Span[UInt8]) -> Optional[ProxyHeader]

Parse a HAProxy PROXY protocol v1 (text) header from buf.

Returns None if the buffer doesn't yet contain the terminating CRLF (caller should keep reading and retry).

Returns the parsed :class:ProxyHeader on success.

Raises :class:ProxyParseError (version=1) on:

  • Missing "PROXY " prefix (wrong wire shape; v2 sniff should have caught this earlier).
  • Header > 107 bytes without CRLF (HAProxy §2.1 cap).
  • Unknown protocol token (must be TCP4, TCP6, or UNKNOWN).
  • Malformed IP / port tokens for TCP4 / TCP6.
  • IPv4 address in a TCP6 header (or vice versa).
  • Trailing whitespace or a missing CR before the LF.

parse_proxy_v2

def parse_proxy_v2(buf: Span[UInt8]) -> Optional[ProxyHeader]

Parse a HAProxy PROXY protocol v2 (binary) header from buf.

Returns None if the buffer is shorter than 16 + payload_length (caller should keep reading).

Raises :class:ProxyParseError (version=2) on:

  • Missing 12-byte signature.
  • Version field != 2 (HAProxy §2.2).
  • Command not in {LOCAL=0, PROXY=1}.
  • Unsupported family.
  • Family + payload-length mismatch (INET requires >= 12, INET6 requires >= 36).

parse_proxy_protocol

def parse_proxy_protocol(buf: Span[UInt8]) -> Optional[ProxyHeader]

Sniff the wire shape and dispatch to v1 or v2.

Returns None when the buffer is too short to decide. Raises :class:ProxyParseError (version=0 for the "no signature found" case; otherwise version=1 or version=2 from the underlying parser) on a buffer that is long enough to commit to a wire shape but fails strict parsing of that shape.