The branch main has been updated by kp:

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

commit 4616481212302b5d875cfc7a00766af017318f7f
Author:     Kristof Provost <[email protected]>
AuthorDate: 2025-12-30 19:06:48 +0000
Commit:     Kristof Provost <[email protected]>
CommitDate: 2026-01-14 06:44:38 +0000

    pf: introduce source and state limiters
    
    both source and state limiters can provide constraints on the number
    of states that a set of rules can create, and optionally the rate
    at which they are created. state limiters have a single limit, but
    source limiters apply limits against a source address (or network).
    the source address entries are dynamically created and destroyed,
    and are also limited.
    
    this started out because i was struggling to understand the source and
    state tracking options in pf.conf, and looking at the code made it
    worse. it looked like some functionality was missing, and the code also
    did some things that surprised me. taking a step back from it, even it
    if did work, what is described doesn't work well outside very simple
    environments.
    
    the functionality i'm talking about is most of the stuff in the
    Stateful Tracking Options section of pf.conf(4).
    
    some of the problems are illustrated one of the simplest options:
    the "max number" option that limits the number of states that a
    rule is allowed to create:
    
    - wiring limits up to rules is a problem because when you load a
      new ruleset the limit is reset, allowing more states to be created
      than you intended.
    - a single "rule" in pf.conf can expand to multiple rules in the
      kernel thanks to things like macro expansion for multiple ports.
      "max 1000" on a line in pf.conf could end up being many times
      that in effect.
    - when a state limit on a rule is reached, the packet is dropped.
      this makes it difficult to do other things with the packet, such a
      redirect it to a tarpit or another server that replies with an
      outage notices or such.
    
    a state limiter solves these problems. the example from the pf.conf.5
    change demonstrates this:
    
         An example use case for a state limiter is to restrict the number of
         connections allowed to a service that is accessible via multiple
         protocols, e.g. a DNS server that can be accessed by both TCP and UDP 
on
         port 53, DNS-over-TLS on TCP port 853, and DNS-over-HTTPS on TCP port 
443
         can be limited to 1000 concurrent connections:
    
               state limiter "dns-server" id 1 limit 1000
    
               pass in proto { tcp udp } to port domain state limiter 
"dns-server"
               pass in proto tcp to port { 853 443 } state limiter "dns-server"
    
    a single limit across all these protocols can't be implemented with
    per rule state limits, and any limits that were applied are reset
    if the ruleset is reloaded.
    
    the existing source-track implementation appears to be incomplete,
    i could only see code for "source-track global", but not "source-track
    rule". source-track global is too heavy and unweildy a hammer, and
    source-track rule would suffer the same issues around rule lifetimes
    and expansions that the "max number" state tracking config above has.
    
    a slightly expanded example from the pf.conf.5 change for source limiters:
    
         An example use for a source limiter is the mitigation of denial of
         service caused by the exhaustion of firewall resources by network or 
port
         scans from outside the network.  The states created by any one scanner
         from any one source address can be limited to avoid impacting other
         sources.  Below, up to 10000 IPv4 hosts and IPv6 /64 networks from the
         external network are each limited to a maximum of 1000 connections, and
         are rate limited to creating 100 states over a 10 second interval:
    
               source limiter "internet" id 1 entries 10000 \
                       limit 1000 rate 100/10 \
                       inet6 mask 64
    
               block in on egress
               pass in quick on egress source limiter "internet"
               pass in on egress proto tcp probability 20% rdr-to $tarpit
    
    the extra bit is if the source limiter doesn't have "space" for the
    state, the rule doesn't match and you can fall through to tarpitting
    20% of the tcp connections for fun.
    
    i've been using this in anger in production for over 3 years now.
    
    sashan@ has been poking me along (slowly) to get it in a good enough
    shape for the tree for a long time. it's been one of those years.
    
    bluhm@ says this doesnt break the regress tests.
    ok sashan@
    
    Obtained from:  OpenBSD, dlg <[email protected]>, 8463cae72e
    Sponsored by:   Rubicon Communications, LLC ("Netgate")
---
 lib/libpfctl/libpfctl.c   |   5 +
 lib/libpfctl/libpfctl.h   |   2 +
 sbin/pfctl/parse.y        | 497 ++++++++++++++++++++++++-
 sbin/pfctl/pfctl.8        |  22 +-
 sbin/pfctl/pfctl.c        | 780 ++++++++++++++++++++++++++++++++++-----
 sbin/pfctl/pfctl_parser.c |  55 +++
 sbin/pfctl/pfctl_parser.h |  33 ++
 share/man/man5/pf.conf.5  | 160 +++++++-
 sys/net/pfvar.h           | 414 ++++++++++++++++++++-
 sys/netpfil/pf/pf.c       | 647 ++++++++++++++++++++++++++++++++
 sys/netpfil/pf/pf_ioctl.c | 922 ++++++++++++++++++++++++++++++++++++++++++++++
 sys/netpfil/pf/pf_nl.c    |   4 +
 sys/netpfil/pf/pf_nl.h    |   2 +
 sys/netpfil/pf/pf_table.c |  20 +
 14 files changed, 3458 insertions(+), 105 deletions(-)

