Index | Thread | Search

From:
Christoph Liebender <christoph@liebender.dev>
Subject:
Re: relayd: add support for PROXY protocol in TCP relays
To:
tech@openbsd.org, reyk@openbsd.org
Date:
Tue, 18 Nov 2025 20:55:56 +0100

Download raw body.

Thread
On 11/18/25 00:14, Kirill A. Korinsky wrote:
> On Sun, 16 Nov 2025 19:31:10 +0100,
> Christoph Liebender <christoph@liebender.dev> wrote:
> 
> when XX was v1 and base on tcpdump relayd sends a line:
> 
> PROXY TCP4 127.0.0.1 0.0.0.0 20042 8080
> 
> I don't think that 0.0.0.0 is the right IP here.

Thanks. Fixed.

> I haven't read the structure but QUIT brings a bell. I had send a few
> requests and all of them contains this QUIT.

I'm sorry for ringing your bell, but that is actually part of the 
protocol. Some bytes of the header are QUIT in ASCII.

> 
> Comments in the code:
> 
>> +
>> +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;
>> +}
> 
> It isn't clear for me why you had difference logic for HTTP and DNS.

I made an argument one before my previous email. But if you insist... 
Attached is a version with the fixes and without any config 
sanitization. It also allows v1 over UDP, and v1/v2 over HTTP. So your 
http protocol xff will work.

> 
>> +#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
> 
> looks like poor ident
> 

Fixed.

Thanks for taking a look.


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..8fc2016246e 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 <christoph@liebender.dev>
  * Copyright (c) 2007 - 2014 Reyk Floeter <reyk@openbsd.org>
  * Copyright (c) 2008 Gilles Chehade <gilles@openbsd.org>
  * Copyright (c) 2006 Pierre-Yves Ritschard <pyr@openbsd.org>
@@ -179,13 +180,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	<v.string>	STRING
 %token  <v.number>	NUMBER
 %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
+%type	<v.number>	opttls opttlsclient optproxyproto
 %type	<v.number>	redirect_proto relay_proto match pflog
 %type	<v.number>	action ruleaf key_option
 %type	<v.port>	port
@@ -1103,6 +1104,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 +1952,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 +1962,8 @@ relayoptsl	: LISTEN ON STRING port opttls {
 				rlay->rl_conf.flags |= F_TLSCLIENT;
 				conf->sc_conf.flags |= F_TLSCLIENT;
 			}
+
+			rlay->rl_conf.flags |= $6;
 		}
 		| SESSION TIMEOUT NUMBER		{
 			if ((rlay->rl_conf.timeout.tv_sec = $3) < 0) {
@@ -1983,6 +1991,7 @@ relayoptsl	: LISTEN ON STRING port opttls {
 				free($2);
 				YYERROR;
 			}
+
 			p->flags |= F_USED;
 			rlay->rl_conf.proto = p->id;
 			rlay->rl_proto = p;
@@ -2479,6 +2488,7 @@ lookup(char *s)
 		{ "prefork",		PREFORK },
 		{ "priority",		PRIORITY },
 		{ "protocol",		PROTO },
+		{ "proxy-protocol",	PROXYPROTO },
 		{ "query",		QUERYSTR },
 		{ "quick",		QUICK },
 		{ "random",		RANDOM },
@@ -2518,6 +2528,8 @@ lookup(char *s)
 		{ "transparent",	TRANSPARENT },
 		{ "ttl",		TTL },
 		{ "url",		URL },
+		{ "v1",			V1 },
+		{ "v2",			V2 },
 		{ "value",		VALUE },
 		{ "websockets",		WEBSOCKETS },
 		{ "with",		WITH }
diff --git a/usr.sbin/relayd/proxy_protocol.c b/usr.sbin/relayd/proxy_protocol.c
new file mode 100644
index 00000000000..d832d3f6678
--- /dev/null
+++ b/usr.sbin/relayd/proxy_protocol.c
@@ -0,0 +1,142 @@
+/*
+ * Copyright (c) 2025 Christoph Liebender <christoph@liebender.dev>
+ *
+ * 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_sockname, obuf, sizeof(obuf));
+
+	if (con->se_relay->rl_conf.flags & F_UDP)
+		proxyproto = "UNKNOWN";
+	else {
+		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 = &con->se_sockname;
+	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..d705ac1552a 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 <christoph@liebender.dev>
  * Copyright (c) 2006 - 2014 Reyk Floeter <reyk@openbsd.org>
  *
  * Permission to use, copy, modify, and distribute this software for any
@@ -767,6 +768,19 @@ relay_connected(int fd, short sig, void *arg)
 		    "failed to allocate output buffer event", 0);
 		return;
 	}
+
+	error = 0;
+
+	if (rlay->rl_conf.flags & F_PROXYV1)
+		error = proxy_protocol_v1(con, bev->output);
+	else if (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 <christoph@liebender.dev>
 .\" Copyright (c) 2006 - 2016 Reyk Floeter <reyk@openbsd.org>
 .\" Copyright (c) 2006, 2007 Pierre-Yves Ritschard <pyr@openbsd.org>
 .\"
@@ -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 <christoph@liebender.dev>
  * Copyright (c) 2006 - 2016 Reyk Floeter <reyk@openbsd.org>
  * Copyright (c) 2006, 2007 Pierre-Yves Ritschard <pyr@openbsd.org>
  * Copyright (c) 2003, 2004 Henning Brauer <henning@openbsd.org>
@@ -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);