]> git.ipfire.org Git - thirdparty/haproxy.git/commitdiff
MINOR: proto-tcp: Add support for TCP MD5 signature for listeners and servers
authorChristopher Faulet <cfaulet@haproxy.com>
Thu, 3 Jul 2025 13:16:47 +0000 (15:16 +0200)
committerChristopher Faulet <cfaulet@haproxy.com>
Thu, 3 Jul 2025 13:25:40 +0000 (15:25 +0200)
This patch adds the support for the RFC2385 (Protection of BGP Sessions via
the + TCP MD5 Signature Option) for the listeners and the servers. The
feature is only available on Linux. Keywords are not exposed otherwise.

By setting "tcp-md5sig <password>" option on a bind line, TCP segments of
all connections instantiated from the listening socket will be signed with a
16-byte MD5 digest. The same option can be set on a server line to protect
outgoing connections to the corresponding server.

The primary use case for this option is to allow BGP to protect itself
against the introduction of spoofed TCP segments into the connection
stream. But it can be useful for any very long-lived TCP connections.

A reg-test was added and it will be executed only on linux. All other
targets are excluded.

doc/configuration.txt
include/haproxy/listener-t.h
include/haproxy/server-t.h
reg-tests/connection/tcp_md5_signature.vtc [new file with mode: 0644]
src/cfgparse-tcp.c
src/listener.c
src/proto_tcp.c
src/proxy.c
src/server.c

index d98584d468049968f5abee2c728b86c2051649cf..509ffbda8bf9c20e34f4b68e1638e3a7467dfe7d 100644 (file)
@@ -17079,6 +17079,16 @@ strict-sni
   disabled on a "bind" line using "no-strict-sni". See the "crt" option for
   more information. See "add ssl crt-list" command in the management guide.
 
+tcp-md5sig <password>
+  Enables the TCP MD5 signature (RFC 2385 Protection of BGP Sessions via the
+  TCP MD5 Signature Option) for all incoming connections instantiated from this
+  listening socket. This option is only available on Linux. When enabled,
+  <password> string is used to sign every TCP segments with a 16-byte MD5
+  digest. This will protect the TCP connection against spoofing. The primary
+  use case for this option is to allow BGP to protect itself against the
+  introduction of spoofed TCP segments into the connection stream. But it can
+  be useful for any very long-lived TCP connections.
+
 tcp-ut <delay>
   Sets the TCP User Timeout for all incoming connections instantiated from this
   listening socket. This option is available on Linux since version 2.6.37. It
@@ -18766,6 +18776,18 @@ socks4 <addr>:<port>
   server. Using this option won't force the health check to go via socks4 by
   default. You will have to use the keyword "check-via-socks4" to enable it.
 
+tcp-md5sig <password>
+  May be used in the following contexts: tcp, http, log, peers, ring
+
+  Enables the TCP MD5 signature (RFC 2385 Protection of BGP Sessions via the
+  TCP MD5 Signature Option) for all outgoing connections to this server. This
+  option is only available on Linux. When enabled, <password> string is used to
+  sign every TCP segments with a 16-byte MD5 digest. This will protect the TCP
+  connection against spoofing. The primary use case for this option is to allow
+  BGP to protect itself against the introduction of spoofed TCP segments into
+  the connection stream. But it can be useful for any very long-lived TCP
+  connections.
+
 tcp-ut <delay>
   May be used in the following contexts: tcp, http, log, peers, ring
 
