git: 00a793def743 - main - pf: Fix fallout from the STATE_LOOKUP macro removal

From: Mark Johnston <markj_at_FreeBSD.org>
Date: Thu, 27 Aug 2026 13:33:30 UTC
The branch main has been updated by markj:

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

commit 00a793def743b5be8fcbdd39fc1dfa61534ed66c
Author:     Mark Johnston <markj@FreeBSD.org>
AuthorDate: 2026-08-27 13:02:37 +0000
Commit:     Mark Johnston <markj@FreeBSD.org>
CommitDate: 2026-08-27 13:33:10 +0000

    pf: Fix fallout from the STATE_LOOKUP macro removal
    
    Commit 8572367b6814 ("pf: remove STATE_LOOKUP") introduced two seemingly
    unintentional changes with respect to divert(4)-injected packets (i.e.,
    the PACKET_LOOPED case): we no longer return the matching state, and
    direct callers of pf_find_state() now treat matches of diverted packets
    the same as having no matching state at all.
    
    This seems inadvertent, and breaks certain rulesets which use divert-to.
    Fix them, and add a regression test case.
    
    Fixes:          8572367b6814 ("pf: remove STATE_LOOKUP")
    Reviewed by:    kp
    MFC after:      2 weeks
    Sponsored by:   OPNsense
    Sponsored by:   Klara, Inc.
    Differential Revision:  https://reviews.freebsd.org/D59015
---
 sys/netpfil/pf/pf.c              | 17 +++------
 tests/sys/netpfil/pf/route_to.sh | 82 ++++++++++++++++++++++++++++++++++++++++
 2 files changed, 88 insertions(+), 11 deletions(-)

diff --git a/sys/netpfil/pf/pf.c b/sys/netpfil/pf/pf.c
index 9f19f8203cd5..92b441d1306a 100644
--- a/sys/netpfil/pf/pf.c
+++ b/sys/netpfil/pf/pf.c
@@ -2231,14 +2231,9 @@ out:
 			return (PF_DROP);
 		}
 	}
-	if (PACKET_LOOPED(pd)) {
-		PF_STATE_UNLOCK(s);
-		return (PF_PASS);
-	}
 
 	*state = s;
-
-	return (PF_MATCH);
+	return (PACKET_LOOPED(pd) ? PF_PASS : PF_MATCH);
 }
 
 /*
@@ -8560,7 +8555,7 @@ pf_icmp_state_lookup(struct pf_state_key_cmp *key, struct pf_pdesc *pd,
 		return (PF_DROP);
 
 	action = pf_find_state(pd, key, state);
-	if (action != PF_MATCH)
+	if (action != PF_MATCH && action != PF_PASS)
 		return (action);
 
 	if ((*state)->state_flags & PFSTATE_SLOPPY)
@@ -8885,7 +8880,7 @@ pf_test_state_icmp(struct pf_kstate **state, struct pf_pdesc *pd,
 			key.port[pd2.didx] = th->th_dport;
 
 			action = pf_find_state(&pd2, &key, state);
-			if (action != PF_MATCH)
+			if (action != PF_MATCH && action != PF_PASS)
 				return (action);
 
 			if (pd->dir == (*state)->direction) {
@@ -9080,7 +9075,7 @@ pf_test_state_icmp(struct pf_kstate **state, struct pf_pdesc *pd,
 			key.port[pd2.didx] = uh->uh_dport;
 
 			action = pf_find_state(&pd2, &key, state);
-			if (action != PF_MATCH)
+			if (action != PF_MATCH && action != PF_PASS)
 				return (action);
 
 			/* translate source/destination address, if necessary */
