Re: relayd: receive the PROXY protocol (v1 and v2) on listeners

Romain FABBRI <[email protected]> Tue, 14 Jul 2026 11:54:02 +0000
Newsgroups gmane.os.openbsd.tech
Message-ID <GV4PR08MB115802D706F8258321C99632FFCF92@GV4PR08MB11580.eurprd08.prod.outlook.com>
Thanks for the quick review!

Revised diff on codeberg (I hope it's ok my email client sucks): 
https://codeberg.org/cybercloud/openbsd-relayd-proxyproto/src/branch/main/patches/current/0001-relayd-proxy-protocol-receive.patch

All four points are correct and addressed:

> 1. I think that multiple listen on directives silently inherit the
>    first listener's proxy setting.

Right.  relay_inherit() copies the whole config of the first relay onto
each additional listener and only restores ss/port/F_TLS, so proxyproto
was overwritten by the first listener's value.  Fixed by preserving it
per-listener there, next to F_TLS.

> 2. Seems that fragmented v2 message will be treated as v1.

Correct.  When fewer than the 12-byte v2 signature had arrived, the code
fell through to the v1 parser, which read the CRLF inside the signature
as a v1 line terminator and rejected the connection.  Now the signature
is compared against whatever has arrived: if it matches so far but is
not yet complete, proxy_protocol_read() returns "need more data" and
reschedules instead of trying v1.

> 3. Not fully sure, but looks like that an IPv6 client advertised over
>    an IPv4 proxy connection produces an invalid mixed family v1 header
>    and a corrupted v2 source.

Yes.  I was adopting only the source, so se_in.ss could end up a
different family than se_sockname; a PROXY header re-emitted towards a
backend then mixed families (a TCP6 source with the IPv4 local address
in v1, or a 4-byte read of a v6 sockaddr in v2).  Fixed by adopting both
advertised endpoints from the header, so the source and the local name
always share the family.  A v1 header is single-family by construction;
a v2 TCP4/TCP6 header carries matching src and dst.

> 4. I think we need regression tests for this.

Added regress/usr.sbin/relayd/args-proxy-protocol.pl and
args-proxy-protocol-v2.pl (a one-line Relayd.pm hook appends the keyword
to the generated listener): the client prepends a v1/v2 header
advertising 192.0.2.1, and "log connection" then shows that address as
the session source rather than the loopback peer.  Both pass in copy and
splice mode on -current.

A standalone harness around proxy_protocol_read() also covers v1, v2,
TCP6, LOCAL, malformed input, a fragmented v2 header, and exact header
consumption, and all cases pass.

One behaviour note that falls out of point 3: since both advertised
endpoints are adopted, protocol filter rules evaluate the advertised
addresses on such listeners.  "from" matches the real client address
(relay_test() compares the rule against the session source the header
replaced), and "to" now matches the destination the client originally
connected to on the fronting proxy rather than the local listen
address.  This makes source-based filter rules meaningful behind a
fronting proxy, where they previously only ever saw the proxy's
address.

To lock that down I added a third regress,
args-proxy-protocol-filter.pl: the protocol blocks every request and
re-allows only "from 192.0.2.1/32", the address advertised in the PROXY
header.  The request reaches the backend only if relay_test() reads the
adopted source; with the loopback peer the pass rule would not match and
the server would time out.  It passes in copy and splice mode; as a
sanity check, pointing the pass rule at a different address makes it
fail as expected.  Included in the diff.

Regards,
Romain

-----Message d'origine-----
De : Kirill A.Korinsky <[email protected]> 
Envoyé : mardi 14 juillet 2026 01:27
À : Romain FABBRI <[email protected]>
Cc : [email protected]
Objet : Re: relayd: receive the PROXY protocol (v1 and v2) on listeners

On Mon, 13 Jul 2026 19:16:44 +0200,
Romain FABBRI <[email protected]> wrote:
> 
> Hi,
> 
> I am not subscribed to tech@, so please keep me in Cc on any replies.
> 
> relayd(8) can already PREPEND a PROXY protocol header towards a 
> backend (proxy_protocol.c, "forward to ... proxy-protocol v1|v2").
> 
> The attached diff adds the symmetric direction: a listener can be told 
> to EXPECT a PROXY header from the immediately upstream proxy and adopt 
> the advertised client address.
> 
> Motivation:
> - When relayd sits behind a TCP SNI demultiplexer that forwards the
>   raw stream to a local relayd for TLS termination, every session
>   appears to come from the loopback address.
> - The real client address is then lost for the REMOTE_ADDR macro /
>   X-Forwarded-For, for source-hash stickiness, and for logging.
> - The PROXY protocol is the standard remedy; relayd could emit it but
>   not consume it.
> 
> Patch design:
> - one new per-listener field, relay_config.proxyproto (the F_* flag
>   word is full, and a receive flag is orthogonal to the emit flags).
> - the header is read before the TLS handshake via MSG_PEEK, then
>   exactly its bytes are consumed with recv(2), leaving the following
>   ClientHello untouched.
> - a partial header reschedules on EV_READ under the existing session
>   timeout (no userland reassembly).
> - the version is auto-detected; a LOCAL/UNSPEC header keeps the
>   accept(2) peer.
> - refused together with a transparent forward (FWD_TRANS), otherwise
>   the advertised address would become a bindany source (spoofing).
> - as with any PROXY receiver, the listener must only be reachable
>   from a trusted upstream (documented in relayd.conf(5)).
> 
> Configuration:
>   listen on <addr> [tls] port <port> [proxy-protocol]
> 
> Testing (diff is against -current; built and run on both 7.9-release, 
> and -current, can send 7.9 patch if usefull to anyone):
> - builds with no new warnings.
> - "relayd -n" accepts the new keyword and rejects the FWD_TRANS
>   combination , on both.
> - a small unit harness around the parser (v1, v2, LOCAL, malformed
>   input, exact header consumption) passes.
> - verified end-to-end with sniproxy on 7.9 prepending a v2 header in front
>   of a TLS-terminating relayd: the backend sees the real client
>   address in X-Forwarded-For instead of the loopback address.
> 
> Feedback welcome, in particular on the naming and on whether a bare 
> "proxy-protocol" (auto-detected) reads well next to the versioned emit 
> directive.
> 
> Regards,
> Romain
> [2 relayd-proxyproto.diff <application/octet-stream (base64)>]

Thanks for a patch.

May I ask you to inline it or attach as 7bit plain/text? It will make review by quoting the patch simpler.

Neverless here first points after fast reading of your diff.

1. I think that multiple listen on directives silently inherit the first
   listener's proxy setting.

2. Seems that fragmented v2 message will be treated as v1.

3. Not fully sure, but looks like that an IPv6 client advertised over an
   IPv4 proxy connection produces an invalid mixed family v1 header and a
   corrupted v2 source.

4. I think we need regression tests for this.

--
wbr, Kirill