The branch main has been updated by kp:

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

commit e4f2733df8c9d2fd0c5e8fdc8bec002bf39811f3
Author:     Kristof Provost <k...@freebsd.org>
AuthorDate: 2025-01-09 20:28:46 +0000
Commit:     Kristof Provost <k...@freebsd.org>
CommitDate: 2025-01-17 08:41:25 +0000

    pf: add 'allow-related' to always allow SCTP multihome extra connections
    
    Allow users to choose to allow permitted SCTP connections to set up 
additional
    multihomed connections regardless of the ruleset. That is, allow an already
    established connection to set up flows that would otherwise be disallowed.
    
    In case of if-bound connections we initially set the extra associations to
    be floating, because we don't know what path they'll be taking when they're
    created. Once we see the first traffic we can bind them.
    
    MFC after:      2 weeks
    Sponsored by:   Orange Business Services
    Differential Revision:  https://reviews.freebsd.org/D48453
---
 sbin/pfctl/parse.y           | 21 ++++++++++--
 share/man/man5/pf.conf.5     |  6 +++-
 sys/net/pfvar.h              |  1 +
 sys/netpfil/pf/pf.c          | 24 ++++++++++++++
 sys/netpfil/pf/pf.h          |  1 +
 tests/sys/netpfil/pf/sctp.py | 76 ++++++++++++++++++++++++++++++++++++++++++++
 6 files changed, 126 insertions(+), 3 deletions(-)

diff --git a/sbin/pfctl/parse.y b/sbin/pfctl/parse.y
index ff2e2d2c93b7..4a7bd3125372 100644
--- a/sbin/pfctl/parse.y
+++ b/sbin/pfctl/parse.y
@@ -173,7 +173,7 @@ enum        { PF_STATE_OPT_MAX, PF_STATE_OPT_NOSYNC, 
PF_STATE_OPT_SRCTRACK,
            PF_STATE_OPT_MAX_SRC_CONN_RATE, PF_STATE_OPT_MAX_SRC_NODES,
            PF_STATE_OPT_OVERLOAD, PF_STATE_OPT_STATELOCK,
            PF_STATE_OPT_TIMEOUT, PF_STATE_OPT_SLOPPY,
-           PF_STATE_OPT_PFLOW };
+           PF_STATE_OPT_PFLOW, PF_STATE_OPT_ALLOW_RELATED };
 
 enum   { PF_SRCTRACK_NONE, PF_SRCTRACK, PF_SRCTRACK_GLOBAL, PF_SRCTRACK_RULE };
 
@@ -526,7 +526,7 @@ int parseport(char *, struct range *r, int);
 %token DNPIPE DNQUEUE RIDENTIFIER
 %token LOAD RULESET_OPTIMIZATION PRIO
 %token STICKYADDRESS ENDPI MAXSRCSTATES MAXSRCNODES SOURCETRACK GLOBAL RULE
-%token MAXSRCCONN MAXSRCCONNRATE OVERLOAD FLUSH SLOPPY PFLOW
+%token MAXSRCCONN MAXSRCCONNRATE OVERLOAD FLUSH SLOPPY PFLOW ALLOW_RELATED
 %token TAGGED TAG IFBOUND FLOATING STATEPOLICY STATEDEFAULTS ROUTE SETTOS
 %token DIVERTTO DIVERTREPLY BRIDGE_TO RECEIVEDON NE LE GE AFTO
 %token <v.string>              STRING
@@ -2648,6 +2648,14 @@ pfrule           : action dir logquick interface route 
af proto fromto
                                        }
                                        r.rule_flag |= PFRULE_PFLOW;
                                        break;