@@ -9212,7 +9207,7 @@ pf_test_state_icmp(struct pf_kstate **state, struct pf_pdesc *pd,
 			key.port[pd2.didx] = sh->dest_port;
 
 			action = pf_find_state(&pd2, &key, state);
-			if (action != PF_MATCH)
+			if (action != PF_MATCH && action != PF_PASS)
 				return (action);
 
 			if (pd->dir == (*state)->direction) {
@@ -9598,7 +9593,7 @@ pf_test_state_icmp(struct pf_kstate **state, struct pf_pdesc *pd,
 			key.port[0] = key.port[1] = 0;
 
 			action = pf_find_state(&pd2, &key, state);
-			if (action != PF_MATCH)
+			if (action != PF_MATCH && action != PF_PASS)
 				return (action);
 
 			/* translate source/destination address, if necessary */
diff --git a/tests/sys/netpfil/pf/route_to.sh b/tests/sys/netpfil/pf/route_to.sh
index 1caf2367a9e3..3a3e44f0975d 100644
--- a/tests/sys/netpfil/pf/route_to.sh
+++ b/tests/sys/netpfil/pf/route_to.sh
@@ -1981,6 +1981,87 @@ mcast_v6_local_cleanup()
 	pft_cleanup
 }
 
+atf_test_case "divert_to" "cleanup"
+divert_to_head()
+{
+	atf_set descr 'Test that route-to is applied to divert-injected packets'
+	atf_set require.user root
+	atf_set require.kmods ipdivert
+}
+divert_to_body()
+{
+	pft_init
+
+	# Set up our topology:
+	#
+	# client <--> firewall <--> gateway <--> server
+	#
+	# The firewall has no default route.  route-to forces TCP port 8080
+	# traffic via epair_wan to the gateway; without it the firewall has no
+	# route to 10.4.4.2 and drops the packet.
+
+	epair_cl=$(vnet_mkepair)
+	epair_wan=$(vnet_mkepair)
+	epair_srv=$(vnet_mkepair)
+
+	vnet_mkjail client ${epair_cl}a
+	vnet_mkjail firewall ${epair_cl}b ${epair_wan}a
+	vnet_mkjail gateway ${epair_wan}b ${epair_srv}a
+	vnet_mkjail server ${epair_srv}b
+
+	# Client
+	atf_check jexec client ifconfig ${epair_cl}a 10.1.1.2/24 up
+	atf_check -o ignore jexec client route add default 10.1.1.1
+
+	# Firewall: no default route; route-to sends traffic out via epair_wan
+	atf_check jexec firewall ifconfig ${epair_cl}b 10.1.1.1/24 up
+	atf_check jexec firewall ifconfig ${epair_wan}a 10.3.3.1/30 up
+	atf_check -o ignore jexec firewall sysctl net.inet.ip.forwarding=1
+
+	# Gateway: routes between firewall and server
+	atf_check jexec gateway ifconfig ${epair_wan}b 10.3.3.2/30 up
+	atf_check jexec gateway ifconfig ${epair_srv}a 10.4.4.1/24 up
+	atf_check -o ignore jexec gateway sysctl net.inet.ip.forwarding=1
+	atf_check -o ignore jexec gateway route add -net 10.1.1.0/24 10.3.3.1
+
+	# Server
+	atf_check jexec server ifconfig ${epair_srv}b 10.4.4.2/24 up
+	atf_check -o ignore jexec server route add default 10.4.4.1
+
+	atf_check -o ignore jexec client ping -c 1 10.1.1.1
+
+	jexec server nc -l 8080 > $(pwd)/out &
+	server_pid=$!
+
+	# Firewall pf: divert the flow and route-to via wan
+	jexec firewall pfctl -e
+	pft_set_rules firewall \
+	    "pass in quick on ${epair_cl}b \
+	        route-to (${epair_wan}a 10.3.3.2) \
+	        inet proto tcp from 10.1.1.0/24 to 10.4.4.2 port 8080 \
+	        keep state divert-to 8000" \
+	    "pass in all" \
+	    "pass out all"
+
+	# Divert daemon: receives the SYN and re-injects it
+	jexec firewall ${common_dir}/divapp 8000 divert-back &
+	divapp_pid=$!
+	sleep 1
+
+	# The connection must succeed: route-to must be applied to the
+	# re-injected packet for it to reach the server.
+	echo "hello there" >$(pwd)/in
+	atf_check jexec client nc -N -w 3 10.4.4.2 8080 < $(pwd)/in
+
+	wait $divapp_pid
+	wait $server_pid
+	atf_check -o match:"hello there" cat $(pwd)/out
+}
+divert_to_cleanup()
+{
+	pft_cleanup
+}
+
 atf_init_test_cases()
 {
 	atf_add_test_case "v4"
@@ -2008,6 +2089,7 @@ atf_init_test_cases()
 	atf_add_test_case "mcast_v4_local"
 	atf_add_test_case "mcast_v6_forwarded"
 	atf_add_test_case "mcast_v6_local"
+	atf_add_test_case "divert_to"
 	# Tests for pf_map_addr() without prefer-ipv6-nexthop
 	atf_add_test_case "table_loop"
 	atf_add_test_case "roundrobin"