diff --git a/lib/libpfctl/libpfctl.c b/lib/libpfctl/libpfctl.c
index f8c92a5cd319..c3fdaf70ad0d 100644
--- a/lib/libpfctl/libpfctl.c
+++ b/lib/libpfctl/libpfctl.c
@@ -1313,6 +1313,9 @@ snl_add_msg_attr_pf_rule(struct snl_writer *nw, uint32_t 
type, const struct pfct
        snl_add_msg_attr_ip6(nw, PF_RT_DIVERT_ADDRESS, &r->divert.addr.v6);
        snl_add_msg_attr_u16(nw, PF_RT_DIVERT_PORT, r->divert.port);
 
+       snl_add_msg_attr_u8(nw, PF_RT_STATE_LIMIT, r->statelim);
+       snl_add_msg_attr_u8(nw, PF_RT_SOURCE_LIMIT, r->sourcelim);
+
        snl_end_attr_nested(nw, off);
 }
 
@@ -1704,6 +1707,8 @@ static struct snl_attr_parser ap_getrule[] = {
        { .type = PF_RT_TYPE_2, .off = _OUT(r.type), .cb = snl_attr_get_uint16 
},
        { .type = PF_RT_CODE_2, .off = _OUT(r.code), .cb = snl_attr_get_uint16 
},
        { .type = PF_RT_EXPTIME, .off = _OUT(r.exptime), .cb = 
snl_attr_get_time_t },
+       { .type = PF_RT_STATE_LIMIT, .off = _OUT(r.statelim), .cb = 
snl_attr_get_uint8 },
+       { .type = PF_RT_SOURCE_LIMIT, .off = _OUT(r.sourcelim), .cb = 
snl_attr_get_uint8 },
 };
 #undef _OUT
 SNL_DECLARE_PARSER(getrule_parser, struct genlmsghdr, snl_f_p_empty, 
ap_getrule);
diff --git a/lib/libpfctl/libpfctl.h b/lib/libpfctl/libpfctl.h
index b885497ab0e8..785ac2bc7fd7 100644
--- a/lib/libpfctl/libpfctl.h
+++ b/lib/libpfctl/libpfctl.h
@@ -249,6 +249,8 @@ struct pfctl_rule {
        struct pf_rule_gid       gid;
        char                     rcv_ifname[IFNAMSIZ];
        bool                     rcvifnot;
+       uint8_t                  statelim;
+       uint8_t                  sourcelim;
 
        uint32_t                 rule_flag;
        uint8_t                  action;
diff --git a/sbin/pfctl/parse.y b/sbin/pfctl/parse.y
index 127e2c257d69..ded74a6391f1 100644
--- a/sbin/pfctl/parse.y
+++ b/sbin/pfctl/parse.y
@@ -72,6 +72,8 @@
 #include "pfctl_parser.h"
 #include "pfctl.h"
 
+#define        ISSET(_v, _m)   ((_v) & (_m))
+
 static struct pfctl    *pf = NULL;
 static int              debug = 0;
 static int              rulestate = 0;
@@ -178,7 +180,8 @@ 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_ALLOW_RELATED };
+           PF_STATE_OPT_PFLOW, PF_STATE_OPT_ALLOW_RELATED,
+           PF_STATE_OPT_STATELIM, PF_STATE_OPT_SOURCELIM };
 
 enum   { PF_SRCTRACK_NONE, PF_SRCTRACK, PF_SRCTRACK_GLOBAL, PF_SRCTRACK_RULE };
 
