git: a983cea4e9a8 - main - pf: fix reply-to after rdr and dummynet

From: Kristof Provost <kp_at_FreeBSD.org>
Date: Thu, 28 Mar 2024 16:06:41 UTC
The branch main has been updated by kp:

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

commit a983cea4e9a8dcd52cfd6a3141d7aa03306b057b
Author:     Kristof Provost <kp@FreeBSD.org>
AuthorDate: 2024-03-27 14:47:21 +0000
Commit:     Kristof Provost <kp@FreeBSD.org>
CommitDate: 2024-03-28 16:06:01 +0000

    pf: fix reply-to after rdr and dummynet
    
    If we redirect a packet to localhost and it gets dummynet'd it may be
    re-injected later (e.g. when delayed) which means it will be passed
    through ip_input() again. ip_input() will then reject the packet because
    it's directed to the loopback address, but did not arrive on a loopback
    interface.
    
    Fix this by having pf set the rcvif to V_iflo if we redirect to
    loopback.
    
    See also:       https://redmine.pfsense.org/issues/15363
    Sponsored by:   Rubicon Communications, LLC ("Netgate")
---
 sys/netpfil/pf/pf.c              | 12 ++++++++
 tests/sys/netpfil/pf/route_to.sh | 61 ++++++++++++++++++++++++++++++++++++++++
 2 files changed, 73 insertions(+)

diff --git a/sys/netpfil/pf/pf.c b/sys/netpfil/pf/pf.c
index 50dc67b72439..4cec0936539e 100644
--- a/sys/netpfil/pf/pf.c
+++ b/sys/netpfil/pf/pf.c
@@ -7954,6 +7954,18 @@ pf_dummynet_route(struct pf_pdesc *pd, struct pf_kstate *s,
 				    sizeof(struct sockaddr_in6));
 		}
 
+		if (s != NULL && s->nat_rule.ptr != NULL &&
+		    s->nat_rule.ptr->action == PF_RDR &&
+		    ((pd->af == AF_INET && IN_LOOPBACK(ntohl(pd->dst->v4.s_addr))) ||
+		    (pd->af == AF_INET6 && IN6_IS_ADDR_LOOPBACK(&pd->dst->v6)))) {
+			/*
+			 * If we're redirecting to loopback mark this packet
+			 * as being local. Otherwise it might get dropped
+			 * if dummynet re-injects.
+			 */
+			(*m0)->m_pkthdr.rcvif = V_loif;
+		}
+
 		if (pf_pdesc_to_dnflow(pd, r, s, &dnflow)) {
 			pd->pf_mtag->flags |= PF_MTAG_FLAG_DUMMYNET;
 			pd->pf_mtag->flags |= PF_MTAG_FLAG_DUMMYNETED;
diff --git a/tests/sys/netpfil/pf/route_to.sh b/tests/sys/netpfil/pf/route_to.sh
index 44fe6786e896..df95eaecc12e 100644
--- a/tests/sys/netpfil/pf/route_to.sh
+++ b/tests/sys/netpfil/pf/route_to.sh
@@ -626,6 +626,66 @@ ifbound_reply_to_v6_cleanup()
 	pft_cleanup
 }
 
+atf_test_case "ifbound_reply_to_rdr_dummynet" "cleanup"
+ifbound_reply_to_rdr_dummynet_head()
+{
+	atf_set descr 'Test that reply-to states bind to the expected non-default-route interface after rdr and dummynet'
+	atf_set require.user root
+}
+
+ifbound_reply_to_rdr_dummynet_body()
+{
+	dummynet_init
+
+	j="route_to:ifbound_reply_to_rdr_dummynet"
+
+	epair_one=$(vnet_mkepair)
+	epair_two=$(vnet_mkepair)
+	ifconfig ${epair_one}b inet 192.0.2.2/24 up
+	ifconfig ${epair_two}b up
+
+	vnet_mkjail $j ${epair_one}a ${epair_two}a
+	jexec $j ifconfig lo0 inet 127.0.0.1/8 up
+	jexec $j ifconfig ${epair_one}a 192.0.2.1/24 up
+	jexec $j ifconfig ${epair_two}a 198.51.100.1/24 up
+	jexec $j route add default 198.51.100.254
+
+	jexec $j pfctl -e
+	jexec $j dnctl pipe 1 config delay 1
+	pft_set_rules $j \
+		"set state-policy if-bound" \
+		"rdr on ${epair_one}a proto icmp from any to 192.0.2.1 -> 127.0.0.1" \
+		"rdr on ${epair_two}a proto icmp from any to 198.51.100.1 -> 127.0.0.1" \
+		"match in on ${epair_one}a inet all dnpipe (1, 1)" \
+		"pass in on ${epair_one}a reply-to (${epair_one}a 192.0.2.2) inet from any to 127.0.0.1 keep state"
+
+	atf_check -s exit:0 -o ignore \
+	    ping -c 3 192.0.2.1
+
+	atf_check -s exit:0 \
+	    ${common_dir}/pft_ping.py \
+	    --to 192.0.2.1 \
+	    --from 203.0.113.2 \
+	    --sendif ${epair_one}b \
+	    --replyif ${epair_one}b
+
+	# pft_ping uses the same ID every time, so this will look like more traffic in the same state
+	atf_check -s exit:0 \
+	    ${common_dir}/pft_ping.py \
+	    --to 192.0.2.1 \
+	    --from 203.0.113.2 \
+	    --sendif ${epair_one}b \
+	    --replyif ${epair_one}b
+
+	jexec $j pfctl -sr -vv
+	jexec $j pfctl -ss -vv
+}
+
+ifbound_reply_to_rdr_dummynet_cleanup()
+{
+	pft_cleanup
+}
+
 atf_test_case "dummynet_frag" "cleanup"
 dummynet_frag_head()
 {
@@ -740,6 +800,7 @@ atf_init_test_cases()
 	atf_add_test_case "ifbound_v6"
 	atf_add_test_case "ifbound_reply_to"
 	atf_add_test_case "ifbound_reply_to_v6"
+	atf_add_test_case "ifbound_reply_to_rdr_dummynet"
 	atf_add_test_case "dummynet_frag"
 	atf_add_test_case "dummynet_double"
 }