From: Kirill A. Korinsky Subject: Re: relayd: receive the PROXY protocol (v1 and v2) on listeners To: Romain FABBRI Cc: Rafael Sadowski , OpenBSD tech Date: Wed, 21 Oct 2026 21:14:53 +0200 On Tue, 29 Sep 2026 16:46:26 +0200, Romain FABBRI 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 context hostname interface table value path %type http_type loglevel quick %type dstmode flag forwardmode retry -%type opttls opttlsclient optproxyproto +%type opttls opttlsclient optproxyproto optproxyproto_in %type redirect_proto relay_proto match pflog %type action ruleaf key_option optpattern %type 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 +#include +#include + #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