git: 6460322a0a51 - main - pf: support if-bound with reply-to

From: Kristof Provost <kp_at_FreeBSD.org>
Date: Fri, 01 Mar 2024 12:19:50 UTC
The branch main has been updated by kp:

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

commit 6460322a0a512f4e2c263bee54fc8bf46091f4cd
Author:     Kristof Provost <kp@FreeBSD.org>
AuthorDate: 2024-02-02 20:56:55 +0000
Commit:     Kristof Provost <kp@FreeBSD.org>
CommitDate: 2024-03-01 08:39:43 +0000

    pf: support if-bound with reply-to
    
    On reply-to we don't know what interface to bind to when we create
    the state. Create any reply-to state as floating, but bind to the
    appropriate interface once we're handling the reply.
    
    See also:       https://redmine.pfsense.org/issues/15220
    Sponsored by:   Rubicon Communications, LLC ("Netgate")
---
 sys/netpfil/pf/pf.c              | 33 ++++++++++++++++++++++-
 tests/sys/netpfil/pf/route_to.sh | 56 ++++++++++++++++++++++++++++++++++++++++
 2 files changed, 88 insertions(+), 1 deletion(-)

diff --git a/sys/netpfil/pf/pf.c b/sys/netpfil/pf/pf.c
index 46f01f986d05..b1f93f605b4f 100644
--- a/sys/netpfil/pf/pf.c
+++ b/sys/netpfil/pf/pf.c
@@ -423,6 +423,13 @@ BOUND_IFACE(struct pf_kstate *st, struct pfi_kkif *k)
 	if (! (st->rule.ptr->rule_flag & PFRULE_IFBOUND))
 		return (V_pfi_all);
 
+	/*
+	 * Initially set to all, because we don't know what interface we'll be
+	 * sending this out when we create the state.
+	 */
+	if (st->rule.ptr->rt == PF_REPLYTO)
+		return (V_pfi_all);
+
 	/* Don't overrule the interface for states created on incoming packets. */
 	if (st->direction == PF_IN)
 		return (k);
@@ -7317,15 +7324,27 @@ pf_route(struct mbuf **m, struct pf_krule *r, struct ifnet *oifp,
 			dst.sin_addr.s_addr = naddr.v4.s_addr;
 		ifp = nkif ? nkif->pfik_ifp : NULL;
 	} else {
+		struct pfi_kkif *kif;
+
 		if (!PF_AZERO(&s->rt_addr, AF_INET))
 			dst.sin_addr.s_addr =
 			    s->rt_addr.v4.s_addr;
 		ifp = s->rt_kif ? s->rt_kif->pfik_ifp : NULL;
+		kif = s->rt_kif;
 		/* If pfsync'd */
 		if (ifp == NULL && r->rpool.cur != NULL) {
 			ifp = r->rpool.cur->kif ?
 			    r->rpool.cur->kif->pfik_ifp : NULL;
+			kif = r->rpool.cur->kif;
+		}
+		if (ifp != NULL && kif != NULL &&
+		    r->rule_flag & PFRULE_IFBOUND &&
+		    r->rt == PF_REPLYTO &&
+		    s->kif == V_pfi_all) {
+			s->kif = kif;
+			s->orig_kif = oifp->if_pf_kif;
 		}
+
 		PF_STATE_UNLOCK(s);
 	}
 
@@ -7538,14 +7557,26 @@ pf_route6(struct mbuf **m, struct pf_krule *r, struct ifnet *oifp,
 			    &naddr, AF_INET6);
 		ifp = nkif ? nkif->pfik_ifp : NULL;
 	} else {
+		struct pfi_kkif *kif;
+
 		if (!PF_AZERO(&s->rt_addr, AF_INET6))
 			PF_ACPY((struct pf_addr *)&dst.sin6_addr,
 			    &s->rt_addr, AF_INET6);
 		ifp = s->rt_kif ? s->rt_kif->pfik_ifp : NULL;
+		kif = s->rt_kif;
 		/* If pfsync'd */
-		if (ifp == NULL && r->rpool.cur != NULL)
+		if (ifp == NULL && r->rpool.cur != NULL) {
 			ifp = r->rpool.cur->kif ?
 			    r->rpool.cur->kif->pfik_ifp : NULL;
+			kif = r->rpool.cur->kif;
+		}
+		if (ifp != NULL && kif != NULL &&
+		    r->rule_flag & PFRULE_IFBOUND &&
+		    r->rt == PF_REPLYTO &&
+		    s->kif == V_pfi_all) {
+			s->kif = kif;
+			s->orig_kif = oifp->if_pf_kif;
+		}
 	}
 
 	if (s)
diff --git a/tests/sys/netpfil/pf/route_to.sh b/tests/sys/netpfil/pf/route_to.sh
index 31a47e75c82e..5223381d9c24 100644
--- a/tests/sys/netpfil/pf/route_to.sh
+++ b/tests/sys/netpfil/pf/route_to.sh
@@ -407,6 +407,61 @@ ifbound_cleanup()
 	pft_cleanup
 }
 
+atf_test_case "ifbound_reply_to" "cleanup"
+ifbound_reply_to_head()
+{
+	atf_set descr 'Test that reply-to states bind to the expected interface'
+	atf_set require.user root
+}
+
+ifbound_reply_to_body()
+{
+	pft_init
+
+	j="route_to:ifbound_reply_to"
+
+	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 ${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
+	pft_set_rules $j \
+		"set state-policy if-bound" \
+		"block" \
+		"pass in on ${epair_one}a reply-to (${epair_one}a 192.0.2.2) inet from any to 192.0.2.0/24 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 -ss -vv
+}
+
+ifbound_reply_to_cleanup()
+{
+	pft_cleanup
+}
+
 atf_test_case "dummynet_frag" "cleanup"
 dummynet_frag_head()
 {
@@ -465,5 +520,6 @@ atf_init_test_cases()
 	atf_add_test_case "icmp_nat"
 	atf_add_test_case "dummynet"
 	atf_add_test_case "ifbound"
+	atf_add_test_case "ifbound_reply_to"
 	atf_add_test_case "dummynet_frag"
 }