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