index 70f6c4ed19f93f14768657571f435b2630ee841d..37840790eb0717c953bb7c38c3163a91f5034725 100644 (file)
@@ -193,6 +193,7 @@ struct bind_conf {
        unsigned int analysers;    /* bitmap of required protocol analysers */
        int maxseg;                /* for TCP, advertised MSS */
        int tcp_ut;                /* for TCP, user timeout */
+       char *tcp_md5sig;          /* TCP MD5 signature password (RFC2385) */
        int idle_ping;             /* MUX idle-ping interval in ms */
        int maxaccept;             /* if set, max number of connections accepted at once (-1 when disabled) */
        unsigned int backlog;      /* if set, listen backlog */
index 57642a0312b28f4292a0e4c9fcd53f637da68fe5..f74f46f06aa7adcf7eba565b3707be45409318c4 100644 (file)
@@ -431,6 +431,7 @@ struct server {
 
        int puid;                               /* proxy-unique server ID, used for SNMP, and "first" LB algo */
        int tcp_ut;                             /* for TCP, user timeout */
+       char *tcp_md5sig;                       /* TCP MD5 signature password (RFC2385) */
 
        int do_check;                           /* temporary variable used during parsing to denote if health checks must be enabled */
        int do_agent;                           /* temporary variable used during parsing to denote if an auxiliary agent check must be enabled */
diff --git a/reg-tests/connection/tcp_md5_signature.vtc b/reg-tests/connection/tcp_md5_signature.vtc
new file mode 100644 (file)
index 0000000..8ea214b
--- /dev/null
@@ -0,0 +1,60 @@
+varnishtest "Test the support for tcp-md5sig option (linux only)"
+
+feature ignore_unknown_macro
+feature cmd "$HAPROXY_PROGRAM -cc 'version_atleast(3.3-dev1)'"
+
+#EXCLUDE_TARGETS=solaris,freebsd,freebsd-glibc,dragonfly,openbsd,netbsd,cygwin,haiku,aix51,aix52,aix72-gcc,osx,generic,custom
+
+
+haproxy h1 -conf {
+    defaults
+        mode http
+        timeout client "${HAPROXY_TEST_TIMEOUT-5s}"
+        timeout server "${HAPROXY_TEST_TIMEOUT-5s}"
+        timeout connect "${HAPROXY_TEST_TIMEOUT-100ms}"
+       retries 0
+        log global
+
+    listen internal
+        bind "fd@${md5_int}" tcp-md5sig mypass
+        bind "fd@${nomd5_int}"
+        http-request return status 200
+
+    listen fe
+        bind "fd@${fe}"
+
+        use-server s1 if { path /s1 }
+        use-server s2 if { path /s2 }
+        use-server s3 if { path /s3 }
+        use-server s4 if { path /s4 }
+
+        server s1 ${h1_md5_int_addr}:${h1_md5_int_port} tcp-md5sig mypass
+        server s2 ${h1_md5_int_addr}:${h1_md5_int_port} tcp-md5sig badpass
+        server s3 ${h1_nomd5_int_addr}:${h1_nomd5_int_port}
+        server s4 ${h1_nomd5_int_addr}:${h1_nomd5_int_port} tcp-md5sig mypass
+
+} -start
+
+client c1 -connect ${h1_fe_sock} {
+    txreq -req "GET" -url "/s1"
+    rxresp
+    expect resp.status == 200
+} -run
+
+client c2 -connect ${h1_fe_sock} {
+    txreq -req "GET" -url "/s2"
+    rxresp
+    expect resp.status == 503
+} -run
+
+client c3 -connect ${h1_fe_sock} {
+    txreq -req "GET" -url "/s3"
+    rxresp
+    expect resp.status == 200
+} -run
+
+client c4 -connect ${h1_fe_sock} {
+    txreq -req "GET" -url "/s4"
+    rxresp
+    expect resp.status == 503
+} -run
index e60a18a54f67664491d554f320628e8a00c99888..0265e7d1992f677ee5ca69b7e3ff98344a086886 100644 (file)
@@ -100,6 +100,31 @@ static int bind_parse_mss(char **args, int cur_arg, struct proxy *px, struct bin
 }
 #endif
 
+#if defined(__linux__) && defined(TCP_MD5SIG)
+/* parse the "tcp-md5sig" bind keyword */
+static int bind_parse_tcp_md5sig(char **args, int cur_arg, struct proxy *px, struct bind_conf *conf, char **err)
+{
+       if (!*args[cur_arg + 1]) {
+               memprintf(err, "'%s' : missing TCP MD5 signature password", args[cur_arg]);
+               return ERR_ALERT | ERR_FATAL;
+       }
+
+       ha_free(&conf->tcp_md5sig);
+       if (strlen(args[cur_arg + 1]) > TCP_MD5SIG_MAXKEYLEN) {
+               memprintf(err, "'%s' : password too long (at most %d characters expected)",
+                         args[cur_arg], TCP_MD5SIG_MAXKEYLEN);
+               return ERR_ALERT | ERR_ABORT;
+       }
+       conf->tcp_md5sig = strdup(args[cur_arg + 1]);
+       if (!conf->tcp_md5sig) {
+               memprintf(err, "'%s %s' : out of memory", args[cur_arg], args[cur_arg + 1]);
+               return ERR_ALERT | ERR_FATAL;
+       }
+
+       return 0;
+}
+#endif
+
 #ifdef TCP_USER_TIMEOUT
 /* parse the "tcp-ut" bind keyword */
 static int bind_parse_tcp_ut(char **args, int cur_arg, struct proxy *px, struct bind_conf *conf, char **err)
@@ -178,6 +203,32 @@ static int bind_parse_namespace(char **args, int cur_arg, struct proxy *px, stru
 }
 #endif
 
+#if defined(__linux__) && defined(TCP_MD5SIG)
+/* parse the "tcp-md5sig" server keyword */
+static int srv_parse_tcp_md5sig(char **args, int *cur_arg, struct proxy *px, struct server *newsrv, char **err)
+{
+       if (!*args[*cur_arg + 1]) {
+               memprintf(err, "'%s' : missing TCP MD5 signature password", args[*cur_arg]);
+               return ERR_ALERT | ERR_FATAL;
+       }
+
+       if (newsrv->addr.ss_family == AF_INET || newsrv->addr.ss_family == AF_INET6) {
+               ha_free(&newsrv->tcp_md5sig);
+               if (strlen(args[*cur_arg + 1]) > TCP_MD5SIG_MAXKEYLEN) {
+                       memprintf(err, "'%s' : password too long (at most %d characters expected)",
+                                 args[*cur_arg], TCP_MD5SIG_MAXKEYLEN);
+                       return ERR_ALERT | ERR_ABORT;
+               }
+               newsrv->tcp_md5sig = strdup(args[*cur_arg + 1]);
+               if (!newsrv->tcp_md5sig) {
+                       memprintf(err, "'%s %s' : out of memory", args[*cur_arg], args[*cur_arg + 1]);
+                       return ERR_ALERT | ERR_FATAL;
+               }
+       }
+       return 0;
+}
+#endif
+
 #ifdef TCP_USER_TIMEOUT
 /* parse the "tcp-ut" server keyword */
 static int srv_parse_tcp_ut(char **args, int *cur_arg, struct proxy *px, struct server *newsrv, char **err)
@@ -235,6 +286,9 @@ static struct bind_kw_list bind_kws = { "TCP", { }, {
 #ifdef TCP_MAXSEG
        { "mss",           bind_parse_mss,          1 }, /* set MSS of listening socket */
 #endif
+#if defined(__linux__) && defined(TCP_MD5SIG)
+       { "tcp-md5sig",    bind_parse_tcp_md5sig,   1 }, /* set TCP MD5 signature password */
+#endif
 #ifdef TCP_USER_TIMEOUT
        { "tcp-ut",        bind_parse_tcp_ut,       1 }, /* set User Timeout on listening socket */
 #endif
@@ -264,6 +318,9 @@ static struct bind_kw_list bind_kws = { "TCP", { }, {
 INITCALL1(STG_REGISTER, bind_register_keywords, &bind_kws);
 
 static struct srv_kw_list srv_kws = { "TCP", { }, {
+#if defined(__linux__) && defined(TCP_MD5SIG)
+       { "tcp-md5sig",    srv_parse_tcp_md5sig,    1,  1,  0 }, /* set TCP MD5 signature password on server */
+#endif
 #ifdef TCP_USER_TIMEOUT
        { "tcp-ut",        srv_parse_tcp_ut,        1,  1,  0 }, /* set TCP user timeout on server */
 #endif
index 14fb55f1afb03425bd9c89ba7b444157659640ea..9d4232e8e594c54e9da35ee476fc0da4f7f09723 100644 (file)
@@ -2073,6 +2073,8 @@ struct bind_conf *bind_conf_alloc(struct proxy *fe, const char *file,
 
        bind_conf->rhttp_srvname = NULL;
 
+       bind_conf->tcp_md5sig = NULL;
+
        return bind_conf;
 
   err:
index 0de67c4ebe7af249b18710bfb6c95cb96205f285..1e568fd8648b09090ab3a15eb444fb405c0b02aa 100644 (file)
@@ -531,6 +531,25 @@ int tcp_connect_server(struct connection *conn, int flags)
                 setsockopt(fd, IPPROTO_TCP, TCP_QUICKACK, &zero, sizeof(zero));
 #endif
 
+#if defined(__linux__) && defined(TCP_MD5SIG)
+       /* if it fails, the connection will fail, so reported an error */
+       if (srv && srv->tcp_md5sig) {
+               struct tcp_md5sig md5;
+
+               if (conn->dst->ss_family == AF_INET)
+                       memcpy(&md5.tcpm_addr, (struct sockaddr_in *)conn->dst, sizeof(struct sockaddr_in));
+               else
+                       memcpy(&md5.tcpm_addr, (struct sockaddr_in6 *)conn->dst, sizeof(struct sockaddr_in6));
+
+               strlcpy2((char*)md5.tcpm_key, srv->tcp_md5sig, sizeof(md5.tcpm_key));
+               md5.tcpm_keylen = strlen(srv->tcp_md5sig);
+               if (setsockopt(fd, IPPROTO_TCP, TCP_MD5SIG, &md5, sizeof(md5)) < 0) {
+                       conn->flags |= CO_FL_ERROR;
+                       return SF_ERR_SRVCL;
+               }
+       }
+#endif
+
 #ifdef TCP_USER_TIMEOUT
        /* there is not much more we can do here when it fails, it's still minor */
        if (srv && srv->tcp_ut)
@@ -712,6 +731,24 @@ int tcp_bind_listener(struct listener *listener, char *errmsg, int errlen)
                }
        }
 #endif
+#if defined(__linux__) && defined(TCP_MD5SIG)
+       if (listener->bind_conf->tcp_md5sig) {
+               struct tcp_md5sig md5;
+
+               if (listener->rx.addr.ss_family == AF_INET)
+                       memcpy(&md5.tcpm_addr, (struct sockaddr_in *)&listener->rx.addr, sizeof(struct sockaddr_in));
+               else
+                       memcpy(&md5.tcpm_addr, (struct sockaddr_in6 *)&listener->rx.addr, sizeof(struct sockaddr_in6));
+
+               strlcpy2((char*)md5.tcpm_key, listener->bind_conf->tcp_md5sig, sizeof(md5.tcpm_key));
+               md5.tcpm_keylen = strlen(listener->bind_conf->tcp_md5sig);
+               if (setsockopt(fd, IPPROTO_TCP, TCP_MD5SIG, &md5, sizeof(md5)) < 0) {
+                       chunk_appendf(msg, "%scannot set TCP MD5 signature, (%s)", msg->data ? ", " : "",
+                                     strerror(errno));
+                       err = ERR_ALERT | ERR_ABORT;
+               }
+       }
+#endif
 #if defined(TCP_USER_TIMEOUT)
        if (listener->bind_conf->tcp_ut) {
                if (setsockopt(fd, IPPROTO_TCP, TCP_USER_TIMEOUT,
index e3a56af4c14e02c667060f5b305c41c7e4a7e624..27f629e92ceae67d9ebedf40bc2b8991a820fc68 100644 (file)
@@ -418,6 +418,7 @@ void deinit_proxy(struct proxy *p)
                LIST_DELETE(&bind_conf->by_fe);
                free(bind_conf->guid_prefix);
                free(bind_conf->rhttp_srvname);
+               free(bind_conf->tcp_md5sig);
 #ifdef USE_QUIC
                free(bind_conf->quic_cc_algo);
 #endif
index 8cad9ca049482ec5ea35be87a5e60fbae977d1d5..c5d29a30a4c21918351747d35b44ef8e28526faf 100644 (file)
@@ -2972,6 +2972,10 @@ void srv_settings_cpy(struct server *srv, const struct server *src, int srv_tmpl
 #if defined(USE_OPENSSL)
        srv_ssl_settings_cpy(srv, src);
 #endif
+#ifdef TCP_MD5SIG
+       if (src->tcp_md5sig != NULL)
+               srv->tcp_md5sig = strdup(src->tcp_md5sig);
+#endif
 #ifdef TCP_USER_TIMEOUT
        srv->tcp_ut = src->tcp_ut;
 #endif
@@ -3114,6 +3118,7 @@ void srv_free_params(struct server *srv)
        free(srv->pool_conn_name);
        release_sample_expr(srv->pool_conn_name_expr);
        free(srv->resolvers_id);
+       free(srv->tcp_md5sig);
        free(srv->addr_node.key);
        free(srv->lb_nodes);
        counters_be_shared_drop(srv->counters.shared);