git: cb007621bc72 - main - netinet6: Check the hop limit after pfil in ip6_tryforward()

From: R. Christian McDonald <rcm_at_FreeBSD.org>
Date: Fri, 30 Oct 2026 17:34:17 UTC
The branch main has been updated by rcm:

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

commit cb007621bc729c346e59b3e5e7e4234f5578d3a0
Author:     R. Christian McDonald <rcm@FreeBSD.org>
AuthorDate: 2026-10-30 17:32:37 +0000
Commit:     R. Christian McDonald <rcm@FreeBSD.org>
CommitDate: 2026-10-30 17:32:37 +0000

    netinet6: Check the hop limit after pfil in ip6_tryforward()
    
    ip6_tryforward() sends time exceeded for an expiring packet before the
    inbound pfil hooks have seen it, whereas ip_tryforward() and
    ip6_forward(), which ip6_input() calls after the hooks, check it
    afterwards.  On the fast path the router therefore answers packets the
    firewall blocks inbound, and pf never handles the expiry itself: a
    nat64 (af-to) packet with hop limit 1 gets a time exceeded error from
    the router, which a "block" ruleset then drops on the way out, instead
    of the one pf_translate_af() sends with the untranslated addresses.
    
    Check the hop limit after the inbound hooks.
    
    Reviewed by:            kp
    Fixes:                  5a1842a2 ("Add ip6_tryforward() - a run to completion ...")
    MFC after:              1 week
    Sponsored by:           Rubicon Communications, LLC ("Netgate")
    Differential Revision:  https://reviews.freebsd.org/D61034
---
 sys/netinet6/ip6_fastfwd.c      | 26 +++++++++++-----------
 tests/sys/netpfil/pf/forward.sh | 49 +++++++++++++++++++++++++++++++++++++++++
 2 files changed, 62 insertions(+), 13 deletions(-)

diff --git a/sys/netinet6/ip6_fastfwd.c b/sys/netinet6/ip6_fastfwd.c
index f6d6df4c7b96..4e60e18a8067 100644
--- a/sys/netinet6/ip6_fastfwd.c
+++ b/sys/netinet6/ip6_fastfwd.c
@@ -145,19 +145,6 @@ ip6_tryforward(struct mbuf *m)
 			    m->m_pkthdr.len);
 	}
 
-	/*
-	 * Hop limit.
-	 */
-#ifdef IPSTEALTH
-	if (!V_ip6stealth)
-#endif
-	if (ip6->ip6_hlim <= IPV6_HLIMDEC) {
-		icmp6_error(m, ICMP6_TIME_EXCEEDED,
-		    ICMP6_TIME_EXCEED_TRANSIT, 0);
-		m = NULL;
-		goto dropin;
-	}
-
 	bzero(&dst, sizeof(dst));
 	dst.sin6_family = AF_INET6;
 	dst.sin6_len = sizeof(dst);
@@ -197,6 +184,19 @@ ip6_tryforward(struct mbuf *m)
 		dst.sin6_addr = ip6->ip6_dst;
 	}
 passin:
+	/*
+	 * Hop limit, checked after the inbound hooks as on the other
+	 * forwarding paths: a packet they drop gets no time exceeded error.
+	 */
+#ifdef IPSTEALTH
+	if (!V_ip6stealth)
+#endif
+	if (ip6->ip6_hlim <= IPV6_HLIMDEC) {
+		icmp6_error(m, ICMP6_TIME_EXCEEDED,
+		    ICMP6_TIME_EXCEED_TRANSIT, 0);
+		m = NULL;
+		goto dropin;
+	}
 	/*
 	 * Find route to destination.
 	 */
diff --git a/tests/sys/netpfil/pf/forward.sh b/tests/sys/netpfil/pf/forward.sh
index e9539bc9d278..a01da5f62e7a 100644
--- a/tests/sys/netpfil/pf/forward.sh
+++ b/tests/sys/netpfil/pf/forward.sh
@@ -163,8 +163,57 @@ v6_cleanup()
 	pft_cleanup
 }
 
+atf_test_case "v6_hlim" "cleanup"
+v6_hlim_head()
+{
+	atf_set descr 'Test that packets pf blocks get no time exceeded error'
+	atf_set require.user root
+}
+
+v6_hlim_body()
+{
+	pft_init
+
+	epair_send=$(vnet_mkepair)
+	epair_recv=$(vnet_mkepair)
+
+	ifconfig ${epair_send}a inet6 2001:db8:42::1/64 up no_dad -ifdisabled
+	ifconfig ${epair_recv}a up
+
+	vnet_mkjail alcatraz ${epair_send}b ${epair_recv}b
+
+	jexec alcatraz ifconfig ${epair_send}b inet6 2001:db8:42::2/64 up no_dad
+	jexec alcatraz ifconfig ${epair_recv}b inet6 2001:db8:43::2/64 up no_dad
+	jexec alcatraz sysctl net.inet6.ip6.forwarding=1
+	jexec alcatraz ndp -s 2001:db8:43::3 00:01:02:03:04:05
+	route add -6 2001:db8:43::/64 2001:db8:42::2
+
+	jexec alcatraz pfctl -e
+	pft_set_rules alcatraz \
+		"block in inet6 proto icmp6 icmp6-type echoreq"
+
+	for redirect in 0 1; do
+		jexec alcatraz sysctl net.inet6.ip6.redirect=${redirect}
+		atf_check -s exit:2 -o ignore \
+		    ping -6 -c 1 -m 1 -W 1 2001:db8:43::3
+		atf_check -o match:"[[:space:]]0 time exceed transit" \
+		    jexec alcatraz netstat -s -p icmp6
+	done
+
+	pft_set_rules alcatraz "pass"
+	atf_check -s exit:2 -o ignore ping -6 -c 1 -m 1 -W 1 2001:db8:43::3
+	atf_check -o match:"[[:space:]]1 time exceed transit" \
+	    jexec alcatraz netstat -s -p icmp6
+}
+
+v6_hlim_cleanup()
+{
+	pft_cleanup
+}
+
 atf_init_test_cases()
 {
 	atf_add_test_case "v4"
 	atf_add_test_case "v6"
+	atf_add_test_case "v6_hlim"
 }