git: a03aff88a144 - main - icmp6: bring rate limiting on a par with IPv4

From: Gleb Smirnoff <glebius_at_FreeBSD.org>
Date: Sun, 24 Mar 2024 16:19:34 UTC
The branch main has been updated by glebius:

URL: https://cgit.FreeBSD.org/src/commit/?id=a03aff88a14448c3084a0384082ec996d7213897

commit a03aff88a14448c3084a0384082ec996d7213897
Author:     Gleb Smirnoff <glebius@FreeBSD.org>
AuthorDate: 2024-03-24 16:13:23 +0000
Commit:     Gleb Smirnoff <glebius@FreeBSD.org>
CommitDate: 2024-03-24 16:13:23 +0000

    icmp6: bring rate limiting on a par with IPv4
    
    Use counter_ratecheck() instead of racy and slow ppsratecheck. Use a
    separate counter for every currently known type of ICMPv6. Provide logging
    of ratelimit events. Provide jitter to counter open UDP port detection.
    
    Reviewed by:            tuexen, zlei
    Differential Revision:  https://reviews.freebsd.org/D44482
---
 sys/netinet6/icmp6.c | 185 ++++++++++++++++++++++++++++++++++++++++++++-------
 1 file changed, 162 insertions(+), 23 deletions(-)

diff --git a/sys/netinet6/icmp6.c b/sys/netinet6/icmp6.c
index 8f039d6dd196..ef4216b844b5 100644
--- a/sys/netinet6/icmp6.c
+++ b/sys/netinet6/icmp6.c
@@ -143,18 +143,6 @@ SYSCTL_INT(_net_inet6_icmp6, ICMPV6CTL_NODEINFO, nodeinfo,
 VNET_DECLARE(struct inpcbinfo, ripcbinfo);
 #define	V_ripcbinfo		VNET(ripcbinfo)
 
-VNET_DEFINE_STATIC(int, icmp6errppslim) = 100;
-#define	V_icmp6errppslim	VNET(icmp6errppslim)
-SYSCTL_INT(_net_inet6_icmp6, ICMPV6CTL_ERRPPSLIMIT, errppslimit,
-    CTLFLAG_VNET | CTLFLAG_RW, &VNET_NAME(icmp6errppslim), 0,
-    "Maximum number of ICMPv6 error messages per second");
-
-VNET_DEFINE_STATIC(int, icmp6errpps_count) = 0;
-VNET_DEFINE_STATIC(struct timeval, icmp6errppslim_last);
-
-#define	V_icmp6errpps_count		VNET(icmp6errpps_count)
-#define	V_icmp6errppslim_last		VNET(icmp6errppslim_last)
-
 static void icmp6_errcount(int, int);
 static int icmp6_rip6_input(struct mbuf **, int);
 static void icmp6_reflect(struct mbuf *, size_t);
@@ -2743,6 +2731,126 @@ icmp6_ctloutput(struct socket *so, struct sockopt *sopt)
 	return (error);
 }
 
