--- usr.sbin/relayd/relayd.h 2026-07-13 18:22:20.945615794 +0200 +++ usr.sbin/relayd/relayd.h 2026-07-13 18:22:20.949644934 +0200 @@ -577,6 +577,7 @@ struct timeval se_tv_last; struct event se_inflightevt; int se_done; + int se_proxydone; /* PROXY header consumed */ int se_retry; int se_retrycount; int se_connectcount; @@ -815,6 +816,7 @@ enum forwardmode fwdmode; union hashkey hashkey; off_t tls_cakey_len; + int proxyproto; /* expect PROXY header from client */ }; struct relay { @@ -1459,5 +1461,6 @@ /* proxy_protocol.c */ int proxy_protocol_v1(struct rsession *, struct evbuffer *); int proxy_protocol_v2(struct rsession *, struct evbuffer *); +int proxy_protocol_read(int, struct sockaddr_storage *, in_port_t *); #endif /* RELAYD_H */ --- usr.sbin/relayd/parse.y 2026-07-13 18:22:20.933105130 +0200 +++ usr.sbin/relayd/parse.y 2026-07-13 18:23:02.927923187 +0200 @@ -189,7 +189,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 %type port @@ -1112,6 +1112,10 @@ | PROXYPROTO V2 { $$ = F_PROXYV2; } ; +optproxyproto_in : /* empty */ { $$ = 0; } + | PROXYPROTO { $$ = 1; } + ; + protopts_n : /* empty */ | '{' '}' | '{' optnl protopts_l '}' @@ -1946,6 +1950,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)) { @@ -1994,7 +2005,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; @@ -2046,6 +2057,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) --- usr.sbin/relayd/relay.c 2026-07-13 18:22:20.939135740 +0200 +++ usr.sbin/relayd/relay.c 2026-07-13 18:22:20.950318844 +0200 @@ -70,6 +70,8 @@ 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_hash_addr(SIPHASH_CTX *, struct sockaddr_storage *, int); @@ -1438,11 +1440,63 @@ } 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 ss; + in_port_t port; + int ret; + + if (event & EV_TIMEOUT) { + relay_close(con, "PROXY header timeout", 1); + return; + } + + /* Seed with the real peer so a LOCAL/UNSPEC header is a no-op. */ + ss = con->se_in.ss; + port = con->se_in.port; + + ret = proxy_protocol_read(fd, &ss, &port); + if (ret == 0) { /* header not complete yet */ + relay_proxy_read(con); + return; + } + if (ret == -1) { + relay_close(con, "invalid PROXY header", 1); + return; + } + + con->se_in.ss = ss; + con->se_in.port = port; + con->se_proxydone = 1; + relay_session(con); +} + +void relay_session(struct rsession *con) { struct relay *rlay = con->se_relay; struct ctl_relay_event *in = &con->se_in, *out = &con->se_out; + /* + * Consume the PROXY protocol header (if the listener expects one) + * before anything reads the socket, so the advertised client address + * replaces the accept(2) peer for hashing, $REMOTE_ADDR and logging. + */ + if (rlay->rl_conf.proxyproto && !con->se_proxydone) { + relay_proxy_read(con); + return; + } + if (bcmp(&rlay->rl_conf.ss, &out->ss, sizeof(out->ss)) == 0 && out->port == rlay->rl_conf.port) { log_debug("%s: session %d: looping", __func__, con->se_id); --- usr.sbin/relayd/proxy_protocol.c 2026-07-13 18:22:20.936194813 +0200 +++ usr.sbin/relayd/proxy_protocol.c 2026-07-13 18:22:20.951323846 +0200 @@ -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,164 @@ return 0; } + +#define PROXY_V1_MAX 107 /* incl. CRLF, per HAProxy spec */ +#define PROXY_HEADER_MAX 536 /* v2 header + address block (+ TLVs) */ + +/* + * Parse a textual (v1) PROXY line already delimited by CRLF within buf. + * Returns the line length including CRLF, 0 if still incomplete, or + * (size_t)-1 on a malformed line. A UNKNOWN transport keeps the peer. + */ +static size_t +proxy_protocol_v1_parse(const u_int8_t *buf, size_t n, + struct sockaddr_storage *ss, in_port_t *port) +{ + char line[PROXY_V1_MAX + 1]; + char *fields[6], *p; + struct sockaddr_in *sin; + struct sockaddr_in6 *sin6; + const char *errstr; + size_t i, len = 0; + int nf = 0, af; + long long srcport; + + 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 peer */ + 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; + + srcport = strtonum(fields[4], 1, USHRT_MAX, &errstr); + if (errstr != NULL) + return (size_t)-1; + + if (af == AF_INET) { + sin = (struct sockaddr_in *)ss; + memset(sin, 0, sizeof(*sin)); + sin->sin_family = AF_INET; + sin->sin_len = sizeof(*sin); + if (inet_pton(AF_INET, fields[2], &sin->sin_addr) != 1) + return (size_t)-1; + } else { + sin6 = (struct sockaddr_in6 *)ss; + memset(sin6, 0, sizeof(*sin6)); + sin6->sin6_family = AF_INET6; + sin6->sin6_len = sizeof(*sin6); + if (inet_pton(AF_INET6, fields[2], &sin6->sin6_addr) != 1) + return (size_t)-1; + } + *port = htons((in_port_t)srcport); + return len; +} + +/* + * Read and consume a PROXY protocol header (v1 or v2) from a client socket, + * before any TLS or application data. The header is peeked first so exactly + * its bytes are removed, leaving the following stream intact. On success the + * real client address and port are stored in ss and port (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. + */ +int +proxy_protocol_read(int fd, struct sockaddr_storage *ss, in_port_t *port) +{ + u_int8_t buf[PROXY_HEADER_MAX]; + struct proxy_v2_hdr *hdr; + union proxy_v2_addr *addr; + struct sockaddr_in *sin; + struct sockaddr_in6 *sin6; + ssize_t n; + size_t hlen, alen; + + n = recv(fd, buf, sizeof(buf), MSG_PEEK); + if (n == -1) + return (errno == EAGAIN || errno == EWOULDBLOCK) ? 0 : -1; + if (n == 0) + return -1; + + if ((size_t)n >= sizeof(PROXY_V2_SIG) && + memcmp(buf, PROXY_V2_SIG, sizeof(PROXY_V2_SIG)) == 0) { + if ((size_t)n < sizeof(struct proxy_v2_hdr)) + return 0; + hdr = (struct proxy_v2_hdr *)buf; + if ((hdr->ver_cmd >> 4) != 0x2) + return -1; + alen = ntohs(hdr->len); + hlen = sizeof(struct proxy_v2_hdr) + alen; + if (hlen > sizeof(buf)) + return -1; + if ((size_t)n < hlen) + return 0; + + if ((hdr->ver_cmd & 0x0f) == PROXY_V2_CMD_PROXY) { + addr = (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; + 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->ipv4_addr.src_addr; + *port = addr->ipv4_addr.src_port; + break; + case PROXY_V2_FAM_TCP6: + if (alen < sizeof(addr->ipv6_addr)) + return -1; + 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->ipv6_addr.src_addr, + sizeof(sin6->sin6_addr)); + *port = addr->ipv6_addr.src_port; + break; + default: + break; /* UNSPEC/UDP: keep peer */ + } + } + } else { + hlen = proxy_protocol_v1_parse(buf, n, ss, port); + if (hlen == 0) + return ((size_t)n >= PROXY_V1_MAX) ? -1 : 0; + if (hlen == (size_t)-1) + return -1; + } + + while ((n = recv(fd, buf, hlen, 0)) == -1 && errno == EINTR) + ; + if (n != (ssize_t)hlen) + return -1; + + return 1; +} --- usr.sbin/relayd/relayd.conf.5 2026-07-13 18:22:20.942455987 +0200 +++ usr.sbin/relayd/relayd.conf.5 2026-07-13 18:22:20.951757770 +0200 @@ -725,6 +725,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 @@ -744,6 +745,24 @@ .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 Ic protocol Ar name Use the specified protocol definition for the relay. The generic TCP protocol options will be used by default;