Index | Thread | Search

From:
Kirill A. Korinsky <kirill@korins.ky>
Subject:
Re: relayd: add support for PROXY protocol in TCP relays
To:
Christoph Liebender <christoph@liebender.dev>
Cc:
tech@openbsd.org, reyk@openbsd.org
Date:
Tue, 18 Nov 2025 00:14:06 +0100

Download raw body.

Thread
On Sun, 16 Nov 2025 19:31:10 +0100,
Christoph Liebender <christoph@liebender.dev> wrote:
> 
> Here goes a patch with support for PROXY V2 over UDP - albeit untested.
> 
> comments, ok?

I think it woth to be added and I had played a bit. I can't figure out how
to make a config like this work:

table <webhosts> { 127.0.0.1 }

http protocol xff {
	match request header set "X-Forwarded-For" value "$REMOTE_ADDR"

	pass request forward to <webhosts>
}

relay www {
	listen on 0.0.0.0 port 8080

	protocol xff

	forward to <webhosts> port 80 check http "/" code 404 proxy-protocol v1
}

if I remove "protocol xff" it starts.

As a test I've used:

table <webhosts> { 127.0.0.1 }

relay www {
	listen on 0.0.0.0 port 8080
	forward to <webhosts> port 80 check http "/" code 404 proxy-protocol XX
}

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.

Next, XX as v2 produces:

0000   0d 0a 0d 0a 00 0d 0a 51 55 49 54 0a 21 11 00 0c   .......QUIT.!...
0010   7f 00 00 01 00 00 00 00 bb fc 1f 90 47 45 54 20   ............GET 
0020   2f 20 48 54 54 50 2f 31 2e 31 0d 0a 48 6f 73 74   / HTTP/1.1..Host
0030   3a 20 31 32 37 2e 30 2e 30 2e 31 3a               : 127.0.0.1:

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

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.

> +#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

-- 
wbr, Kirill


On Sun, 16 Nov 2025 19:31:10 +0100,
Christoph Liebender <christoph@liebender.dev> wrote:
> 
> [1  <text/plain; UTF-8 (8bit)>]
> 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?
> [2 relayd6.patch <text/x-patch; UTF-8 (base64)>]
> 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 <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>
> @@ -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	<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 +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 <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_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 <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,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 <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);

-- 
wbr, Kirill