MEDIUM: config: prevent communication with privileged ports

This commit introduces a new global setting named
harden.reject_privileged_ports.{tcp|quic}. When active, communications
with clients which use privileged source ports are forbidden. Such
behavior is considered suspicious as it can be used as spoofing or
DNS/NTP amplication attack.

Value is configured per transport protocol. For each TCP and QUIC
distinct code locations are impacted by this setting. The first one is
in sock_accept_conn() which acts as a filter for all TCP based
communications just after accept() returns a new connection. The second
one is dedicated for QUIC communication in quic_recv(). In both cases,
if a privileged source port is used and setting is disabled, received
message is silently dropped.

By default, protection are disabled for both protocols. This is to be
able to backport it without breaking changes on stable release.

This should be backported as it is an interesting security feature yet
relatively simple to implement.

(cherry picked from commit 45f40bac4cd256935d3157cd03f9434609d7a36a)
 [ad: context adjustment for doc]
Signed-off-by: Amaury Denoyelle <adenoyelle@haproxy.com>
(cherry picked from commit 083f20a12d433633556f38a72044d21f22dc23f9)
Signed-off-by: Amaury Denoyelle <adenoyelle@haproxy.com>
diff --git a/doc/configuration.txt b/doc/configuration.txt
index 82582c4..5b5ad1b 100644
--- a/doc/configuration.txt
+++ b/doc/configuration.txt
@@ -1086,6 +1086,8 @@
    - h1-case-adjust-file
    - h2-workaround-bogus-websocket-clients
    - hard-stop-after
+   - harden.reject-privileged-ports.tcp
+   - harden.reject-privileged-ports.quic
    - insecure-fork-wanted
    - insecure-setuid-wanted
    - issuers-chain-path
@@ -1721,6 +1723,12 @@
 
   See also: grace
 
+harden.reject-privileged-ports.tcp { on | off }
+harden.reject-privileged-ports.quic { on | off }
+  Toggle per protocol protection which forbid communication with clients which
+  use privileged ports as their source port. This range of ports is defined
+  according to RFC 6335. Protection is inactive by default on both protocols.
+
 insecure-fork-wanted
   By default HAProxy tries hard to prevent any thread and process creation
   after it starts. Doing so is particularly important when using Lua files of
diff --git a/include/haproxy/global-t.h b/include/haproxy/global-t.h
index fa3bc57..0a39e6c 100644
--- a/include/haproxy/global-t.h
+++ b/include/haproxy/global-t.h
@@ -192,6 +192,8 @@
 	struct proxy *cli_fe;           /* the frontend holding the stats settings */
 	int numa_cpu_mapping;
 	int prealloc_fd;
+	uchar clt_privileged_ports;     /* bitmask to allow client privileged ports exchanges per protocol */
+	/* 3-bytes hole */
 	int cfg_curr_line;              /* line number currently being parsed */
 	const char *cfg_curr_file;      /* config file currently being parsed or NULL */
 	char *cfg_curr_section;         /* config section name currently being parsed or NULL */
diff --git a/include/haproxy/protocol-t.h b/include/haproxy/protocol-t.h
index 47df366..9c929e8 100644
--- a/include/haproxy/protocol-t.h
+++ b/include/haproxy/protocol-t.h
@@ -137,6 +137,17 @@
 	struct list list;				/* list of registered protocols (under proto_lock) */
 };
 
+/* Transport protocol identifiers which can be used as masked values. */
+enum ha_proto {
+	HA_PROTO_NONE = 0x00,
+
+	HA_PROTO_TCP  = 0x01,
+	HA_PROTO_UDP  = 0x02,
+	HA_PROTO_QUIC = 0x04,
+
+	HA_PROTO_ANY  = 0xff,
+};
+
 #endif /* _HAPROXY_PROTOCOL_T_H */
 
 /*
diff --git a/include/haproxy/tools.h b/include/haproxy/tools.h
index b298f6e..e4481d3 100644
--- a/include/haproxy/tools.h
+++ b/include/haproxy/tools.h
@@ -42,6 +42,7 @@
 #include <haproxy/api.h>
 #include <haproxy/chunk.h>
 #include <haproxy/intops.h>
+#include <haproxy/global.h>
 #include <haproxy/namespace-t.h>
 #include <haproxy/protocol-t.h>
 #include <haproxy/tools-t.h>
@@ -762,6 +763,21 @@
 	return 0;
 }
 
+/* Returns true if <addr> port is forbidden as client source using <proto>. */
+static inline int port_is_restricted(const struct sockaddr_storage *addr,
+                                     enum ha_proto proto)
+{
+	const uint16_t port = get_host_port(addr);
+
+	BUG_ON_HOT(proto != HA_PROTO_TCP && proto != HA_PROTO_QUIC);
+
+	/* RFC 6335 6. Port Number Ranges */
+	if (unlikely(port < 1024 && port > 0))
+		return !(global.clt_privileged_ports & proto);
+
+	return 0;
+}
+
 /* Convert mask from bit length form to in_addr form.
  * This function never fails.
  */
