git: 31828075e456 - main - pf: bind route-to states to their route-to interface

From: Kristof Provost <kp_at_FreeBSD.org>
Date: Mon, 29 Jan 2024 13:52:42 UTC
The branch main has been updated by kp:

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

commit 31828075e456c042cabd47ff4eb127c5c19f16e0
Author:     Kristof Provost <kp@FreeBSD.org>
AuthorDate: 2024-01-25 10:16:49 +0000
Commit:     Kristof Provost <kp@FreeBSD.org>
CommitDate: 2024-01-29 13:10:26 +0000

    pf: bind route-to states to their route-to interface
    
    When we route-to the state should be bound to the route-to interface,
    not the default route interface. However, we should only do so for
    outbound traffic, because inbound traffic should bind on the arriving
    interface, not the one we eventually transmit on.
    
    Explicitly check for this in BOUND_IFACE().
    
    We must also extend pf_find_state(), because subsequent packets within
    the established state will attempt to match the original interface, not
    the route-to interface.
    
    Sponsored by:   Rubicon Communications, LLC ("Netgate")
    Differential Revision:  https://reviews.freebsd.org/D43589
---
 sys/netpfil/pf/pf.c              | 27 +++++++++++++++++++++----
 tests/sys/netpfil/pf/route_to.sh | 43 ++++++++++++++++++++++++++++++++++++++++
 2 files changed, 66 insertions(+), 4 deletions(-)

diff --git a/sys/netpfil/pf/pf.c b/sys/netpfil/pf/pf.c
index 2dc6d02d330a..36ff0eac16ad 100644
--- a/sys/netpfil/pf/pf.c
+++ b/sys/netpfil/pf/pf.c
@@ -412,8 +412,27 @@ VNET_DEFINE(struct pf_limit, pf_limits[PF_LIMIT_MAX]);
 			return (PF_PASS);				\
 	} while (0)
 
-#define	BOUND_IFACE(r, k) \
-	((r)->rule_flag & PFRULE_IFBOUND) ? (k) : V_pfi_all
+static struct pfi_kkif *
+BOUND_IFACE(struct pf_krule *r, struct pfi_kkif *k, struct pf_pdesc *pd)
+{
+	/* Floating unless otherwise specified. */
+	if (! (r->rule_flag & PFRULE_IFBOUND))
+		return (V_pfi_all);
+
+	/* Don't overrule the interface for states created on incoming packets. */
+	if (pd->dir == PF_IN)
+		return (k);
+
+	/* No route-to, so don't overrrule. */
+	if (r->rt != PF_ROUTETO)
+		return (k);
+
+	if (r->rpool.cur == NULL)
+		return (k);
+
+	/* Bind to the route-to interface. */
+	return (r->rpool.cur->kif);
+}
 
 #define	STATE_INC_COUNTERS(s)						\
 	do {								\
@@ -1600,7 +1619,7 @@ pf_find_state(struct pfi_kkif *kif, struct pf_state_key_cmp *key, u_int dir)
 
 	/* List is sorted, if-bound states before floating ones. */
 	TAILQ_FOREACH(s, &sk->states[idx], key_list[idx])
-		if (s->kif == V_pfi_all || s->kif == kif) {
+		if (s->kif == V_pfi_all || s->kif == kif || s->orig_kif == kif) {
 			PF_STATE_LOCK(s);
 			PF_HASHROW_UNLOCK(kh);
 			if (__predict_false(s->timeout >= PFTM_MAX)) {
@@ -4999,7 +5018,7 @@ pf_create_state(struct pf_krule *r, struct pf_krule *nr, struct pf_krule *a,
 		    __func__, nr, sk, nk));
 
 	/* Swap sk/nk for PF_OUT. */
-	if (pf_state_insert(BOUND_IFACE(r, kif), kif,
+	if (pf_state_insert(BOUND_IFACE(r, kif, pd), kif,
 	    (pd->dir == PF_IN) ? sk : nk,
 	    (pd->dir == PF_IN) ? nk : sk, s)) {
 		REASON_SET(&reason, PFRES_STATEINS);
diff --git a/tests/sys/netpfil/pf/route_to.sh b/tests/sys/netpfil/pf/route_to.sh
index d8cfb1b22d8b..7e8310bceb30 100644
--- a/tests/sys/netpfil/pf/route_to.sh
+++ b/tests/sys/netpfil/pf/route_to.sh
@@ -365,6 +365,48 @@ dummynet_cleanup()
 	pft_cleanup
 }
 
+atf_test_case "ifbound" "cleanup"
+ifbound_head()
+{
+	atf_set descr 'Test that route-to states bind the expected interface'
+	atf_set require.user root
+}
+
+ifbound_body()
+{
+	pft_init
+
+	j="route_to:ifbound"
+
+	epair_one=$(vnet_mkepair)
+	epair_two=$(vnet_mkepair)
+	ifconfig ${epair_one}b up
+
+	vnet_mkjail ${j}2 ${epair_two}b
+	jexec ${j}2 ifconfig ${epair_two}b inet 198.51.100.2/24 up
+	jexec ${j}2 ifconfig ${epair_two}b inet alias 203.0.113.1/24
+	jexec ${j}2 route add default 198.51.100.1
+
+	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 192.0.2.2
+
+	jexec $j pfctl -e
+	pft_set_rules $j \
+		"set state-policy if-bound" \
+		"block" \
+		"pass out route-to (${epair_two}a 198.51.100.2)"
+
+	atf_check -s exit:0 -o ignore \
+	    jexec $j ping -c 3 203.0.113.1
+}
+
+ifbound_cleanup()
+{
+	pft_cleanup
+}
+
 atf_init_test_cases()
 {
 	atf_add_test_case "v4"
@@ -373,4 +415,5 @@ atf_init_test_cases()
 	atf_add_test_case "multiwanlocal"
 	atf_add_test_case "icmp_nat"
 	atf_add_test_case "dummynet"
+	atf_add_test_case "ifbound"
 }