@@ -284,6 +287,8 @@ static struct filter_opts {
        u_int32_t                tos;
        u_int32_t                prob;
        u_int32_t                ridentifier;
+       u_int32_t                statelim;
+       u_int32_t                sourcelim;
        struct {
                int                      action;
                struct node_state_opt   *options;
@@ -362,6 +367,51 @@ static struct table_opts {
        struct node_tinithead   init_nodes;
 } table_opts;
 
+struct statelim_opts {
+       unsigned int             marker;
+#define        STATELIM_M_ID           0x01
+#define        STATELIM_M_LIMIT        0x02
+#define        STATELIM_M_RATE         0x04
+
+       uint32_t                 id;
+       char                     name[PF_STATELIM_NAME_LEN];
+       unsigned int     limit;
+       struct {
+               unsigned int     limit;
+               unsigned int     seconds;
+       } rate;
+};
+
+static struct statelim_opts statelim_opts;
+
+struct sourcelim_opts {
+       unsigned int             marker;
+#define        SOURCELIM_M_ID                  0x01
+#define        SOURCELIM_M_ENTRIES             0x02
+#define        SOURCELIM_M_LIMIT               0x04
+#define        SOURCELIM_M_RATE                0x08
+#define        SOURCELIM_M_TABLE               0x10
+#define        SOURCELIM_M_INET_MASK   0x20
+#define        SOURCELIM_M_INET6_MASK  0x40
+
+       uint32_t                         id;
+       unsigned int             entries;
+       unsigned int             limit;
+       struct {
+               unsigned int     limit;
+               unsigned int     seconds;
+       } rate;
+       struct {
+               char                     name[PF_TABLE_NAME_SIZE];
+               unsigned int     above;
+               unsigned int     below;
+       } table;
+       unsigned int             inet_mask;
+       unsigned int             inet6_mask;
+};
+
+static struct sourcelim_opts sourcelim_opts;
+
 static struct codel_opts        codel_opts;
 static struct node_hfsc_opts    hfsc_opts;
 static struct node_fairq_opts   fairq_opts;
@@ -513,6 +563,8 @@ typedef struct {
                struct node_hfsc_opts    hfsc_opts;
                struct node_fairq_opts   fairq_opts;
                struct codel_opts        codel_opts;
+               struct statelim_opts    *statelim_opts;
+               struct sourcelim_opts   *sourcelim_opts;
                struct pfctl_watermarks *watermarks;
        } v;
        int lineno;
@@ -548,12 +600,13 @@ int       parseport(char *, struct range *r, int);
 %token TAGGED TAG IFBOUND FLOATING STATEPOLICY STATEDEFAULTS ROUTE SETTOS
 %token DIVERTTO DIVERTREPLY BRIDGE_TO RECEIVEDON NE LE GE AFTO NATTO RDRTO
 %token BINATTO MAXPKTRATE MAXPKTSIZE IPV6NH
+%token LIMITER ID RATE SOURCE ENTRIES ABOVE BELOW MASK
 %token <v.string>              STRING
 %token <v.number>              NUMBER
 %token <v.i>                   PORTBINARY
 %type  <v.interface>           interface if_list if_item_not if_item
 %type  <v.number>              number icmptype icmp6type uid gid
-%type  <v.number>              tos not yesno optnodf
+%type  <v.number>              tos not yesno optnodf sourcelim_opt_below
 %type  <v.probability>         probability
 %type  <v.i>                   no dir af fragcache optimizer syncookie_val
 %type  <v.i>                   sourcetrack flush unaryop statelock
@@ -610,12 +663,19 @@ int       parseport(char *, struct range *r, int);
 %type  <v.etheraddr>           etherfrom etherto
 %type  <v.bridge_to>           bridge
 %type  <v.mac>                 xmac mac mac_list macspec
+%type  <v.string>                      statelim_nm sourcelim_nm
+%type  <v.number>                      statelim_id sourcelim_id
+%type  <v.number>                      statelim_filter_opt sourcelim_filter_opt
+%type  <v.statelim_opts>       statelim_opts
+%type  <v.sourcelim_opts>      sourcelim_opts
 %%
 
 ruleset                : /* empty */
                | ruleset include '\n'
                | ruleset '\n'
                | ruleset option '\n'
+               | ruleset statelim '\n'
+               | ruleset sourcelim '\n'
                | ruleset etherrule '\n'
                | ruleset etheranchorrule '\n'
                | ruleset scrubrule '\n'
@@ -2322,6 +2382,401 @@ qassign_item    : STRING                        {
                }
                ;
 
+statelim               : statelim_nm statelim_opts {
+                       struct pfctl_statelim *stlim;
+                       size_t len;
+
+                       if (!ISSET($2->marker, STATELIM_M_ID)) {
+                               yyerror("id not specified");
+                               free($1);
+                               YYERROR;
+                       }
+                       if (!ISSET($2->marker, STATELIM_M_LIMIT)) {
+                               yyerror("limit not specified");
+                               free($1);
+                               YYERROR;
+                       }
+
+                       stlim = calloc(1, sizeof(*stlim));
+                       if (stlim == NULL)
+                               err(1, "state limiter: malloc");
+
+                       len = strlcpy(stlim->ioc.name, $1,
+                           sizeof(stlim->ioc.name));
+                       free($1);
+                       if (len >= sizeof(stlim->ioc.name)) {
+                               /* abort? */
+                               YYERROR;
+                       }
+
+                       stlim->ioc.id = $2->id;
+                       stlim->ioc.limit = $2->limit;
+                       stlim->ioc.rate.limit = $2->rate.limit;
+                       stlim->ioc.rate.seconds = $2->rate.seconds;
+
+                       if (pfctl_add_statelim(pf, stlim) != 0) {
+                               yyerror("state limiter %s id %u"
+                                   " already exists",
+                                   stlim->ioc.name, stlim->ioc.id);
+                               free(stlim);
+                               YYERROR;
+                       }
+               }
+               ;
+
+statelim_nm            : STATE LIMITER string {
+                       size_t len = strlen($3);
+                       if (len < 1) {
+                               yyerror("state limiter name is too short");
+                               free($3);
+                               YYERROR;
+                       }
+                       if (len >= PF_STATELIM_NAME_LEN) {
+                               yyerror("state limiter name is too long");
+                               free($3);
+                               YYERROR;
+                       }
+                       $$ = $3;
+               }
+               ;
+
+statelim_id            : ID NUMBER {
+                       if ($2 < PF_STATELIM_ID_MIN ||
+                           $2 > PF_STATELIM_ID_MAX) {
+                               yyerror("state limiter id %lld: "
+                                   "invalid identifier", $2);
+                               YYERROR;
+                       }
+
+                       $$ = $2;
+               }
+               ;
+
+statelim_opts          : /* empty */ {
+                       yyerror("state limiter missing options");
+                       YYERROR;
+               }
+               | {
+                       memset(&statelim_opts, 0, sizeof(statelim_opts));
+               } statelim_opts_l {
+                       $$ = &statelim_opts;
+               }
+               ;
+
+statelim_opts_l                : statelim_opts_l statelim_opt
+               | statelim_opt
+               ;
+
+statelim_opt           : statelim_id {
+                       if (ISSET(statelim_opts.marker, STATELIM_M_ID)) {
+                               yyerror("id cannot be respecified");
+                               YYERROR;
+                       }
+
+                       statelim_opts.id = $1;
+
+                       statelim_opts.marker |= STATELIM_M_ID;
+               }
+               | LIMIT NUMBER  {
+                       if (ISSET(statelim_opts.marker, STATELIM_M_LIMIT)) {
+                               yyerror("limit cannot be respecified");
+                               YYERROR;
+                       }
+
+                       if ($2 < PF_STATELIM_LIMIT_MIN ||
+                           $2 > PF_STATELIM_LIMIT_MAX) {
+                               yyerror("invalid state limiter limit");
+                               YYERROR;
+                       }
+
+                       statelim_opts.limit = $2;
+
+                       statelim_opts.marker |= STATELIM_M_LIMIT;
+               }
+               | RATE NUMBER '/' NUMBER {
+                       if (ISSET(statelim_opts.marker, STATELIM_M_RATE)) {
+                               yyerror("rate cannot be respecified");
+                               YYERROR;
+                       }
+                       if ($2 < 1) {
+                               yyerror("invalid rate limit %lld", $2);
+                               YYERROR;
+                       }
+                       if ($4 < 1) {
+                               yyerror("invalid rate seconds %lld", $4);
+                               YYERROR;
+                       }
+
+                       statelim_opts.rate.limit = $2;
+                       statelim_opts.rate.seconds = $4;
+
+                       statelim_opts.marker |= STATELIM_M_RATE;
+               }
+               ;
+
+statelim_filter_opt
+               : statelim_nm {
+                       struct pfctl_statelim *stlim;
+
+                       stlim = pfctl_get_statelim_nm(pf, $1);
+                       free($1);
+                       if (stlim == NULL) {
+                               yyerror("state limiter not found");
+                               YYERROR;
+                       }
+
+                       $$ = stlim->ioc.id;
+               }
+               | STATE LIMITER statelim_id {
+                       $$ = $3;
+               }
+               ;
+
+sourcelim              : sourcelim_nm sourcelim_opts {
+                       struct pfctl_sourcelim *srlim;
+                       size_t len;
+
+                       if (!ISSET($2->marker, SOURCELIM_M_ID)) {
+                               yyerror("id not specified");
+                               free($1);
+                               YYERROR;
+                       }
+                       if (!ISSET($2->marker, SOURCELIM_M_ENTRIES)) {
+                               yyerror("entries not specified");
+                               free($1);
+                               YYERROR;
+                       }
+                       if (!ISSET($2->marker, SOURCELIM_M_LIMIT)) {
+                               yyerror("state limit not specified");
+                               free($1);
+                               YYERROR;
+                       }
+
+                       srlim = calloc(1, sizeof(*srlim));
+                       if (srlim == NULL)
+                               err(1, "source limiter: malloc");
+
+                       len = strlcpy(srlim->ioc.name, $1,
+                           sizeof(srlim->ioc.name));
+                       free($1);
+                       if (len >= sizeof(srlim->ioc.name)) {
+                               /* abort? */
+                               YYERROR;
+                       }
+
+                       srlim->ioc.id = $2->id;
+                       srlim->ioc.entries = $2->entries;
+                       srlim->ioc.limit = $2->limit;
+                       srlim->ioc.rate.limit = $2->rate.limit;
+                       srlim->ioc.rate.seconds = $2->rate.seconds;
+
+                       if (ISSET($2->marker, SOURCELIM_M_TABLE)) {
+                               if (strlcpy(srlim->ioc.overload_tblname,
+                                   $2->table.name,
+                                   sizeof(srlim->ioc.overload_tblname)) >=
+                                   sizeof(srlim->ioc.overload_tblname)) {
+                                       abort();
+                               }
+                               srlim->ioc.overload_hwm = $2->table.above;
+                               srlim->ioc.overload_lwm = $2->table.below;
+                       }
+
+                       srlim->ioc.inet_prefix = $2->inet_mask;
+                       srlim->ioc.inet6_prefix = $2->inet6_mask;
+
+                       if (pfctl_add_sourcelim(pf, srlim) != 0) {
+                               yyerror("source limiter %s id %u"
+                                   " already exists",
+                                   srlim->ioc.name, srlim->ioc.id);
+                               free(srlim);
+                               YYERROR;
+                       }
+               }
+               ;
+
+sourcelim_nm           : SOURCE LIMITER string {
+                       size_t len = strlen($3);
+                       if (len < 1) {
+                               yyerror("source limiter name is too short");
+                               free($3);
+                               YYERROR;
+                       }
+                       if (len >= PF_SOURCELIM_NAME_LEN) {
+                               yyerror("source limiter name is too long");
+                               free($3);
+                               YYERROR;
+                       }
+                       $$ = $3;
+               }
+               ;
+
+sourcelim_id           : ID NUMBER {
+                       if ($2 < PF_SOURCELIM_ID_MIN ||
+                           $2 > PF_SOURCELIM_ID_MAX) {
+                               yyerror("source limiter id %lld: "
+                                   "invalid identifier", $2);
+                               YYERROR;
+                       }
+
+                       $$ = $2;
+               }
+               ;
+
+sourcelim_opts         : /* empty */ {
+                       yyerror("source limiter missing options");
+                       YYERROR;
+               }
+               | {
+                       memset(&sourcelim_opts, 0, sizeof(sourcelim_opts));
+                       sourcelim_opts.inet_mask = 32;
+                       sourcelim_opts.inet6_mask = 128;
+               } sourcelim_opts_l {
+                       $$ = &sourcelim_opts;
+               }
+               ;
+
+sourcelim_opts_l               : sourcelim_opts_l sourcelim_opt
+               | sourcelim_opt
+               ;
+
+sourcelim_opt          : sourcelim_id {
+                       if (ISSET(sourcelim_opts.marker, SOURCELIM_M_ID)) {
+                               yyerror("entries cannot be respecified");
+                               YYERROR;
+                       }
+
+                       sourcelim_opts.id = $1;
+
+                       sourcelim_opts.marker |= SOURCELIM_M_ID;
+               }
+               | ENTRIES NUMBER {
+                       if (ISSET(sourcelim_opts.marker, SOURCELIM_M_ENTRIES)) {
+                               yyerror("entries cannot be respecified");
+                               YYERROR;
+                       }
+
+                       sourcelim_opts.entries = $2;
+
+                       sourcelim_opts.marker |= SOURCELIM_M_ENTRIES;
+               }
+               | LIMIT NUMBER {
+                       if (ISSET(sourcelim_opts.marker, SOURCELIM_M_LIMIT)) {
+                               yyerror("state limit cannot be respecified");
+                               YYERROR;
+                       }
+
+                       sourcelim_opts.limit = $2;
+
+                       sourcelim_opts.marker |= SOURCELIM_M_LIMIT;
+               }
+               | RATE NUMBER '/' NUMBER {
+                       if (ISSET(sourcelim_opts.marker, SOURCELIM_M_RATE)) {
+                               yyerror("rate cannot be respecified");
+                               YYERROR;
+                       }
+
+                       sourcelim_opts.rate.limit = $2;
+                       sourcelim_opts.rate.seconds = $4;
+
+                       sourcelim_opts.marker |= SOURCELIM_M_RATE;
+               }
+               | TABLE '<' STRING '>' ABOVE NUMBER sourcelim_opt_below {
+                       size_t stringlen;
+
+                       if (ISSET(sourcelim_opts.marker, SOURCELIM_M_TABLE)) {
+                               free($3);
+                               yyerror("rate cannot be respecified");
+                               YYERROR;
+                       }
+
+                       stringlen = strlcpy(sourcelim_opts.table.name,
+                           $3, sizeof(sourcelim_opts.table.name));
+                       free($3);
+                       if (stringlen == 0 ||
+                           stringlen >= PF_TABLE_NAME_SIZE) {
+                               yyerror("invalid table name");
+                               YYERROR;
+                       }
+
+                       if ($6 < 0) {
+                               yyerror("above limit is invalid");
+                               YYERROR;
+                       }
+                       if ($7 > $6) {
+                               yyerror("below limit higher than above limit");
+                               YYERROR;
+                       }
+
+                       sourcelim_opts.table.above = $6;
+                       sourcelim_opts.table.below = $7;
+
+                       sourcelim_opts.marker |= SOURCELIM_M_TABLE;
+               }
+               | INET MASK NUMBER {
+                       if (ISSET(sourcelim_opts.marker,
+                           SOURCELIM_M_INET_MASK)) {
+                               yyerror("inet mask cannot be respecified");
+                               YYERROR;
+                       }
+
+                       if ($3 < 1 || $3 > 32) {
+                               yyerror("inet mask length out of range");
+                               YYERROR;
+                       }
+
+                       sourcelim_opts.inet_mask = $3;
+
+                       sourcelim_opts.marker |= SOURCELIM_M_INET_MASK;
+               }
+               | INET6 MASK NUMBER {
+                       if (ISSET(sourcelim_opts.marker,
+                           SOURCELIM_M_INET6_MASK)) {
+                               yyerror("inet6 mask cannot be respecified");
+                               YYERROR;
+                       }
+
+                       if ($3 < 1 || $3 > 128) {
+                               yyerror("inet6 mask length out of range");
+                               YYERROR;
+                       }
+
+                       sourcelim_opts.inet6_mask = $3;
+
+                       sourcelim_opts.marker |= SOURCELIM_M_INET6_MASK;
+               }
+               ;
+
+sourcelim_opt_below
+               : /* empty */ {
+                       $$ = 0;
+               }
+               | BELOW NUMBER {
+                       if ($2 < 1) {
+                               yyerror("below limit is invalid");
+                               YYERROR;
+                       }
+                       $$ = $2;
+               }
+               ;
+
+sourcelim_filter_opt
+               : sourcelim_nm {
+                       struct pfctl_sourcelim *srlim;
+
+                       srlim = pfctl_get_sourcelim_nm(pf, $1);
+                       free($1);
+                       if (srlim == NULL) {
+                               yyerror("source limiter not found");
+                               YYERROR;
+                       }
+
+                       $$ = srlim->ioc.id;
+               }
+               | SOURCE LIMITER sourcelim_id {
+                       $$ = $3;
+               }
+               ;
+
 pfrule         : action dir logquick interface route af proto fromto
                    filter_opts
                {
@@ -2562,6 +3017,7 @@ pfrule            : action dir logquick interface route 
af proto fromto
                                        }
                                        r.timeout[o->data.timeout.number] =
                                            o->data.timeout.seconds;
+                                       break;
                                }
                                o = o->next;
                                if (!defaults)
@@ -2713,12 +3169,16 @@ pfrule          : action dir logquick interface route 
af proto fromto
 
 filter_opts    :       {
                                bzero(&filter_opts, sizeof filter_opts);
+                               filter_opts.statelim = PF_STATELIM_ID_NONE;
+                               filter_opts.sourcelim = PF_SOURCELIM_ID_NONE;
                                filter_opts.rtableid = -1;
                        }
                    filter_opts_l
                        { $$ = filter_opts; }
                | /* empty */   {
                        bzero(&filter_opts, sizeof filter_opts);
+                       filter_opts.statelim = PF_STATELIM_ID_NONE;
+                       filter_opts.sourcelim = PF_SOURCELIM_ID_NONE;
                        filter_opts.rtableid = -1;
                        $$ = filter_opts;
                }
@@ -2862,6 +3322,20 @@ filter_opt       : USER uids {
                        if (filter_opts.prob == 0)
                                filter_opts.prob = 1;
                }
+               | statelim_filter_opt {
+                       if (filter_opts.statelim != PF_STATELIM_ID_NONE) {
+                               yyerror("state limiter already specified");
+                               YYERROR;
+                       }
+                       filter_opts.statelim = $1;
+               }
+               | sourcelim_filter_opt {
+                       if (filter_opts.sourcelim != PF_SOURCELIM_ID_NONE) {
+                               yyerror("source limiter already specified");
+                               YYERROR;
+                       }
+                       filter_opts.sourcelim = $1;
+               }
                | RTABLE NUMBER                         {
                        if ($2 < 0 || $2 > rt_tableid_max()) {
                                yyerror("invalid rtable id");
@@ -6615,6 +7089,7 @@ lookup(char *s)
 {
        /* this has to be sorted always */
        static const struct keywords keywords[] = {
+               { "above",              ABOVE},
                { "af-to",              AFTO},
                { "all",                ALL},
                { "allow-opts",         ALLOWOPTS},
@@ -6624,6 +7099,7 @@ lookup(char *s)
                { "antispoof",          ANTISPOOF},
                { "any",                ANY},
                { "bandwidth",          BANDWIDTH},
+               { "below",              BELOW},
                { "binat",              BINAT},
                { "binat-anchor",       BINATANCHOR},
                { "binat-to",           BINATTO},
@@ -6643,6 +7119,7 @@ lookup(char *s)
                { "drop",               DROP},
                { "dup-to",             DUPTO},
                { "endpoint-independent", ENDPI},
+               { "entries",    ENTRIES},
                { "ether",              ETHER},
                { "fail-policy",        FAILPOLICY},
                { "fairq",              FAIRQ},
@@ -6662,6 +7139,7 @@ lookup(char *s)
                { "hostid",             HOSTID},
                { "icmp-type",          ICMPTYPE},
                { "icmp6-type",         ICMP6TYPE},
+               { "id",                 ID},
                { "if-bound",           IFBOUND},
                { "in",                 IN},
                { "include",            INCLUDE},
@@ -6673,11 +7151,13 @@ lookup(char *s)
                { "l3",                 L3},
                { "label",              LABEL},
                { "limit",              LIMIT},
+               { "limiter",    LIMITER},
                { "linkshare",          LINKSHARE},
                { "load",               LOAD},
                { "log",                LOG},
                { "loginterface",       LOGINTERFACE},
                { "map-e-portset",      MAPEPORTSET},
+               { "mask",               MASK},
                { "match",              MATCH},
                { "matches",    MATCHES},
                { "max",                MAXIMUM},
@@ -6717,6 +7197,7 @@ lookup(char *s)
                { "quick",              QUICK},
                { "random",             RANDOM},
                { "random-id",          RANDOMID},
+               { "rate",               RATE},
                { "rdr",                RDR},
                { "rdr-anchor",         RDRANCHOR},
                { "rdr-to",             RDRTO},
@@ -6741,6 +7222,7 @@ lookup(char *s)
                { "set-tos",            SETTOS},
                { "skip",               SKIP},
                { "sloppy",             SLOPPY},
+               { "source",             SOURCE},
                { "source-hash",        SOURCEHASH},
                { "source-track",       SOURCETRACK},
                { "state",              STATE},
@@ -7720,10 +8202,21 @@ filteropts_to_rule(struct pfctl_rule *r, struct 
filter_opts *opts)
                r->rule_flag |= PFRULE_ONCE;
        }
 
+       if (opts->statelim != PF_STATELIM_ID_NONE && r->action != PF_PASS) {
+               yyerror("state limiter only applies to pass rules");
+               return (1);
+       }
+       if (opts->sourcelim != PF_SOURCELIM_ID_NONE && r->action != PF_PASS) {
+               yyerror("source limiter only applies to pass rules");
+               return (1);
+       }
+
        r->keep_state = opts->keep.action;
        r->pktrate.limit = opts->pktrate.limit;
        r->pktrate.seconds = opts->pktrate.seconds;
        r->prob = opts->prob;
+       r->statelim = opts->statelim;
+       r->sourcelim = opts->sourcelim;
        r->rtableid = opts->rtableid;
        r->ridentifier = opts->ridentifier;
        r->max_pkt_size = opts->max_pkt_size;
diff --git a/sbin/pfctl/pfctl.8 b/sbin/pfctl/pfctl.8
index 58de54cdf923..d3c8b1273b79 100644
--- a/sbin/pfctl/pfctl.8
+++ b/sbin/pfctl/pfctl.8
@@ -24,7 +24,7 @@
 .\" (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 .\" THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 .\"
-.Dd August 28, 2025
+.Dd December 30, 2025
 .Dt PFCTL 8
 .Os
 .Sh NAME
@@ -524,6 +524,26 @@ When used together with
 interface statistics are also shown.
 .Fl i
 can be used to select an interface or a group of interfaces.
+.It Cm Stlimiter
+Show information about state limiters.
+If
+.Fl R Ar id
+is specified as well,
+only the state limiter identified by
+.Ar id
+is shown.
+.It Cm Srclimiter
+Show information about source limiters.
+If
+.Fl R Ar id
+is specified as well,
+only the state limiter identified by
+.Ar id
+is shown.
+If
+.Fl v
+is specified,
+the address entries for the source pools are shown too.
 .It Cm all
 Show all of the above, except for the lists of interfaces and operating
 system fingerprints.
diff --git a/sbin/pfctl/pfctl.c b/sbin/pfctl/pfctl.c
index da27afb0a179..04deccf7e890 100644
--- a/sbin/pfctl/pfctl.c
+++ b/sbin/pfctl/pfctl.c
@@ -60,11 +60,14 @@
 #include <string.h>
 #include <unistd.h>
 #include <stdarg.h>
+#include <stddef.h>
 #include <libgen.h>
 
 #include "pfctl_parser.h"
 #include "pfctl.h"
 
+struct pfctl_opt_id;
+
 void    usage(void);
 int     pfctl_enable(int, int);
 int     pfctl_disable(int, int);
@@ -87,6 +90,7 @@ void   pfctl_gateway_kill_states(int, const char *, int);
 void    pfctl_label_kill_states(int, const char *, int);
 void    pfctl_id_kill_states(int, const char *, int);
 void    pfctl_key_kill_states(int, const char *, int);
+void    pfctl_kill_source(int, const char *, const char *, int);
 int     pfctl_parse_host(char *, struct pf_rule_addr *);
 void    pfctl_init_options(struct pfctl *);
 int     pfctl_load_options(struct pfctl *);
@@ -101,6 +105,8 @@ int  pfctl_get_pool(int, struct pfctl_pool *, u_int32_t, 
u_int32_t, int,
            const char *, int);
 void    pfctl_print_eth_rule_counters(struct pfctl_eth_rule *, int);
 void    pfctl_print_rule_counters(struct pfctl_rule *, int);
+int     pfctl_show_statelims(int, enum pfctl_show);
+int     pfctl_show_sourcelims(int, enum pfctl_show, int, const char *);
 int     pfctl_show_eth_rules(int, char *, int, enum pfctl_show, char *, int, 
int);
 int     pfctl_show_rules(int, char *, int, enum pfctl_show, char *, int, int);
 int     pfctl_show_nat(int, const char *, int, char *, int, int);
@@ -117,6 +123,10 @@ int         pfctl_test_altqsupport(int, int);
 int     pfctl_show_anchors(int, int, char *);
 int     pfctl_show_eth_anchors(int, int, char *);
 int     pfctl_ruleset_trans(struct pfctl *, char *, struct pfctl_anchor *, 
bool);
+void    pfctl_load_statelims(struct pfctl *);
+void    pfctl_load_statelim(struct pfctl *, struct pfctl_statelim *);
+void    pfctl_load_sourcelims(struct pfctl *);
+void    pfctl_load_sourcelim(struct pfctl *, struct pfctl_sourcelim *);
 int     pfctl_eth_ruleset_trans(struct pfctl *, char *,
            struct pfctl_eth_anchor *);
 int     pfctl_load_eth_ruleset(struct pfctl *, char *,
@@ -127,6 +137,7 @@ int  pfctl_load_ruleset(struct pfctl *, char *,
                struct pfctl_ruleset *, int, int);
 int     pfctl_load_rule(struct pfctl *, char *, struct pfctl_rule *, int);
 const char     *pfctl_lookup_option(char *, const char * const *);
+int     pfctl_lookup_id(const char *, const struct pfctl_opt_id *);
 void    pfctl_reset(int, int);
 int     pfctl_walk_show(int, struct pfioc_ruleset *, void *);
 int     pfctl_walk_get(int, struct pfioc_ruleset *, void *);
@@ -141,6 +152,38 @@ int         pfctl_call_cleartables(int, int, struct 
pfr_anchoritem *);
 int     pfctl_call_clearanchors(int, int, struct pfr_anchoritem *);
 int     pfctl_call_showtables(int, int, struct pfr_anchoritem *);
 
+RB_PROTOTYPE(pfctl_statelim_ids, pfctl_statelim, entry,
+    pfctl_statelim_id_cmp);
+RB_PROTOTYPE(pfctl_statelim_nms, pfctl_statelim, entry,
+    pfctl_statelim_nm_cmp);
+RB_PROTOTYPE(pfctl_sourcelim_ids, pfctl_sourcelim, entry,
+    pfctl_sourcelim_id_cmp);
+RB_PROTOTYPE(pfctl_sourcelim_nms, pfctl_sourcelim, entry,
+    pfctl_sourcelim_nm_cmp);
+
+enum showopt_id {
+       SHOWOPT_NONE = 0,
+       SHOWOPT_ETHER,
+       SHOWOPT_NAT,
+       SHOWOPT_QUEUE,
+       SHOWOPT_RULES,
+       SHOWOPT_ANCHORS,
+       SHOWOPT_SOURCES,
+       SHOWOPT_STATES,
+       SHOWOPT_INFO,
+       SHOWOPT_IFACES,
+       SHOWOPT_LABELS,
+       SHOWOPT_TIMEOUTS,
+       SHOWOPT_MEMORY,
+       SHOWOPT_TABLES,
+       SHOWOPT_OSFP,
+       SHOWOPT_RUNNING,
+       SHOWOPT_STATELIMS,
+       SHOWOPT_SOURCELIMS,
+       SHOWOPT_CREATORIDS,
+       SHOWOPT_ALL,
+};
+
 static struct pfctl_anchor_global       pf_anchors;
 struct pfctl_anchor     pf_main_anchor;
 struct pfctl_eth_anchor         pf_eth_main_anchor;
@@ -148,7 +191,7 @@ static struct pfr_buffer skip_b;
 
 static const char      *clearopt;
 static char            *rulesopt;
-static const char      *showopt;
+static int              showopt;
 static const char      *debugopt;
 static char            *anchoropt;
 static const char      *optiopt = NULL;
@@ -256,10 +299,33 @@ static const char * const clearopt_list[] = {
        "ethernet", "Reset", NULL
 };
 
-static const char * const showopt_list[] = {
-       "ether", "nat", "queue", "rules", "Anchors", "Sources", "states",
-       "info", "Interfaces", "labels", "timeouts", "memory", "Tables",
-       "osfp", "Running", "all", "creatorids", NULL
+struct pfctl_opt_id {
+       const char      *name;
+       int              id;
+};
+
+static const struct pfctl_opt_id showopt_list[] = {
+       { "ether",              SHOWOPT_ETHER },
+       { "nat",                SHOWOPT_NAT },
+       { "queue",              SHOWOPT_QUEUE },
+       { "rules",              SHOWOPT_RULES },
+       { "Anchors",            SHOWOPT_ANCHORS },
+       { "Sources",            SHOWOPT_SOURCES },
+       { "states",             SHOWOPT_STATES },
+       { "info",               SHOWOPT_INFO },
+       { "Interfaces",         SHOWOPT_IFACES },
+       { "labels",             SHOWOPT_LABELS },
+       { "timeouts",           SHOWOPT_TIMEOUTS },
+       { "memory",             SHOWOPT_MEMORY },
+       { "Tables",             SHOWOPT_TABLES },
+       { "osfp",               SHOWOPT_OSFP },
+       { "Running",            SHOWOPT_RUNNING },
+       { "Stlimiters",         SHOWOPT_STATELIMS },
*** 3470 LINES SKIPPED ***

Reply via email to