diff --git a/src/cfgparse-global.c b/src/cfgparse-global.c
index d15bc53..864e9d4 100644
--- a/src/cfgparse-global.c
+++ b/src/cfgparse-global.c
@@ -1383,8 +1383,59 @@
 	return 0;
 }
 
+/* Parser for harden.reject-privileged-ports.{tcp|quic}. */
+static int cfg_parse_reject_privileged_ports(char **args, int section_type,
+                                             struct proxy *curpx,
+                                             const struct proxy *defpx,
+                                             const char *file, int line, char **err)
+{
+	struct ist proto;
+	char onoff;
+
+	if (!*(args[1])) {
+		memprintf(err, "'%s' expects either 'on' or 'off'.", args[0]);
+		return -1;
+	}
+
+	proto = ist(args[0]);
+	while (istlen(istfind(proto, '.')))
+		proto = istadv(istfind(proto, '.'), 1);
+
+	if (strcmp(args[1], "on") == 0) {
+		onoff = 1;
+	}
+	else if (strcmp(args[1], "off") == 0) {
+		onoff = 0;
+	}
+	else {
+		memprintf(err, "'%s' expects either 'on' or 'off'.", args[0]);
+		return -1;
+	}
+
+	if (istmatch(proto, ist("tcp"))) {
+		if (!onoff)
+			global.clt_privileged_ports |= HA_PROTO_TCP;
+		else
+			global.clt_privileged_ports &= ~HA_PROTO_TCP;
+	}
+	else if (istmatch(proto, ist("quic"))) {
+		if (!onoff)
+			global.clt_privileged_ports |= HA_PROTO_QUIC;
+		else
+			global.clt_privileged_ports &= ~HA_PROTO_QUIC;
+	}
+	else {
+		memprintf(err, "invalid protocol for '%s'.", args[0]);
+		return -1;
+	}
+
+	return 0;
+}
+
 static struct cfg_kw_list cfg_kws = {ILH, {
 	{ CFG_GLOBAL, "prealloc-fd", cfg_parse_prealloc_fd },
+	{ CFG_GLOBAL, "harden.reject-privileged-ports.tcp",  cfg_parse_reject_privileged_ports },
+	{ CFG_GLOBAL, "harden.reject-privileged-ports.quic", cfg_parse_reject_privileged_ports },
 	{ 0, NULL, NULL },
 }};
 
diff --git a/src/haproxy.c b/src/haproxy.c
index 5edd4c3..b320b85 100644
--- a/src/haproxy.c
+++ b/src/haproxy.c
@@ -224,6 +224,8 @@
 	.maxsslconn = DEFAULT_MAXSSLCONN,
 #endif
 #endif
+	/* by default do not protect against clients using privileged port */
+	.clt_privileged_ports = HA_PROTO_ANY,
 	/* others NULL OK */
 };
 
diff --git a/src/quic_sock.c b/src/quic_sock.c
index 66aba93..b7320a5 100644
--- a/src/quic_sock.c
+++ b/src/quic_sock.c
@@ -29,6 +29,7 @@
 #include <haproxy/listener.h>
 #include <haproxy/log.h>
 #include <haproxy/pool.h>
+#include <haproxy/protocol-t.h>
 #include <haproxy/proto_quic.h>
 #include <haproxy/proxy-t.h>
 #include <haproxy/quic_conn.h>
@@ -341,6 +342,11 @@
 	if (ret < 0)
 		goto end;
 
+	if (unlikely(port_is_restricted((struct sockaddr_storage *)from, HA_PROTO_QUIC))) {
+		ret = -1;
+		goto end;
+	}
+
 	for (cmsg = CMSG_FIRSTHDR(&msg); cmsg; cmsg = CMSG_NXTHDR(&msg, cmsg)) {
 		switch (cmsg->cmsg_level) {
 		case IPPROTO_IP:
diff --git a/src/sock.c b/src/sock.c
index 5453140..5340667 100644
--- a/src/sock.c
+++ b/src/sock.c
@@ -30,6 +30,7 @@
 #include <haproxy/listener.h>
 #include <haproxy/log.h>
 #include <haproxy/namespace.h>
+#include <haproxy/protocol-t.h>
 #include <haproxy/proto_sockpair.h>
 #include <haproxy/sock.h>
 #include <haproxy/sock_inet.h>
@@ -109,6 +110,9 @@
 			goto fail_conn;
 		}
 
+		if (unlikely(port_is_restricted(addr, HA_PROTO_TCP)))
+			goto fail_conn;
+
 		/* Perfect, the connection was accepted */
 		conn = conn_new(&l->obj_type);
 		if (!conn)