From: Christoph Liebender Subject: Re: relayd: add support for PROXY protocol in TCP relays To: tech@openbsd.org, reyk@openbsd.org Date: Sun, 16 Nov 2025 19:31:10 +0100 On 11/12/25 07:45, Christoph Liebender wrote: > On 11/12/25 00:24, Kirill A. Korinsky wrote: >> >> 5. Not sure why have you limited proxy protocol only for TCP >> connection. It >>     can be used for anything, am I wrong? >> > Thats up for debate... For v1, I say there is no use for proxy protocol > over non-tcp connections, as the header is just PROXY UNKNOWN\r\n. For > v2, it is true that UDP is supported, but I do not know about any > application server that I can test this with. For HTTP relay, it might > make more sense to use X-Real-IP or X-Forwarded-For? > Here goes a patch with support for PROXY V2 over UDP - albeit untested. comments, ok? diff --git a/usr.sbin/relayd/Makefile b/usr.sbin/relayd/Makefile index 2e1ef449b04..5aacf84fc87 100644 --- a/usr.sbin/relayd/Makefile +++ b/usr.sbin/relayd/Makefile @@ -4,8 +4,9 @@ PROG= relayd SRCS= parse.y SRCS+= agentx_control.c ca.c carp.c check_icmp.c check_script.c \ check_tcp.c check_tls.c config.c control.c hce.c log.c \ - name2id.c pfe.c pfe_filter.c pfe_route.c proc.c relay.c \ - relay_http.c relay_udp.c relayd.c shuffle.c ssl.c util.c + name2id.c pfe.c pfe_filter.c pfe_route.c proc.c \ + proxy_protocol.c relay.c relay_http.c relay_udp.c relayd.c \ + shuffle.c ssl.c util.c MAN= relayd.8 relayd.conf.5 LDADD= -lagentx -levent -ltls -lssl -lcrypto -lutil diff --git a/usr.sbin/relayd/parse.y b/usr.sbin/relayd/parse.y index fcdfb8e92e3..26e959800cd 100644 --- a/usr.sbin/relayd/parse.y +++ b/usr.sbin/relayd/parse.y @@ -1,6 +1,7 @@ /* $OpenBSD: parse.y,v 1.258 2024/10/28 19:56:18 tb Exp $ */ /* + * Copyright (c) 2025 Christoph Liebender * Copyright (c) 2007 - 2014 Reyk Floeter * Copyright (c) 2008 Gilles Chehade * Copyright (c) 2006 Pierre-Yves Ritschard @@ -140,6 +141,9 @@ struct relay *relay_inherit(struct relay *, struct relay *); int getservice(char *); int is_if_in_group(const char *, const char *); +int is_proxy_protocol_allowed(const struct protocol *, + u_int32_t); + typedef struct { union { int64_t number; @@ -179,13 +183,13 @@ typedef struct { %token TIMEOUT TLS TO ROUTER RTLABEL TRANSPARENT URL WITH TTL RTABLE %token MATCH PARAMS RANDOM LEASTSTATES SRCHASH KEY CERTIFICATE PASSWORD ECDHE %token EDH TICKETS CONNECTION CONNECTIONS CONTEXT ERRORS STATE CHANGES CHECKS -%token WEBSOCKETS PFLOG CLIENT +%token WEBSOCKETS PFLOG CLIENT PROXYPROTO V1 V2 %token STRING %token NUMBER %type context hostname interface table value path %type http_type loglevel quick %type dstmode flag forwardmode retry -%type opttls opttlsclient +%type opttls opttlsclient optproxyproto %type redirect_proto relay_proto match pflog %type action ruleaf key_option %type port @@ -1103,6 +1107,11 @@ proto : relay_proto PROTO STRING { } ; +optproxyproto : /* empty */ { $$ = 0; } + | PROXYPROTO V1 { $$ = F_PROXYV1; } + | PROXYPROTO V2 { $$ = F_PROXYV2; } + ; + protopts_n : /* empty */ | '{' '}' | '{' optnl protopts_l '}' @@ -1946,7 +1955,7 @@ relayoptsl : LISTEN ON STRING port opttls { tableport = h->port.val[0]; host_free(&al); } - | forwardmode opttlsclient TO forwardspec dstaf { + | forwardmode opttlsclient TO forwardspec dstaf optproxyproto { rlay->rl_conf.fwdmode = $1; if ($1 == FWD_ROUTE) { yyerror("no route for relays"); @@ -1956,6 +1965,14 @@ relayoptsl : LISTEN ON STRING port opttls { rlay->rl_conf.flags |= F_TLSCLIENT; conf->sc_conf.flags |= F_TLSCLIENT; } + + if (!is_proxy_protocol_allowed(rlay->rl_proto, $6)) { + yyerror("invalid proxy-protocol " + "for relay protocol"); + YYERROR; + } + + rlay->rl_conf.flags |= $6; } | SESSION TIMEOUT NUMBER { if ((rlay->rl_conf.timeout.tv_sec = $3) < 0) { @@ -1983,6 +2000,14 @@ relayoptsl : LISTEN ON STRING port opttls { free($2); YYERROR; } + + if (!is_proxy_protocol_allowed(p, + rlay->rl_conf.flags)) { + yyerror("invalid proxy-protocol " + "for relay protocol"); + YYERROR; + } + p->flags |= F_USED; rlay->rl_conf.proto = p->id; rlay->rl_proto = p; @@ -2479,6 +2504,7 @@ lookup(char *s) { "prefork", PREFORK }, { "priority", PRIORITY }, { "protocol", PROTO }, + { "proxy-protocol", PROXYPROTO }, { "query", QUERYSTR }, { "quick", QUICK }, { "random", RANDOM }, @@ -2518,6 +2544,8 @@ lookup(char *s) { "transparent", TRANSPARENT }, { "ttl", TTL }, { "url", URL }, + { "v1", V1 }, + { "v2", V2 }, { "value", VALUE }, { "websockets", WEBSOCKETS }, { "with", WITH } @@ -3530,3 +3558,25 @@ end: close(s); return (ret); } + +int +is_proxy_protocol_allowed(const struct protocol *p, u_int32_t flags) +{ + if (!p) + return 1; + + switch (p->type) { + case RELAY_PROTO_HTTP: + if (flags & (F_PROXYV1|F_PROXYV2)) + return 0; + break; + case RELAY_PROTO_DNS: + if (flags & F_PROXYV1) + return 0; + break; + default: + break; + } + + return 1; +} diff --git a/usr.sbin/relayd/proxy_protocol.c b/usr.sbin/relayd/proxy_protocol.c new file mode 100644 index 00000000000..e968bc7b334 --- /dev/null +++ b/usr.sbin/relayd/proxy_protocol.c @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2025 Christoph Liebender + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include "relayd.h" + +#define PROXY_V2_CMD_PROXY 0x01 + +#define PROXY_V2_FAM_UNSPEC 0x00 +#define PROXY_V2_FAM_TCP4 0x11 +#define PROXY_V2_FAM_UDP4 0x12 +#define PROXY_V2_FAM_TCP6 0x21 +#define PROXY_V2_FAM_UDP6 0x22 + +static const u_int8_t PROXY_V2_SIG[12] = { + 0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54, 0x0A +}; + +struct proxy_v2_hdr { + u_int8_t sig[12]; + u_int8_t ver_cmd; + u_int8_t fam; + u_int16_t len; +}; + +union proxy_v2_addr { + struct { + u_int32_t src_addr; + u_int32_t dst_addr; + in_port_t src_port; + in_port_t dst_port; + } ipv4_addr; + struct { + u_int8_t src_addr[16]; + u_int8_t dst_addr[16]; + in_port_t src_port; + in_port_t dst_port; + } ipv6_addr; +}; + +int +proxy_protocol_v1(const struct rsession *con, struct evbuffer *dstout) +{ + char ibuf[128], obuf[128]; + const char *proxyproto; + int ret; + + bzero(&ibuf, sizeof(ibuf)); + bzero(&obuf, sizeof(obuf)); + print_host(&con->se_in.ss, ibuf, sizeof(ibuf)); + print_host(&con->se_relay->rl_conf.ss, obuf, sizeof(obuf)); + + switch (con->se_in.ss.ss_family) { + case AF_INET: + proxyproto = "TCP4"; + break; + case AF_INET6: + proxyproto = "TCP6"; + break; + default: + proxyproto = "UNKNOWN"; + break; + } + + ret = evbuffer_add_printf(dstout, + "PROXY %s %s %s %d %d\r\n", proxyproto, ibuf, obuf, + ntohs(con->se_in.port), ntohs(con->se_relay->rl_conf.port)); + + return ret == -1 ? -1 : 0; +} + +int +proxy_protocol_v2(const struct rsession *con, struct evbuffer *dstout) +{ + union proxy_v2_addr addr; + struct proxy_v2_hdr hdr; + const struct relay_config *conf = &con->se_relay->rl_conf; + const struct sockaddr_storage *srcss = &con->se_in.ss; + const struct sockaddr_storage *dstss = &conf->ss; + int error; + in_port_t srcport = con->se_in.port; + in_port_t dstport = conf->port; + u_int16_t len; + + bcopy(PROXY_V2_SIG, hdr.sig, sizeof(hdr.sig)); + hdr.ver_cmd = 0x20 | PROXY_V2_CMD_PROXY; + + switch (dstss->ss_family) { + case AF_INET: + hdr.fam = (conf->flags & F_UDP) ? + PROXY_V2_FAM_UDP4 : PROXY_V2_FAM_TCP4; + len = sizeof(addr.ipv4_addr); + addr.ipv4_addr.src_addr = + ((const struct sockaddr_in *)srcss)->sin_addr.s_addr; + addr.ipv4_addr.dst_addr = + ((const struct sockaddr_in *)dstss)->sin_addr.s_addr; + addr.ipv4_addr.src_port = srcport; + addr.ipv4_addr.dst_port = dstport; + break; + case AF_INET6: + hdr.fam = (conf->flags & F_UDP) ? + PROXY_V2_FAM_UDP6 : PROXY_V2_FAM_TCP6; + len = sizeof(addr.ipv6_addr); + bcopy(&((const struct sockaddr_in6 *)srcss)->sin6_addr, + addr.ipv6_addr.src_addr, sizeof(addr.ipv6_addr.src_addr)); + bcopy(&((const struct sockaddr_in6 *)dstss)->sin6_addr, + addr.ipv6_addr.dst_addr, sizeof(addr.ipv6_addr.dst_addr)); + addr.ipv6_addr.src_port = srcport; + addr.ipv6_addr.dst_port = dstport; + break; + default: + hdr.fam = 0x00; + len = 0; + break; + } + + hdr.len = htons(len); + + if ((error = evbuffer_add(dstout, &hdr, sizeof(hdr))) != 0) + return error; + + if (len > 0 && (error = evbuffer_add(dstout, &addr, len)) != 0) + return error; + + return 0; +} diff --git a/usr.sbin/relayd/relay.c b/usr.sbin/relayd/relay.c index 6d0970802c5..72260e92896 100644 --- a/usr.sbin/relayd/relay.c +++ b/usr.sbin/relayd/relay.c @@ -1,6 +1,7 @@ /* $OpenBSD: relay.c,v 1.260 2024/10/28 19:56:18 tb Exp $ */ /* + * Copyright (c) 2025 Christoph Liebender * Copyright (c) 2006 - 2014 Reyk Floeter * * Permission to use, copy, modify, and distribute this software for any @@ -767,6 +768,21 @@ relay_connected(int fd, short sig, void *arg) "failed to allocate output buffer event", 0); return; } + + error = 0; + + if (rlay->rl_proto->type == RELAY_PROTO_TCP && + (rlay->rl_conf.flags & F_PROXYV1)) + error = proxy_protocol_v1(con, bev->output); + else if (rlay->rl_proto->type != RELAY_PROTO_HTTP && + (rlay->rl_conf.flags & F_PROXYV2)) + error = proxy_protocol_v2(con, bev->output); + + if (error) { + relay_abort_http(con, 500, "failed to write PROXY header", 0); + return; + } + /* write pending output buffer now */ if (bufferevent_write_buffer(bev, con->se_out.output)) { relay_abort_http(con, 500, strerror(errno), 0); diff --git a/usr.sbin/relayd/relayd.conf.5 b/usr.sbin/relayd/relayd.conf.5 index 29a13efbe8a..6f3a35c0fca 100644 --- a/usr.sbin/relayd/relayd.conf.5 +++ b/usr.sbin/relayd/relayd.conf.5 @@ -1,5 +1,6 @@ .\" $OpenBSD: relayd.conf.5,v 1.213 2025/07/08 14:26:45 schwarze Exp $ .\" +.\" Copyright (c) 2025 Christoph Liebender .\" Copyright (c) 2006 - 2016 Reyk Floeter .\" Copyright (c) 2006, 2007 Pierre-Yves Ritschard .\" @@ -673,6 +674,13 @@ option will be used as a tolerance for failed host connections; the connection will be retried for .Ar number more times. +.It Ic proxy-protocol Pq Ic v1 Ns | Ns Ic v2 +Upon connection to the destination, +.Xr relayd 8 +will prepend a PROXY protocol header of the specified version to the relayed +data, with the IP address and port of the remote host as well as the +.Xr relayd 8 +server. .El .It Xo .Ic forward to diff --git a/usr.sbin/relayd/relayd.h b/usr.sbin/relayd/relayd.h index 3b5c3987f93..4fca5b5329f 100644 --- a/usr.sbin/relayd/relayd.h +++ b/usr.sbin/relayd/relayd.h @@ -1,6 +1,7 @@ /* $OpenBSD: relayd.h,v 1.276 2024/10/28 19:56:18 tb Exp $ */ /* + * Copyright (c) 2025 Christoph Liebender * Copyright (c) 2006 - 2016 Reyk Floeter * Copyright (c) 2006, 2007 Pierre-Yves Ritschard * Copyright (c) 2003, 2004 Henning Brauer @@ -402,6 +403,8 @@ union hashkey { #define F_HASHKEY 0x08000000 #define F_AGENTX_TRAPONLY 0x10000000 #define F_PFLOG 0x20000000 +#define F_PROXYV1 0x40000000 +#define F_PROXYV2 0x80000000 #define F_BITS \ "\10\01DISABLE\02BACKUP\03USED\04DOWN\05ADD\06DEL\07CHANGED" \ @@ -1166,7 +1169,7 @@ const char *table_check(enum table_check); const char *relay_state(enum relay_state); #endif const char *print_availability(u_long, u_long); -const char *print_host(struct sockaddr_storage *, char *, size_t); +const char *print_host(const struct sockaddr_storage *, char *, size_t); const char *print_time(struct timeval *, struct timeval *, char *, size_t); const char *printb_flags(const u_int32_t, const char *); void getmonotime(struct timeval *); @@ -1476,4 +1479,8 @@ int config_getrelay(struct relayd *, struct imsg *); int config_getrelaytable(struct relayd *, struct imsg *); int config_getrelayfd(struct relayd *, struct imsg *); +/* proxy_protocol.c */ +int proxy_protocol_v1(const struct rsession *, struct evbuffer *); +int proxy_protocol_v2(const struct rsession *, struct evbuffer *); + #endif /* RELAYD_H */ diff --git a/usr.sbin/relayd/util.c b/usr.sbin/relayd/util.c index 751e82c43c8..c82d254821f 100644 --- a/usr.sbin/relayd/util.c +++ b/usr.sbin/relayd/util.c @@ -214,9 +214,9 @@ print_availability(u_long cnt, u_long up) } const char * -print_host(struct sockaddr_storage *ss, char *buf, size_t len) +print_host(const struct sockaddr_storage *ss, char *buf, size_t len) { - if (getnameinfo((struct sockaddr *)ss, ss->ss_len, + if (getnameinfo((const struct sockaddr *)ss, ss->ss_len, buf, len, NULL, 0, NI_NUMERICHOST) != 0) { buf[0] = '\0'; return (NULL);