Download raw body.
relayd: receive the PROXY protocol (v1 and v2) on listeners
On Tue, 29 Sep 2026 16:46:26 +0200,
Romain FABBRI <romain.fabbri@alienconsulting.net> wrote:
>
> Thanks for getting back to it. I've attached the diff directly this time
> 0001-relayd-proxy-protocol-receive.patch
>
> It already includes your last review round: the header is consumed
> incrementally with no event-loop spin, hooked in relay_accept ->
> relay_accepted, and proxy-protocol is refused on udp and with a
> transparent forward.
>
> Short version of what it does: it lets a relayd(8) listener *receive* a
> PROXY header (v1/v2) via a new "proxy-protocol" keyword on the listen line,
> so a relay sitting behind a raw-TCP upstream (e.g. an SNI demux that hands
> the stream to a local relayd for TLS termination) recovers the real client
> address for $REMOTE_ADDR / X-Forwarded-For, source-hash stickiness and
> logging. relayd can already emit a PROXY header but couldn't receive one.
>
> I rebased it on today's -current (8.0 snapshot #65): it applies with no
> fuzz, builds with no new warnings, links, and "relayd -n" is happy. The one
> hunk that had drifted was the F_NATLOOK proc_compose error check in relay.c,
> which the rebase keeps in the moved relay_accepted(). The unit harness for
> proxy_protocol_read() still passes on all cases (v1/v2, IPv6, LOCAL,
> malformed, partial-header anti-spin, byte-by-byte trickle).
>
> It still builds on 7.9 too :)
>
Thanks for the diff.
I think here two rest small things.
1. Logic of multiple listeners, specially when the later one enabled
proxy-protocol isn't clear. It smells buggy. It reads like it passes
validation but resulted state is not that is expected nor clear.
2. I think here a potential miss when relayd recivies and sends proxy
protocol header, seems it messed up destination port with source port.
Otherwise I think it is almost good to go.
Rafael?
Also, to save time to search for the diff I had re-inlined it:
relayd: receive the PROXY protocol (v1 and v2) on relay listeners
Adds reception of a PROXY protocol header on a relayd(8) listener so the
relay adopts the real client address advertised by the immediately upstream
proxy (e.g. an SNI demultiplexer forwarding the raw stream to a local relayd
for TLS termination), restoring $REMOTE_ADDR/X-Forwarded-For, source-hash
stickiness and logging.
Design: a per-listener field relay_config.proxyproto (the F_* flag word is
full). The header is consumed right after accept(2), before nat-lookup /
tls-inspect / session start (in relay_accept -> relay_accepted), so the
advertised address is in place for every path. proxy_protocol_read() reads
the header incrementally into a per-session buffer, consuming only its bytes
and never past its end, so the ClientHello that follows is left intact and a
partial header does not spin the event loop (the socket is drained; the read
resumes on EV_READ, bounded by the session timeout). The version is auto-
detected; both advertised endpoints are adopted so source and local name keep
the same family. proxy-protocol is refused with a transparent forward
(FWD_TRANS) and on udp relays; documented in relayd.conf(5).
Regression tests under regress/usr.sbin/relayd (v1 and v2).
--- regress/usr.sbin/relayd/Relayd.pm
+++ regress/usr.sbin/relayd/Relayd.pm
@@ -108,8 +108,9 @@
print $fh "\n\tprotocol proto-$test"
unless grep { /^protocol / } @relay;
my $tls = $self->{listenssl} ? " tls" : "";
+ my $proxy = $self->{listenproxyproto} ? " proxy-protocol" : "";
print $fh "\n\tlisten on $self->{listenaddr} ".
- "port $self->{listenport}$tls" unless grep { /^listen / } @relay;
+ "port $self->{listenport}$tls$proxy" unless grep { /^listen / } @relay;
my $withtls = $self->{forwardssl} ? " with tls" : "";
print $fh "\n\tforward$withtls to $self->{connectaddr} ".
"port $self->{connectport}" unless grep { /^forward / } @relay;
--- /dev/null
+++ regress/usr.sbin/relayd/args-proxy-protocol-filter.pl
@@ -0,0 +1,40 @@
+# test that a "from" filter rule matches the PROXY-advertised source
+
+use strict;
+use warnings;
+
+# The client prepends a PROXY v1 header advertising 192.0.2.1. The protocol
+# blocks every request but re-allows those coming "from 192.0.2.1/32". The
+# request therefore only reaches the backend if relay_test() evaluates the
+# address adopted from the PROXY header; had it kept the real loopback peer
+# 127.0.0.1, the pass rule would not match, the block would stand, and the
+# server would never see the connection. "log connection" also shows the
+# adopted source.
+
+our %args = (
+ client => {
+ func => sub {
+ my $self = shift;
+ print "PROXY TCP4 192.0.2.1 198.51.100.1 4321 80\r\n";
+ http_client($self);
+ },
+ },
+ relayd => {
+ listenproxyproto => 1,
+ protocol => [ "http",
+ "block request",
+ "pass request from 192.0.2.1/32",
+ ],
+ loggrep => {
+ qr/, 192\.0\.2\.1 -> / => 1,
+ qr/Forbidden/ => 0,
+ },
+ },
+ server => {
+ func => \&http_server,
+ },
+ len => 251,
+ md5 => "bc3a3f39af35fe5b1687903da2b00c7f",
+);
+
+1;
--- /dev/null
+++ regress/usr.sbin/relayd/args-proxy-protocol-v2.pl
@@ -0,0 +1,37 @@
+# test PROXY protocol version 2 header received on the listener
+
+use strict;
+use warnings;
+use Socket qw(inet_aton);
+
+# Same as the v1 test but with a binary v2 header (TCP over IPv4).
+
+our %args = (
+ client => {
+ func => sub {
+ my $self = shift;
+ my $sig = pack("C12", 0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D,
+ 0x0A, 0x51, 0x55, 0x49, 0x54, 0x0A);
+ my $hdr = $sig . pack("CCn", 0x21, 0x11, 12) .
+ inet_aton("192.0.2.1") . inet_aton("198.51.100.1") .
+ pack("nn", 4321, 80);
+ print $hdr;
+ http_client($self);
+ },
+ },
+ relayd => {
+ listenproxyproto => 1,
+ protocol => [ "http" ],
+ loggrep => {
+ qr/, 192\.0\.2\.1 -> / => 1,
+ qr/, 127\.0\.0\.1 -> / => 0,
+ },
+ },
+ server => {
+ func => \&http_server,
+ },
+ len => 251,
+ md5 => "bc3a3f39af35fe5b1687903da2b00c7f",
+);
+
+1;
--- /dev/null
+++ regress/usr.sbin/relayd/args-proxy-protocol.pl
@@ -0,0 +1,33 @@
+# test PROXY protocol version 1 header received on the listener
+
+use strict;
+use warnings;
+
+# The client prepends a PROXY protocol v1 header advertising a client address
+# that differs from the real loopback peer. With "log connection" the relay
+# logs the adopted source; check that it is the advertised address.
+
+our %args = (
+ client => {
+ func => sub {
+ my $self = shift;
+ print "PROXY TCP4 192.0.2.1 198.51.100.1 4321 80\r\n";
+ http_client($self);
+ },
+ },
+ relayd => {
+ listenproxyproto => 1,
+ protocol => [ "http" ],
+ loggrep => {
+ qr/, 192\.0\.2\.1 -> / => 1,
+ qr/, 127\.0\.0\.1 -> / => 0,
+ },
+ },
+ server => {
+ func => \&http_server,
+ },
+ len => 251,
+ md5 => "bc3a3f39af35fe5b1687903da2b00c7f",
+);
+
+1;
--- usr.sbin/relayd/parse.y
+++ usr.sbin/relayd/parse.y
@@ -202,7 +202,7 @@
%type <v.string> context hostname interface table value path
%type <v.number> http_type loglevel quick
%type <v.number> dstmode flag forwardmode retry
-%type <v.number> opttls opttlsclient optproxyproto
+%type <v.number> opttls opttlsclient optproxyproto optproxyproto_in
%type <v.number> redirect_proto relay_proto match pflog
%type <v.number> action ruleaf key_option optpattern
%type <v.port> port
@@ -1131,6 +1131,10 @@
| PROXYPROTO V2 { $$ = F_PROXYV2; }
;
+optproxyproto_in : /* empty */ { $$ = 0; }
+ | PROXYPROTO { $$ = 1; }
+ ;
+
protopts_n : /* empty */
| '{' '}'
| '{' optnl protopts_l '}'
@@ -2006,6 +2010,13 @@
"and peer options", rlay->rl_conf.name);
YYERROR;
}
+ if (rlay->rl_conf.proxyproto &&
+ rlay->rl_conf.fwdmode == FWD_TRANS) {
+ yyerror("relay %s cannot accept proxy-protocol "
+ "with a transparent forward",
+ rlay->rl_conf.name);
+ YYERROR;
+ }
if ((rlay->rl_conf.flags & (F_NATLOOK|F_DIVERT)) == 0 &&
rlay->rl_conf.dstss.ss_family == AF_UNSPEC &&
TAILQ_EMPTY(&rlay->rl_tables)) {
@@ -2018,6 +2029,13 @@
rlay->rl_conf.proto = conf->sc_proto_default.id;
}
+ if (rlay->rl_conf.proxyproto &&
+ rlay->rl_proto->type == RELAY_PROTO_DNS) {
+ yyerror("relay %s cannot accept proxy-protocol "
+ "on a udp relay", rlay->rl_conf.name);
+ YYERROR;
+ }
+
if (TAILQ_EMPTY(&rlay->rl_proto->tlscerts) &&
relay_load_certfiles(conf, rlay, NULL) == -1) {
yyerror("cannot load certificates for relay %s",
@@ -2054,7 +2072,7 @@
| relayoptsl optnl
;
-relayoptsl : LISTEN ON STRING port opttls {
+relayoptsl : LISTEN ON STRING port opttls optproxyproto_in {
struct addresslist al;
struct address *h;
struct relay *nr, *r;
@@ -2106,6 +2124,8 @@
nr->rl_conf.port = h->port.val[0];
if ($5)
nr->rl_conf.flags |= F_TLS;
+ if ($6)
+ nr->rl_conf.proxyproto = 1;
cnt++;
}
if ($5)
@@ -3638,6 +3658,7 @@
rb->rl_conf.port = rc.port;
rb->rl_conf.flags =
(ra->rl_conf.flags & ~F_TLS) | (rc.flags & F_TLS);
+ rb->rl_conf.proxyproto = rc.proxyproto;
if (!(rb->rl_conf.flags & F_TLS)) {
rb->rl_tls_cacert_fd = -1;
rb->rl_tls_ca_fd = -1;
--- usr.sbin/relayd/proxy_protocol.c
+++ usr.sbin/relayd/proxy_protocol.c
@@ -14,6 +14,10 @@
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
+#include <arpa/inet.h>
+#include <errno.h>
+#include <stdlib.h>
+
#include "relayd.h"
#define PROXY_V2_CMD_PROXY 0x01
@@ -142,3 +146,216 @@
return 0;
}
+
+static void
+proxy_addr4(struct sockaddr_storage *ss, in_addr_t addr, in_port_t port)
+{
+ struct sockaddr_in *sin = (struct sockaddr_in *)ss;
+
+ memset(sin, 0, sizeof(*sin));
+ sin->sin_family = AF_INET;
+ sin->sin_len = sizeof(*sin);
+ sin->sin_addr.s_addr = addr;
+ sin->sin_port = port;
+}
+
+static void
+proxy_addr6(struct sockaddr_storage *ss, const u_int8_t *addr, in_port_t port)
+{
+ struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *)ss;
+
+ memset(sin6, 0, sizeof(*sin6));
+ sin6->sin6_family = AF_INET6;
+ sin6->sin6_len = sizeof(*sin6);
+ memcpy(&sin6->sin6_addr, addr, sizeof(sin6->sin6_addr));
+ sin6->sin6_port = port;
+}
+
+/*
+ * Parse a textual (v1) PROXY line already delimited by CRLF within buf.
+ * Both endpoints are always the same family (TCP4 or TCP6), so src and dst
+ * are stored consistently. Returns the line length including CRLF, 0 if
+ * still incomplete, or (size_t)-1 on a malformed line. A UNKNOWN transport
+ * keeps the peers untouched.
+ */
+static size_t
+proxy_protocol_v1_parse(const u_int8_t *buf, size_t n,
+ struct sockaddr_storage *src, struct sockaddr_storage *dst)
+{
+ char line[PROXY_V1_MAX + 1];
+ char *fields[6], *p;
+ struct in_addr sa, da;
+ struct in6_addr sa6, da6;
+ const char *errstr;
+ size_t i, len = 0;
+ int nf = 0, af;
+ long long sport, dport;
+
+ for (i = 1; i < n && i <= PROXY_V1_MAX; i++) {
+ if (buf[i - 1] == '\r' && buf[i] == '\n') {
+ len = i + 1;
+ break;
+ }
+ }
+ if (len == 0)
+ return 0;
+
+ memcpy(line, buf, len - 2);
+ line[len - 2] = '\0';
+
+ fields[nf++] = line;
+ for (p = line; *p != '\0' && nf < 6; p++) {
+ if (*p == ' ') {
+ *p = '\0';
+ fields[nf++] = p + 1;
+ }
+ }
+ if (strcmp(fields[0], "PROXY") != 0)
+ return (size_t)-1;
+ if (nf == 2 && strcmp(fields[1], "UNKNOWN") == 0)
+ return len; /* keep the real peers */
+ if (nf != 6)
+ return (size_t)-1;
+
+ if (strcmp(fields[1], "TCP4") == 0)
+ af = AF_INET;
+ else if (strcmp(fields[1], "TCP6") == 0)
+ af = AF_INET6;
+ else
+ return (size_t)-1;
+
+ sport = strtonum(fields[4], 0, USHRT_MAX, &errstr);
+ if (errstr != NULL)
+ return (size_t)-1;
+ dport = strtonum(fields[5], 0, USHRT_MAX, &errstr);
+ if (errstr != NULL)
+ return (size_t)-1;
+
+ if (af == AF_INET) {
+ if (inet_pton(AF_INET, fields[2], &sa) != 1 ||
+ inet_pton(AF_INET, fields[3], &da) != 1)
+ return (size_t)-1;
+ proxy_addr4(src, sa.s_addr, htons((in_port_t)sport));
+ proxy_addr4(dst, da.s_addr, htons((in_port_t)dport));
+ } else {
+ if (inet_pton(AF_INET6, fields[2], &sa6) != 1 ||
+ inet_pton(AF_INET6, fields[3], &da6) != 1)
+ return (size_t)-1;
+ proxy_addr6(src, sa6.s6_addr, htons((in_port_t)sport));
+ proxy_addr6(dst, da6.s6_addr, htons((in_port_t)dport));
+ }
+ return len;
+}
+
+/*
+ * Given the OFF header bytes already accumulated in BUF, return how many more
+ * bytes may be consumed from the socket without reading past the end of the
+ * PROXY header (so the stream that follows is left untouched): a positive
+ * count to read next, 0 when the header is complete, or (size_t)-1 on a
+ * malformed header. v2 is length-delimited (16-byte fixed header then its
+ * address block); v1 is read one byte at a time up to the terminating CRLF.
+ */
+static size_t
+proxy_header_want(const u_int8_t *buf, size_t off)
+{
+ const struct proxy_v2_hdr *hdr;
+ size_t cmp, hlen;
+
+ if (off == 0)
+ return 1; /* need the first byte to pick v1/v2 */
+
+ cmp = off < sizeof(PROXY_V2_SIG) ? off : sizeof(PROXY_V2_SIG);
+ if (memcmp(buf, PROXY_V2_SIG, cmp) == 0) {
+ if (off < sizeof(struct proxy_v2_hdr))
+ return sizeof(struct proxy_v2_hdr) - off;
+ hdr = (const struct proxy_v2_hdr *)buf;
+ hlen = sizeof(struct proxy_v2_hdr) + ntohs(hdr->len);
+ if (hlen > PROXY_HEADER_MAX)
+ return (size_t)-1;
+ return off < hlen ? hlen - off : 0;
+ }
+
+ if (off >= 2 && buf[off - 2] == '\r' && buf[off - 1] == '\n')
+ return 0;
+ if (off >= PROXY_V1_MAX)
+ return (size_t)-1;
+ return 1;
+}
+
+/*
+ * Read and consume a PROXY protocol header (v1 or v2) from a client socket,
+ * before any TLS or application data. Bytes are consumed as they arrive and
+ * accumulated in buf (PROXY_HEADER_MAX, allocated by the caller and carried
+ * across calls through *off), never reading past the header, so the following
+ * stream is left intact -- and, since nothing readable is left unconsumed, a
+ * partial header does not spin the event loop. On success the advertised
+ * source and destination are stored in src and dst (both of the same family);
+ * a LOCAL/UNSPEC header leaves them unchanged. Returns 1 when a full header
+ * was consumed, 0 when more data is needed (call again on EV_READ), -1 on a
+ * bad header or a connection error.
+ */
+int
+proxy_protocol_read(int fd, u_int8_t *buf, size_t *off,
+ struct sockaddr_storage *src, struct sockaddr_storage *dst)
+{
+ const struct proxy_v2_hdr *hdr;
+ const union proxy_v2_addr *addr;
+ ssize_t n;
+ size_t want, alen, hlen;
+
+ for (;;) {
+ want = proxy_header_want(buf, *off);
+ if (want == (size_t)-1)
+ return -1;
+ if (want == 0)
+ break; /* full header accumulated */
+
+ n = recv(fd, buf + *off, want, 0);
+ if (n == -1) {
+ if (errno == EINTR)
+ continue;
+ if (errno == EAGAIN || errno == EWOULDBLOCK)
+ return 0; /* drained; wait for EV_READ */
+ return -1;
+ }
+ if (n == 0)
+ return -1; /* EOF before a full header */
+ *off += n;
+ }
+
+ if (*off >= sizeof(PROXY_V2_SIG) &&
+ memcmp(buf, PROXY_V2_SIG, sizeof(PROXY_V2_SIG)) == 0) {
+ hdr = (const struct proxy_v2_hdr *)buf;
+ if ((hdr->ver_cmd >> 4) != 0x2)
+ return -1;
+ if ((hdr->ver_cmd & 0x0f) != PROXY_V2_CMD_PROXY)
+ return 1; /* LOCAL: keep the real peers */
+ alen = ntohs(hdr->len);
+ addr = (const union proxy_v2_addr *)
+ (buf + sizeof(struct proxy_v2_hdr));
+ switch (hdr->fam) {
+ case PROXY_V2_FAM_TCP4:
+ if (alen < sizeof(addr->ipv4_addr))
+ return -1;
+ proxy_addr4(src, addr->ipv4_addr.src_addr,
+ addr->ipv4_addr.src_port);
+ proxy_addr4(dst, addr->ipv4_addr.dst_addr,
+ addr->ipv4_addr.dst_port);
+ break;
+ case PROXY_V2_FAM_TCP6:
+ if (alen < sizeof(addr->ipv6_addr))
+ return -1;
+ proxy_addr6(src, addr->ipv6_addr.src_addr,
+ addr->ipv6_addr.src_port);
+ proxy_addr6(dst, addr->ipv6_addr.dst_addr,
+ addr->ipv6_addr.dst_port);
+ break;
+ default:
+ break; /* UNSPEC/UDP: keep peers */
+ }
+ return 1;
+ }
+
+ hlen = proxy_protocol_v1_parse(buf, *off, src, dst);
+ return (hlen == 0 || hlen == (size_t)-1) ? -1 : 1;
+}
--- usr.sbin/relayd/relay.c
+++ usr.sbin/relayd/relay.c
@@ -70,6 +70,9 @@
void relay_accept(int, short, void *);
void relay_input(struct rsession *);
+void relay_proxy_read(struct rsession *);
+void relay_proxy_readcb(int, short, void *);
+void relay_accepted(struct rsession *);
void relay_hash_addr(SIPHASH_CTX *, struct sockaddr_storage *, int);
@@ -1108,15 +1111,70 @@
relay_close(con, strerror(errno), 1);
}
+/*
+ * Continue an accepted connection once its real endpoints are known:
+ * divert / nat-lookup / tls-inspect selection, then start the session.
+ */
+void
+relay_accepted(struct rsession *con)
+{
+ struct relay *rlay = con->se_relay;
+ struct privsep *ps = env->sc_ps;
+ struct ctl_natlook *cnl;
+ struct timeval tv;
+
+ if (rlay->rl_conf.flags & F_DIVERT) {
+ memcpy(&con->se_out.ss, &con->se_sockname,
+ sizeof(con->se_out.ss));
+ con->se_out.port = relay_socket_getport(&con->se_out.ss);
+
+ /* Detect loop and fall back to the alternate forward target */
+ if (bcmp(&rlay->rl_conf.ss, &con->se_out.ss,
+ sizeof(con->se_out.ss)) == 0 &&
+ con->se_out.port == rlay->rl_conf.port)
+ con->se_out.ss.ss_family = AF_UNSPEC;
+ } else if (rlay->rl_conf.flags & F_NATLOOK) {
+ if ((cnl = calloc(1, sizeof(*cnl))) == NULL) {
+ relay_close(con, "failed to allocate nat lookup", 1);
+ return;
+ }
+
+ con->se_cnl = cnl;
+ bzero(cnl, sizeof(*cnl));
+ cnl->in = -1;
+ cnl->id = con->se_id;
+ cnl->proc = ps->ps_instance;
+ cnl->proto = IPPROTO_TCP;
+
+ memcpy(&cnl->src, &con->se_in.ss, sizeof(cnl->src));
+ memcpy(&cnl->dst, &con->se_sockname, sizeof(cnl->dst));
+
+ if (proc_compose(env->sc_ps, PROC_PFE, IMSG_NATLOOK,
+ cnl, sizeof(*cnl)) == -1)
+ log_warn("%s: proc_compose", __func__);
+
+ /* Schedule timeout */
+ evtimer_set(&con->se_ev, relay_natlook, con);
+ bcopy(&rlay->rl_conf.timeout, &tv, sizeof(tv));
+ evtimer_add(&con->se_ev, &tv);
+ return;
+ }
+
+ if (rlay->rl_conf.flags & F_TLSINSPECT) {
+ relay_preconnect(con);
+ return;
+ }
+
+ relay_session(con);
+}
+
void
relay_accept(int fd, short event, void *arg)
{
struct privsep *ps = env->sc_ps;
struct relay *rlay = arg;
struct rsession *con = NULL;
- struct ctl_natlook *cnl = NULL;
socklen_t slen;
- struct timeval tv;
struct sockaddr_storage ss;
int s = -1;
@@ -1211,49 +1269,16 @@
return;
}
- if (rlay->rl_conf.flags & F_DIVERT) {
- memcpy(&con->se_out.ss, &con->se_sockname,
- sizeof(con->se_out.ss));
- con->se_out.port = relay_socket_getport(&con->se_out.ss);
-
- /* Detect loop and fall back to the alternate forward target */
- if (bcmp(&rlay->rl_conf.ss, &con->se_out.ss,
- sizeof(con->se_out.ss)) == 0 &&
- con->se_out.port == rlay->rl_conf.port)
- con->se_out.ss.ss_family = AF_UNSPEC;
- } else if (rlay->rl_conf.flags & F_NATLOOK) {
- if ((cnl = calloc(1, sizeof(*cnl))) == NULL) {
- relay_close(con, "failed to allocate nat lookup", 1);
+ if (rlay->rl_conf.proxyproto) {
+ if ((con->se_proxybuf = calloc(1, PROXY_HEADER_MAX)) == NULL) {
+ relay_close(con, "failed to allocate proxy buffer", 1);
return;
}
-
- con->se_cnl = cnl;
- bzero(cnl, sizeof(*cnl));
- cnl->in = -1;
- cnl->id = con->se_id;
- cnl->proc = ps->ps_instance;
- cnl->proto = IPPROTO_TCP;
-
- memcpy(&cnl->src, &con->se_in.ss, sizeof(cnl->src));
- memcpy(&cnl->dst, &con->se_sockname, sizeof(cnl->dst));
-
- if (proc_compose(env->sc_ps, PROC_PFE, IMSG_NATLOOK,
- cnl, sizeof(*cnl)) == -1)
- log_warn("%s: proc_compose", __func__);
-
- /* Schedule timeout */
- evtimer_set(&con->se_ev, relay_natlook, con);
- bcopy(&rlay->rl_conf.timeout, &tv, sizeof(tv));
- evtimer_add(&con->se_ev, &tv);
+ relay_proxy_read(con);
return;
}
- if (rlay->rl_conf.flags & F_TLSINSPECT) {
- relay_preconnect(con);
- return;
- }
-
- relay_session(con);
+ relay_accepted(con);
return;
err:
if (s != -1) {
@@ -1443,6 +1468,53 @@
}
void
+relay_proxy_read(struct rsession *con)
+{
+ struct relay *rlay = con->se_relay;
+
+ event_again(&con->se_ev, con->se_in.s, EV_TIMEOUT|EV_READ,
+ relay_proxy_readcb, &con->se_tv_start, &rlay->rl_conf.timeout, con);
+}
+
+void
+relay_proxy_readcb(int fd, short event, void *arg)
+{
+ struct rsession *con = arg;
+ struct sockaddr_storage src, dst;
+ int ret;
+
+ if (event & EV_TIMEOUT) {
+ relay_close(con, "PROXY protocol header timeout", 1);
+ return;
+ }
+
+ /* Seed with the real endpoints so a LOCAL/UNSPEC header is a no-op. */
+ src = con->se_in.ss;
+ dst = con->se_sockname;
+
+ ret = proxy_protocol_read(fd, con->se_proxybuf, &con->se_proxyoff,
+ &src, &dst);
+ if (ret == 0) { /* header not complete yet */
+ relay_proxy_read(con);
+ return;
+ }
+ if (ret == -1) {
+ relay_close(con, "invalid PROXY protocol header", 1);
+ return;
+ }
+
+ /*
+ * Adopt both advertised endpoints so the source and the local name
+ * stay in the same address family (a mismatched family would corrupt
+ * a PROXY header re-emitted towards the backend).
+ */
+ con->se_in.ss = src;
+ con->se_in.port = relay_socket_getport(&src);
+ con->se_sockname = dst;
+ relay_accepted(con);
+}
+
+void
relay_session(struct rsession *con)
{
struct relay *rlay = con->se_relay;
@@ -1831,6 +1903,7 @@
free(con->se_cnl);
}
+ free(con->se_proxybuf);
free(con);
relay_sessions--;
}
--- usr.sbin/relayd/relayd.conf.5
+++ usr.sbin/relayd/relayd.conf.5
@@ -731,6 +731,7 @@
.It Xo
.Ic listen on Ar address Ic port Ar port
.Op Ic tls
+.Op Ic proxy-protocol
.Xc
Specify the address and port for the relay to listen on.
The relay will accept incoming connections to the specified address or
@@ -750,6 +751,25 @@
.Ic tls
keyword is present, the relay will accept connections using the
encrypted TLS protocol.
+.Pp
+If the
+.Ic proxy-protocol
+keyword is present, the relay expects each connection to begin with a
+PROXY protocol header (version 1 or 2, automatically detected) sent by the
+immediately upstream proxy, and adopts the advertised client address and
+port for source-based session distribution, the
+.Va REMOTE_ADDR
+macro, and logging.
+The header is consumed before the optional TLS handshake.
+Because the header is trusted, a listener using
+.Ic proxy-protocol
+must only be reachable from a trusted upstream: bind it to a private
+interface or the loopback address and restrict access with
+.Xr pf.conf 5 .
+This keyword may not be combined with a transparent
+.Ic forward to
+destination.
+It is not available for UDP relays.
.It Ic protocol Ar name
Use the specified protocol definition for the relay.
The generic TCP protocol options will be used by default;
--- usr.sbin/relayd/relayd.h
+++ usr.sbin/relayd/relayd.h
@@ -81,6 +81,9 @@
#define RELAY_MAX_HASH_RETRIES 5
#define RELAY_TLS_PRIV_TIMEOUT 1000 /* wait 1sec for the ca */
+#define PROXY_V1_MAX 107 /* v1 line incl. CRLF, per spec */
+#define PROXY_HEADER_MAX 536 /* v2 fixed header + address block */
+
#define CONFIG_RELOAD 0x00
#define CONFIG_TABLES 0x01
#define CONFIG_RDRS 0x02
@@ -581,6 +584,8 @@
struct timeval se_tv_last;
struct event se_inflightevt;
int se_done;
+ u_int8_t *se_proxybuf; /* PROXY header accumulator */
+ size_t se_proxyoff; /* PROXY bytes accumulated */
int se_retry;
int se_retrycount;
int se_connectcount;
@@ -819,6 +824,7 @@
enum forwardmode fwdmode;
union hashkey hashkey;
off_t tls_cakey_len;
+ int proxyproto; /* expect PROXY header from client */
};
struct relay {
@@ -1459,5 +1465,7 @@
/* proxy_protocol.c */
int proxy_protocol_v1(struct rsession *, struct evbuffer *);
int proxy_protocol_v2(struct rsession *, struct evbuffer *);
+int proxy_protocol_read(int, u_int8_t *, size_t *,
+ struct sockaddr_storage *, struct sockaddr_storage *);
#endif /* RELAYD_H */
--
wbr, Kirill
relayd: receive the PROXY protocol (v1 and v2) on listeners