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 ***
