From: Christopher Faulet Date: Thu, 3 Jul 2025 13:16:47 +0000 (+0200) Subject: MINOR: proto-tcp: Add support for TCP MD5 signature for listeners and servers X-Git-Tag: v3.3-dev3~45 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=5232df57abe1d4ff1513e4642f1e247c399ebc64;p=thirdparty%2Fhaproxy.git MINOR: proto-tcp: Add support for TCP MD5 signature for listeners and servers 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 " 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. --- diff --git a/doc/configuration.txt b/doc/configuration.txt index d98584d46..509ffbda8 100644 --- a/doc/configuration.txt +++ b/doc/configuration.txt @@ -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 + 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, + 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 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 : 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 + 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, 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 May be used in the following contexts: tcp, http, log, peers, ring diff --git a/include/haproxy/listener-t.h b/include/haproxy/listener-t.h index 70f6c4ed1..37840790e 100644 --- a/include/haproxy/listener-t.h +++ b/include/haproxy/listener-t.h @@ -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 */ diff --git a/include/haproxy/server-t.h b/include/haproxy/server-t.h index 57642a031..f74f46f06 100644 --- a/include/haproxy/server-t.h +++ b/include/haproxy/server-t.h @@ -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 index 000000000..8ea214b8c --- /dev/null +++ b/reg-tests/connection/tcp_md5_signature.vtc @@ -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 diff --git a/src/cfgparse-tcp.c b/src/cfgparse-tcp.c index e60a18a54..0265e7d19 100644 --- a/src/cfgparse-tcp.c +++ b/src/cfgparse-tcp.c @@ -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 diff --git a/src/listener.c b/src/listener.c index 14fb55f1a..9d4232e8e 100644 --- a/src/listener.c +++ b/src/listener.c @@ -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: diff --git a/src/proto_tcp.c b/src/proto_tcp.c index 0de67c4eb..1e568fd86 100644 --- a/src/proto_tcp.c +++ b/src/proto_tcp.c @@ -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, diff --git a/src/proxy.c b/src/proxy.c index e3a56af4c..27f629e92 100644 --- a/src/proxy.c +++ b/src/proxy.c @@ -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 diff --git a/src/server.c b/src/server.c index 8cad9ca04..c5d29a30a 100644 --- a/src/server.c +++ b/src/server.c @@ -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);