+                               case PF_STATE_OPT_ALLOW_RELATED:
+                                       if (r.rule_flag & PFRULE_ALLOW_RELATED) 
{
+                                               yyerror("state allow-related 
option: "
+                                                   "multiple definitions");
+                                               YYERROR;
+                                       }
+                                       r.rule_flag |= PFRULE_ALLOW_RELATED;
+                                       break;
                                case PF_STATE_OPT_TIMEOUT:
                                        if (o->data.timeout.number ==
                                            PFTM_ADAPTIVE_START ||
@@ -4490,6 +4498,14 @@ state_opt_item   : MAXIMUM NUMBER                {
                        $$->next = NULL;
                        $$->tail = $$;
                }
+               | ALLOW_RELATED {
+                       $$ = calloc(1, sizeof(struct node_state_opt));
+                       if ($$ == NULL)
+                               err(1, "state_opt_item: calloc");
+                       $$->type = PF_STATE_OPT_ALLOW_RELATED;
+                       $$->next = NULL;
+                       $$->tail = $$;
+               }
                | STRING NUMBER                 {
                        int     i;
 
@@ -6432,6 +6448,7 @@ lookup(char *s)
                { "af-to",              AFTO},
                { "all",                ALL},
                { "allow-opts",         ALLOWOPTS},
+               { "allow-related",      ALLOW_RELATED},
                { "altq",               ALTQ},
                { "anchor",             ANCHOR},
                { "antispoof",          ANTISPOOF},
diff --git a/share/man/man5/pf.conf.5 b/share/man/man5/pf.conf.5
index 7e2c96c07473..a517015b5ff8 100644
--- a/share/man/man5/pf.conf.5
+++ b/share/man/man5/pf.conf.5
@@ -27,7 +27,7 @@
 .\" ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 .\" POSSIBILITY OF SUCH DAMAGE.
 .\"
-.Dd January 9, 2025
+.Dd January 10, 2025
 .Dt PF.CONF 5
 .Os
 .Sh NAME
@@ -2513,6 +2513,10 @@ Cannot be used with modulate or synproxy state.
 States created by this rule are exported on the
 .Xr pflow 4
 interface.
+.It Ar allow-related
+Automatically allow connections related to this one, regardless of rules that
+might otherwise affect them.
+This currently only applies to SCTP multihomed connection.
 .El
 .Pp
 Multiple options can be specified, separated by commas:
diff --git a/sys/net/pfvar.h b/sys/net/pfvar.h
index 3f109d31ccde..6c9d75bb1365 100644
--- a/sys/net/pfvar.h
+++ b/sys/net/pfvar.h
@@ -1660,6 +1660,7 @@ struct pf_pdesc {
 #define PFDESC_SCTP_ADD_IP     0x1000
        u_int16_t        sctp_flags;
        u_int32_t        sctp_initiate_tag;
+       struct pf_krule *related_rule;
 
        struct pf_sctp_multihome_jobs   sctp_multihome_jobs;
 };
diff --git a/sys/netpfil/pf/pf.c b/sys/netpfil/pf/pf.c
index 7b0ee24965b9..07b7aa543dbd 100644
--- a/sys/netpfil/pf/pf.c
+++ b/sys/netpfil/pf/pf.c
@@ -464,6 +464,14 @@ BOUND_IFACE(struct pf_kstate *st, struct pf_pdesc *pd)
        if (st->rule->rt == PF_REPLYTO || (pd->af != pd->naf))
                return (V_pfi_all);
 
+       /*
+        * If this state is created based on another state (e.g. SCTP
+        * multihome) always set it floating initially. We can't know for sure
+        * what interface the actual traffic for this state will come in on.
+        */
+       if (pd->related_rule)
+               return (V_pfi_all);
+
        /* Don't overrule the interface for states created on incoming packets. 
*/
        if (st->direction == PF_IN)
                return (k);
@@ -5702,6 +5710,10 @@ pf_test_rule(struct pf_krule **rm, struct pf_kstate **sm,
        }
 
        while (r != NULL) {
+               if (pd->related_rule) {
+                       *rm = pd->related_rule;
+                       break;
+               }
                pf_counter_u64_add(&r->evaluations, 1);
                PF_TEST_ATTRIB(pfi_kkif_match(r->kif, pd->kif) == r->ifnot,
                        r->skip[PF_SKIP_IFP]);
@@ -7165,6 +7177,15 @@ pf_test_state_sctp(struct pf_kstate **state, struct 
pf_pdesc *pd,
                        dst->scrub->pfss_v_tag = pd->sctp_initiate_tag;
        }
 
+       /*
+        * Bind to the correct interface if we're if-bound. For multihomed
+        * extra associations we don't know which interface that will be until
+        * here, so we've inserted the state on V_pf_all. Fix that now.
+        */
+       if ((*state)->kif == V_pfi_all &&
+           (*state)->rule->rule_flag & PFRULE_IFBOUND)
+               (*state)->kif = pd->kif;
+
        if (pd->sctp_flags & (PFDESC_SCTP_COOKIE | PFDESC_SCTP_HEARTBEAT_ACK)) {
                if (src->state < SCTP_ESTABLISHED) {
                        pf_set_protostate(*state, psrc, SCTP_ESTABLISHED);
@@ -7378,6 +7399,9 @@ again:
                        j->pd.sctp_flags |= PFDESC_SCTP_ADD_IP;
                        PF_RULES_RLOCK();
                        sm = NULL;
+                       if (s->rule->rule_flag & PFRULE_ALLOW_RELATED) {
+                               j->pd.related_rule = s->rule;
+                       }
                        ret = pf_test_rule(&r, &sm,
                            &j->pd, &ra, &rs, NULL);
                        PF_RULES_RUNLOCK();
diff --git a/sys/netpfil/pf/pf.h b/sys/netpfil/pf/pf.h
index 10431d9b77d9..5de85c1fe7ef 100644
--- a/sys/netpfil/pf/pf.h
+++ b/sys/netpfil/pf/pf.h
@@ -617,6 +617,7 @@ struct pf_rule {
 #define        PFRULE_IFBOUND          0x00010000 /* if-bound */
 #define        PFRULE_STATESLOPPY      0x00020000 /* sloppy state tracking */
 #define        PFRULE_PFLOW            0x00040000
+#define        PFRULE_ALLOW_RELATED    0x00080000
 #define        PFRULE_AFTO             0x00200000  /* af-to rule */
 
 #ifdef _KERNEL
diff --git a/tests/sys/netpfil/pf/sctp.py b/tests/sys/netpfil/pf/sctp.py
index 38bb9f2dff74..230dbae0d327 100644
--- a/tests/sys/netpfil/pf/sctp.py
+++ b/tests/sys/netpfil/pf/sctp.py
@@ -426,6 +426,82 @@ class TestSCTP(VnetTestTemplate):
         assert re.search(r"all sctp 192.0.2.4:.*192.0.2.3:1234", states)
         assert re.search(r"all sctp 192.0.2.4:.*192.0.2.2:1234", states)
 
+    @pytest.mark.require_user("root")
+    def test_disallow_related(self):
+        srv_vnet = self.vnet_map["vnet2"]
+
+        ToolsHelper.print_output("/sbin/pfctl -e")
+        ToolsHelper.pf_rules([
+            "block proto sctp",
+            "pass inet proto sctp to 192.0.2.3",
+            "pass on lo"])
+
+        # Sanity check, we can communicate with the primary address.
+        client = SCTPClient("192.0.2.3", 1234)
+        client.send(b"hello", 0)
+        rcvd = self.wait_object(srv_vnet.pipe)
+        print(rcvd)
+        assert rcvd['ppid'] == 0
+        assert rcvd['data'] == "hello"
+
+        # This shouldn't work
+        success=False
+        try:
+            client.newpeer("192.0.2.2")
+            client.send(b"world", 0)
+            rcvd = self.wait_object(srv_vnet.pipe)
+            print(rcvd)
+            assert rcvd['ppid'] == 0
+            assert rcvd['data'] == "world"
+            success=True
+        except:
+            success=False
+        assert not success
+
+        # Check that we have a state for 192.0.2.3, but not 192.0.2.2 to 
192.0.2.1
+        states = ToolsHelper.get_output("/sbin/pfctl -ss")
+        assert re.search(r"all sctp 192.0.2.1:.*192.0.2.3:1234", states)
+        assert not re.search(r"all sctp 192.0.2.1:.*192.0.2.2:1234", states)
+
+    @pytest.mark.require_user("root")
+    def test_allow_related(self):
+        srv_vnet = self.vnet_map["vnet2"]
+
+        ToolsHelper.print_output("/sbin/pfctl -e")
+        ToolsHelper.pf_rules([
+            "set state-policy if-bound",
+            "block proto sctp",
+            "pass inet proto sctp to 192.0.2.3 keep state (allow-related)",
+            "pass on lo"])
+
+        # Sanity check, we can communicate with the primary address.
+        client = SCTPClient("192.0.2.3", 1234)
+        client.send(b"hello", 0)
+        rcvd = self.wait_object(srv_vnet.pipe)
+        print(rcvd)
+        assert rcvd['ppid'] == 0
+        assert rcvd['data'] == "hello"
+
+        success=False
+        try:
+            client.newpeer("192.0.2.2")
+            client.send(b"world", 0)
+            rcvd = self.wait_object(srv_vnet.pipe)
+            print(rcvd)
+            assert rcvd['ppid'] == 0
+            assert rcvd['data'] == "world"
+            success=True
+        finally:
+            # Debug output
+            ToolsHelper.print_output("/sbin/pfctl -ss")
+            ToolsHelper.print_output("/sbin/pfctl -sr -vv")
+        assert success
+
+        # Check that we have a state for 192.0.2.3 and 192.0.2.2 to 192.0.2.1
+        states = ToolsHelper.get_output("/sbin/pfctl -ss")
+        assert re.search(r"epair.*sctp 192.0.2.1:.*192.0.2.3:1234", states)
+        assert re.search(r"epair.*sctp 192.0.2.1:.*192.0.2.2:1234", states)
+
 class TestSCTPv6(VnetTestTemplate):
     REQUIRED_MODULES = ["sctp", "pf"]
     TOPOLOGY = {

Reply via email to