+static int sysctl_icmp6lim_and_jitter(SYSCTL_HANDLER_ARGS);
+VNET_DEFINE_STATIC(u_int, icmp6errppslim) = 100;
+#define	V_icmp6errppslim	VNET(icmp6errppslim)
+SYSCTL_PROC(_net_inet6_icmp6, ICMPV6CTL_ERRPPSLIMIT, errppslimit,
+    CTLTYPE_UINT | CTLFLAG_VNET | CTLFLAG_RW, &VNET_NAME(icmp6errppslim), 0,
+    &sysctl_icmp6lim_and_jitter, "IU",
+    "Maximum number of ICMPv6 error/reply messages per second");
+
+VNET_DEFINE_STATIC(int, icmp6lim_curr_jitter) = 0;
+#define	V_icmp6lim_curr_jitter	VNET(icmp6lim_curr_jitter)
+
+VNET_DEFINE_STATIC(u_int, icmp6lim_jitter) = 8;
+#define	V_icmp6lim_jitter	VNET(icmp6lim_jitter)
+SYSCTL_PROC(_net_inet6_icmp6, OID_AUTO, icmp6lim_jitter, CTLTYPE_UINT |
+    CTLFLAG_VNET | CTLFLAG_RW, &VNET_NAME(icmp6lim_jitter), 0,
+    &sysctl_icmp6lim_and_jitter, "IU",
+    "Random errppslimit jitter adjustment limit");
+
+VNET_DEFINE_STATIC(int, icmp6lim_output) = 1;
+#define	V_icmp6lim_output	VNET(icmp6lim_output)
+SYSCTL_INT(_net_inet6_icmp6, OID_AUTO, icmp6lim_output,
+    CTLFLAG_VNET | CTLFLAG_RW, &VNET_NAME(icmp6lim_output), 0,
+    "Enable logging of ICMPv6 response rate limiting");
+
+typedef enum {
+	RATELIM_PARAM_PROB = 0,
+	RATELIM_TOO_BIG,
+	RATELIM_UNREACH,
+	RATELIM_TEXCEED,
+	RATELIM_REDIR,
+	RATELIM_REPLY,
+	RATELIM_OTHER,
+	RATELIM_MAX
+} ratelim_which;
+
+static const char *icmp6_rate_descrs[RATELIM_MAX] = {
+	[RATELIM_PARAM_PROB] = "bad IPv6 header",
+	[RATELIM_TOO_BIG] = "packet too big",
+	[RATELIM_UNREACH] = "destination unreachable",
+	[RATELIM_TEXCEED] = "time exceeded",
+	[RATELIM_REPLY] = "echo reply",
+	[RATELIM_REDIR] = "neighbor discovery redirect",
+	[RATELIM_OTHER] = "(other)",
+};
+
+static void
+icmp6lim_new_jitter(void)
+{
+	/*
+	 * Adjust limit +/- to jitter the measurement to deny a side-channel
+	 * port scan as in https://dl.acm.org/doi/10.1145/3372297.3417280
+	 */
+	if (V_icmp6lim_jitter > 0)
+		V_icmp6lim_curr_jitter =
+		    arc4random_uniform(V_icmp6lim_jitter * 2 + 1) -
+		    V_icmp6lim_jitter;
+}
+
+static int
+sysctl_icmp6lim_and_jitter(SYSCTL_HANDLER_ARGS)
+{
+	uint32_t new;
+	int error;
+	bool lim;
+
+	MPASS(oidp->oid_arg1 == &VNET_NAME(icmp6errppslim) ||
+	    oidp->oid_arg1 == &VNET_NAME(icmp6lim_jitter));
+
+	lim = (oidp->oid_arg1 == &VNET_NAME(icmp6errppslim));
+	new = lim ? V_icmp6errppslim : V_icmp6lim_jitter;
+	error = sysctl_handle_int(oidp, &new, 0, req);
+	if (error == 0 && req->newptr) {
+		if (lim) {
+			if (new <= V_icmp6lim_jitter)
+				error = EINVAL;
+			else
+				V_icmp6errppslim = new;
+		} else {
+			if (new >= V_icmp6errppslim)
+				error = EINVAL;
+			else {
+				V_icmp6lim_jitter = new;
+				icmp6lim_new_jitter();
+			}
+		}
+	}
+	MPASS(V_icmp6errppslim + V_icmp6lim_curr_jitter > 0);
+
+	return (error);
+}
+
+
+VNET_DEFINE_STATIC(struct counter_rate, icmp6_rates[RATELIM_MAX]);
+#define	V_icmp6_rates	VNET(icmp6_rates)
+
+static void
+icmp6_ratelimit_init(void)
+{
+
+	for (int i = 0; i < RATELIM_MAX; i++) {
+		V_icmp6_rates[i].cr_rate = counter_u64_alloc(M_WAITOK);
+		V_icmp6_rates[i].cr_ticks = ticks;
+	}
+	icmp6lim_new_jitter();
+}
+VNET_SYSINIT(icmp6_ratelimit, SI_SUB_PROTO_DOMAIN, SI_ORDER_ANY,
+    icmp6_ratelimit_init, NULL);
+
+#ifdef VIMAGE
+static void
+icmp6_ratelimit_uninit(void)
+{
+
+	for (int i = 0; i < RATELIM_MAX; i++)
+		counter_u64_free(V_icmp6_rates[i].cr_rate);
+}
+VNET_SYSUNINIT(icmp6_ratelimit, SI_SUB_PROTO_DOMAIN, SI_ORDER_THIRD,
+    icmp6_ratelimit_uninit, NULL);
+#endif
+
 /*
  * Perform rate limit check.
  * Returns 0 if it is okay to send the icmp6 packet.
@@ -2752,24 +2860,55 @@ icmp6_ctloutput(struct socket *so, struct sockopt *sopt)
  * XXX per-destination/type check necessary?
  *
  * dst - not used at this moment
- * type - not used at this moment
  * code - not used at this moment
  */
 int
-icmp6_ratelimit(const struct in6_addr *dst, const int type,
-    const int code)
+icmp6_ratelimit(const struct in6_addr *dst, const int type, const int code)
 {
-	int ret;
+	ratelim_which which;
+	int64_t pps;
 
-	ret = 0;	/* okay to send */
+	if (V_icmp6errppslim == 0)
+		return (0);
 
-	/* PPS limit */
-	if (!ppsratecheck(&V_icmp6errppslim_last, &V_icmp6errpps_count,
-	    V_icmp6errppslim)) {
-		/* The packet is subject to rate limit */
-		ret++;
+	switch (type) {
+	case ICMP6_PARAM_PROB:
+		which = RATELIM_PARAM_PROB;
+		break;
+	case ICMP6_PACKET_TOO_BIG:
+		which = RATELIM_TOO_BIG;
+		break;
+	case ICMP6_DST_UNREACH:
+		which = RATELIM_UNREACH;
+		break;
+	case ICMP6_TIME_EXCEEDED:
+		which = RATELIM_TEXCEED;
+		break;
+	case ND_REDIRECT:
+		which = RATELIM_REDIR;
+		break;
+	case ICMP6_ECHO_REPLY:
+		which = RATELIM_REPLY;
+		break;
+	default:
+		which = RATELIM_OTHER;
+		break;
+	};
+
+	pps = counter_ratecheck(&V_icmp6_rates[which], V_icmp6errppslim +
+	    V_icmp6lim_curr_jitter);
+	if (pps > 0) {
+		if (V_icmp6lim_output)
+			log(LOG_NOTICE, "Limiting ICMPv6 %s output from %jd "
+			    "to %d packets/sec\n", icmp6_rate_descrs[which],
+			    (intmax_t )pps, V_icmp6errppslim +
+			    V_icmp6lim_curr_jitter);
+		icmp6lim_new_jitter();
+	}
+	if (pps == -1) {
 		ICMP6STAT_INC(icp6s_toofreq);
+		return (-1);
 	}
 
-	return ret;
+	return (0);
 }