Download raw body.
relayd: add support for PROXY protocol in TCP relays
On 11/9/25 20:06, Christoph Liebender wrote:
> On 11/9/25 14:54, Kirill A. Korinsky wrote:
>> On Sun, 09 Nov 2025 11:30:16 +0100,
>> Christoph Liebender <christoph@liebender.dev> wrote:
>>>
>>> No, there is no apparent reason - I had implemented v1 in op@'s gmid(8)
>>> (in ports) a while back [1] and still had in mind that v1 is relatively
>>> straight forward - therefore I initially didn't think about v2 at all.
>>>
>>> Also, nginx stream proxy module only speaks version 1. That of course
>>> doesn't mean relayd needs to be limited to v1... Though v2 probably
>>> needs more effort and is less trivial to debug.
>>>
>>> With v1, one can just nc(1) on a port that relayd forwards to, connect
>>> to the listen port of relayd and see the proxy line in plaintext.
>>>
>>> In my opition, the question about supporting v2 is about:
>>>
>>> - does v2 offer any functionality that v1 doesn't while possibly being
>>> useful for relayd?
>>> - is there any server implementation that relayd would forward to that
>>> only supports v2, not v1?
>>> - is the added code complexity of v2 worth the "performance benefits"
>>> that are stated in the v2 spec?
>>>
>>> If this patch gets ok'd and in the future, v2 is supposed to be
>>> supported as well, it probably makes sense to alter the configuration
>>> syntax to something like:
>>>
>>> proxy-protocol v1
>>>
>>> to at some point add an option to place a "v2" there.
>>>
>>
>> Well, v2 isn't more complicated: read / write fixed header with a bit
>> which
>> specific a kind of connection and length the payload.
>>
>> What's all.
>>
>> If I not mistaken the haproxy docs contains a union with example.
>>
>
> Ok then, this patch implements V2 as well. Feel free to test.
>
I've tested this in front of nginx that is configured via
listen 8443 ssl proxy_protocol;
and it works as expected with both PROXY versions. Below is a patch with
some formatting nits and #defined magic numbers.
comments, ok?
diff --git a/usr.sbin/relayd/Makefile b/usr.sbin/relayd/Makefile
index 2e1ef449b04..a7b33cbe4f1 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.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..49c12d771ec 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,7 @@ 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) {
@@ -2479,6 +2486,7 @@ lookup(char *s)
{ "prefork", PREFORK },
{ "priority", PRIORITY },
{ "protocol", PROTO },
+ { "proxy-protocol", PROXYPROTO },
{ "query", QUERYSTR },
{ "quick", QUICK },
{ "random", RANDOM },
@@ -2518,6 +2526,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.c b/usr.sbin/relayd/proxy.c
new file mode 100644
index 00000000000..d0a9dd745ff
--- /dev/null
+++ b/usr.sbin/relayd/proxy.c
@@ -0,0 +1,129 @@
+/*
+ * 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_TCP6 0x21
+
+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_v1(const struct rsession *con, struct evbuffer *dstout)
+{
+ char ibuf[128], obuf[128];
+ const char *proxyproto;
+ int ret;
+
+ memset(&ibuf, 0, sizeof(ibuf));
+ memset(&obuf, 0, 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_v2(const struct rsession *con, struct evbuffer *dstout)
+{
+ union proxy_v2_addr addr;
+ struct proxy_v2_hdr hdr;
+ int error;
+ u_int16_t len;
+
+ bcopy(PROXY_V2_SIG, hdr.sig, sizeof(hdr.sig));
+ hdr.ver_cmd = 0x20 | PROXY_V2_CMD_PROXY;
+
+ switch (con->se_in.ss.ss_family) {
+ case AF_INET:
+ hdr.fam = PROXY_V2_FAM_TCP4;
+ len = sizeof(addr.ipv4_addr);
+ addr.ipv4_addr.src_addr =
+ ((const struct sockaddr_in *)&con->se_in.ss)->sin_addr.s_addr;
+ addr.ipv4_addr.dst_addr =
+ ((const struct sockaddr_in *)&con->se_relay->rl_conf.ss)->sin_addr.s_addr;
+ addr.ipv4_addr.src_port = con->se_in.port;
+ addr.ipv4_addr.dst_port = con->se_relay->rl_conf.port;
+ break;
+ case AF_INET6:
+ hdr.fam = PROXY_V2_FAM_TCP6;
+ len = sizeof(addr.ipv6_addr);
+ bcopy(&((const struct sockaddr_in6 *)&con->se_in.ss)->sin6_addr,
+ addr.ipv6_addr.src_addr, sizeof(addr.ipv6_addr.src_addr));
+ bcopy(&((const struct sockaddr_in6 *)&con->se_relay->rl_conf.ss)->sin6_addr,
+ addr.ipv6_addr.dst_addr, sizeof(addr.ipv6_addr.dst_addr));
+ addr.ipv6_addr.src_port = con->se_in.port;
+ addr.ipv6_addr.dst_port = con->se_relay->rl_conf.port;
+ 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..f166ff02172 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,20 @@ relay_connected(int fd, short sig, void *arg)
"failed to allocate output buffer event", 0);
return;
}
+
+ if (rlay->rl_proto->type == RELAY_PROTO_TCP) {
+ error = 0;
+ if (rlay->rl_conf.flags & F_PROXYV1)
+ error = proxy_v1(con, bev->output);
+ else if (rlay->rl_conf.flags & F_PROXYV2)
+ error = proxy_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..e8f76a8a471 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.c */
+int proxy_v1(const struct rsession *, struct evbuffer *);
+int proxy_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);
relayd: add support for PROXY protocol in TCP relays