diff --git a/lib/libpfctl/libpfctl.h b/lib/libpfctl/libpfctl.h --- a/lib/libpfctl/libpfctl.h +++ b/lib/libpfctl/libpfctl.h @@ -159,6 +159,13 @@ uint32_t ticket; }; +struct pfctl_threshold { + uint32_t limit; + uint32_t seconds; + uint32_t count; + uint32_t last; +}; + struct pfctl_rule { struct pf_rule_addr src; struct pf_rule_addr dst; @@ -181,6 +188,7 @@ struct pfctl_pool rdr; }; struct pfctl_pool route; + struct pfctl_threshold pktrate; uint64_t evaluations; uint64_t packets[2]; @@ -396,13 +404,6 @@ uint32_t halfopen_states; }; -struct pfctl_threshold { - uint32_t limit; - uint32_t seconds; - uint32_t count; - uint32_t last; -}; - struct pfctl_src_node { struct pf_addr addr; struct pf_addr raddr; diff --git a/lib/libpfctl/libpfctl.c b/lib/libpfctl/libpfctl.c --- a/lib/libpfctl/libpfctl.c +++ b/lib/libpfctl/libpfctl.c @@ -1208,6 +1208,19 @@ snl_end_attr_nested(nw, off); } +static void +snl_add_msg_attr_threshold(struct snl_writer *nw, uint32_t type, const struct pfctl_threshold *th) +{ + int off; + + off = snl_add_msg_attr_nested(nw, type); + + snl_add_msg_attr_u32(nw, PF_TH_LIMIT, th->limit); + snl_add_msg_attr_u32(nw, PF_TH_SECONDS, th->seconds); + + snl_end_attr_nested(nw, off); +} + static void snl_add_msg_attr_pf_rule(struct snl_writer *nw, uint32_t type, const struct pfctl_rule *r) { @@ -1228,6 +1241,7 @@ snl_add_msg_attr_rpool(nw, PF_RT_RPOOL_RDR, &r->rdr); snl_add_msg_attr_rpool(nw, PF_RT_RPOOL_NAT, &r->nat); snl_add_msg_attr_rpool(nw, PF_RT_RPOOL_RT, &r->route); + snl_add_msg_attr_threshold(nw, PF_RT_PKTRATE, &r->pktrate); snl_add_msg_attr_u32(nw, PF_RT_OS_FINGERPRINT, r->os_fingerprint); snl_add_msg_attr_u32(nw, PF_RT_RTABLEID, r->rtableid); snl_add_msg_attr_timeouts(nw, PF_RT_TIMEOUT, r->timeout); @@ -1581,6 +1595,15 @@ SNL_DECLARE_ATTR_PARSER(rule_uid_parser, ap_rule_uid); #undef _OUT +#define _OUT(_field) offsetof(struct pfctl_threshold, _field) +static const struct snl_attr_parser ap_pfctl_threshold[] = { + { .type = PF_TH_LIMIT, .off = _OUT(limit), .cb = snl_attr_get_uint32 }, + { .type = PF_TH_SECONDS, .off = _OUT(seconds), .cb = snl_attr_get_uint32 }, + { .type = PF_TH_COUNT, .off = _OUT(count), .cb = snl_attr_get_uint32 }, +}; +SNL_DECLARE_ATTR_PARSER(pfctl_threshold_parser, ap_pfctl_threshold); +#undef _OUT + struct pfctl_nl_get_rule { struct pfctl_rule r; char anchor_call[MAXPATHLEN]; @@ -1668,6 +1691,7 @@ { .type = PF_RT_SRC_NODES_LIMIT, .off = _OUT(r.src_nodes_type[PF_SN_LIMIT]), .cb = snl_attr_get_uint64 }, { .type = PF_RT_SRC_NODES_NAT, .off = _OUT(r.src_nodes_type[PF_SN_NAT]), .cb = snl_attr_get_uint64 }, { .type = PF_RT_SRC_NODES_ROUTE, .off = _OUT(r.src_nodes_type[PF_SN_ROUTE]), .cb = snl_attr_get_uint64 }, + { .type = PF_RT_PKTRATE, .off = _OUT(r.pktrate), .arg = &pfctl_threshold_parser, .cb = snl_attr_get_nested }, }; #undef _OUT SNL_DECLARE_PARSER(getrule_parser, struct genlmsghdr, snl_f_p_empty, ap_getrule); @@ -3001,16 +3025,6 @@ return (e.error); } -#define _OUT(_field) offsetof(struct pfctl_threshold, _field) -static const struct snl_attr_parser ap_pfctl_threshold[] = { - { .type = PF_TH_LIMIT, .off = _OUT(limit), .cb = snl_attr_get_uint32 }, - { .type = PF_TH_SECONDS, .off = _OUT(seconds), .cb = snl_attr_get_uint32 }, - { .type = PF_TH_COUNT, .off = _OUT(count), .cb = snl_attr_get_uint32 }, - { .type = PF_TH_LAST, .off = _OUT(last), .cb = snl_attr_get_uint32 }, -}; -SNL_DECLARE_ATTR_PARSER(pfctl_threshold_parser, ap_pfctl_threshold); -#undef _OUT - #define _OUT(_field) offsetof(struct pfctl_src_node, _field) static struct snl_attr_parser ap_srcnode[] = { { .type = PF_SN_ADDR, .off = _OUT(addr), .cb = snl_attr_get_in6_addr }, diff --git a/sbin/pfctl/parse.y b/sbin/pfctl/parse.y --- a/sbin/pfctl/parse.y +++ b/sbin/pfctl/parse.y @@ -308,6 +308,10 @@ int settos; int randomid; int max_mss; + struct { + uint32_t limit; + uint32_t seconds; + } pktrate; } filter_opts; static struct antispoof_opts { @@ -531,7 +535,7 @@ %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 NATTO RDRTO -%token BINATTO +%token BINATTO MAXPKTRATE %token STRING %token NUMBER %token PORTBINARY @@ -1012,6 +1016,8 @@ r.prob = $9.prob; r.rtableid = $9.rtableid; r.ridentifier = $9.ridentifier; + r.pktrate.limit = $9.pktrate.limit; + r.pktrate.seconds = $9.pktrate.seconds; if ($9.tag) if (strlcpy(r.tagname, $9.tag, @@ -2489,6 +2495,8 @@ r.tos = $9.tos; r.keep_state = $9.keep.action; + r.pktrate.limit = $9.pktrate.limit; + r.pktrate.seconds = $9.pktrate.seconds; o = $9.keep.options; /* 'keep state' by default on pass rules. */ @@ -3112,6 +3120,19 @@ } filter_opts.marker |= FOM_AFTO; } + | MAXPKTRATE NUMBER '/' NUMBER { + if ($2 < 0 || $2 > UINT_MAX || + $4 < 0 || $4 > UINT_MAX) { + yyerror("only positive values permitted"); + YYERROR; + } + if (filter_opts.pktrate.limit) { + yyerror("cannot respecify max-pkt-rate"); + YYERROR; + } + filter_opts.pktrate.limit = $2; + filter_opts.pktrate.seconds = $4; + } | filter_sets ; @@ -6697,6 +6718,7 @@ { "matches", MATCHES}, { "max", MAXIMUM}, { "max-mss", MAXMSS}, + { "max-pkt-rate", MAXPKTRATE}, { "max-src-conn", MAXSRCCONN}, { "max-src-conn-rate", MAXSRCCONNRATE}, { "max-src-nodes", MAXSRCNODES}, diff --git a/sbin/pfctl/pfctl_parser.c b/sbin/pfctl/pfctl_parser.c --- a/sbin/pfctl/pfctl_parser.c +++ b/sbin/pfctl/pfctl_parser.c @@ -1007,6 +1007,9 @@ printf(" tos 0x%2.2x", r->tos); if (r->prio) printf(" prio %u", r->prio == PF_PRIO_ZERO ? 0 : r->prio); + if (r->pktrate.limit) + printf(" max-pkt-rate %u/%u", r->pktrate.limit, + r->pktrate.seconds); if (r->scrub_flags & PFSTATE_SETMASK) { char *comma = ""; printf(" set ("); diff --git a/share/man/man5/pf.conf.5 b/share/man/man5/pf.conf.5 --- 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 May 29, 2025 +.Dd May 30, 2025 .Dt PF.CONF 5 .Os .Sh NAME @@ -2209,6 +2209,22 @@ .It Ar ridentifier Aq Ar number Add an identifier (number) to the rule, which can be used to correlate the rule to pflog entries, even after ruleset updates. +.It Cm max-pkt-rate Ar number Ns / Ns Ar seconds +Measure the rate of packets matching the rule and states created by it. +When the specified rate is exceeded, the rule stops matching. +Only packets in the direction in which the state was created are considered, +so that typically requests are counted and replies are not. +For example: +.Pp +.Bd -literal -offset indent -compact +block in proto icmp +pass in proto icmp max-pkt-rate 100/10 +.Ed +.Pp +passes up to 100 icmp packets per 10 seconds. +When the rate is exceeded, all icmp is blocked until the rate falls below +100 per 10 seconds again. +.Pp .It Xo Ar queue Aq Ar queue .No \*(Ba ( Aq Ar queue , .Aq Ar queue ) @@ -3381,6 +3397,7 @@ "max-mss" number | "random-id" | "reassemble tcp" | fragmentation | "allow-opts" | "label" string | "tag" string | [ "!" ] "tagged" string | + "max-pkt-rate" number "/" seconds | "set prio" ( number | "(" number [ [ "," ] number ] ")" ) | "queue" ( string | "(" string [ [ "," ] string ] ")" ) | "rtable" number | "probability" number"%" | "prio" number | diff --git a/sys/net/pfvar.h b/sys/net/pfvar.h --- a/sys/net/pfvar.h +++ b/sys/net/pfvar.h @@ -821,6 +821,7 @@ struct pf_kpool nat; struct pf_kpool rdr; struct pf_kpool route; + struct pf_kthreshold pktrate; struct pf_counter_u64 evaluations; struct pf_counter_u64 packets[2]; diff --git a/sys/netpfil/pf/pf.c b/sys/netpfil/pf/pf.c --- a/sys/netpfil/pf/pf.c +++ b/sys/netpfil/pf/pf.c @@ -445,6 +445,12 @@ SDT_PROBE5(pf, ip, state, lookup, pd->kif, k, (pd->dir), pd, (s)); \ if ((s) == NULL) \ return (PF_DROP); \ + if ((s)->rule->pktrate.limit && pd->dir == (s)->direction) { \ + if (pf_check_threshold(&(s)->rule->pktrate)) { \ + s = NULL; \ + return (PF_DROP); \ + } \ + } \ if (PACKET_LOOPED(pd)) \ return (PF_PASS); \ } while (0) @@ -5606,6 +5612,11 @@ pf_osfp_fingerprint(pd, ctx->th), r->os_fingerprint)), TAILQ_NEXT(r, entries)); + /* must be last! */ + if (r->pktrate.limit) { + PF_TEST_ATTRIB((pf_check_threshold(&r->pktrate)), + TAILQ_NEXT(r, entries)); + } /* FALLTHROUGH */ if (r->tag) ctx->tag = r->tag; diff --git a/sys/netpfil/pf/pf_ioctl.c b/sys/netpfil/pf/pf_ioctl.c --- a/sys/netpfil/pf/pf_ioctl.c +++ b/sys/netpfil/pf/pf_ioctl.c @@ -2156,7 +2156,6 @@ if (rule->rtableid > 0 && rule->rtableid >= rt_numfibs) error = EBUSY; - #ifdef ALTQ /* set queue IDs */ if (rule->qname[0] != 0) { @@ -2181,6 +2180,9 @@ error = EINVAL; if (!rule->log) rule->logif = 0; + if (! pf_init_threshold(&rule->pktrate, rule->pktrate.limit, + rule->pktrate.seconds)) + error = ENOMEM; if (pf_addr_setup(ruleset, &rule->src.addr, rule->af)) error = ENOMEM; if (pf_addr_setup(ruleset, &rule->dst.addr, rule->af)) diff --git a/sys/netpfil/pf/pf_nl.h b/sys/netpfil/pf/pf_nl.h --- a/sys/netpfil/pf/pf_nl.h +++ b/sys/netpfil/pf/pf_nl.h @@ -278,6 +278,7 @@ PF_RT_SRC_NODES_LIMIT = 79, /* u64 */ PF_RT_SRC_NODES_NAT = 80, /* u64 */ PF_RT_SRC_NODES_ROUTE = 81, /* u64 */ + PF_RT_PKTRATE = 82, /* nested, pf_threshold_type_t */ }; enum pf_addrule_type_t { diff --git a/sys/netpfil/pf/pf_nl.c b/sys/netpfil/pf/pf_nl.c --- a/sys/netpfil/pf/pf_nl.c +++ b/sys/netpfil/pf/pf_nl.c @@ -51,6 +51,9 @@ #include _DECLARE_DEBUG(LOG_DEBUG); +static bool nlattr_add_pf_threshold(struct nl_writer *, int, + struct pf_kthreshold *); + struct nl_parsed_state { uint8_t version; uint32_t id; @@ -679,6 +682,14 @@ return (true); } +#define _OUT(_field) offsetof(struct pf_kthreshold, _field) +static const struct nlattr_parser nla_p_threshold[] = { + { .type = PF_TH_LIMIT, .off = _OUT(limit), .cb = nlattr_get_uint32 }, + { .type = PF_TH_SECONDS, .off = _OUT(seconds), .cb = nlattr_get_uint32 }, +}; +NL_DECLARE_ATTR_PARSER(threshold_parser, nla_p_threshold); +#undef _OUT + #define _OUT(_field) offsetof(struct pf_krule, _field) static const struct nlattr_parser nla_p_rule[] = { { .type = PF_RT_SRC, .off = _OUT(src), .arg = &rule_addr_parser,.cb = nlattr_get_nested }, @@ -749,6 +760,7 @@ { .type = PF_RT_NAF, .off = _OUT(naf), .cb = nlattr_get_uint8 }, { .type = PF_RT_RPOOL_RT, .off = _OUT(route), .arg = &pool_parser, .cb = nlattr_get_nested }, { .type = PF_RT_RCV_IFNOT, .off = _OUT(rcvifnot), .cb = nlattr_get_bool }, + { .type = PF_RT_PKTRATE, .off = _OUT(pktrate), .arg = &threshold_parser, .cb = nlattr_get_nested }, }; NL_DECLARE_ATTR_PARSER(rule_parser, nla_p_rule); #undef _OUT @@ -1003,6 +1015,7 @@ nlattr_add_u64(nw, PF_RT_SRC_NODES_LIMIT, counter_u64_fetch(rule->src_nodes[PF_SN_LIMIT])); nlattr_add_u64(nw, PF_RT_SRC_NODES_NAT, counter_u64_fetch(rule->src_nodes[PF_SN_NAT])); nlattr_add_u64(nw, PF_RT_SRC_NODES_ROUTE, counter_u64_fetch(rule->src_nodes[PF_SN_ROUTE])); + nlattr_add_pf_threshold(nw, PF_RT_PKTRATE, &rule->pktrate); error = pf_kanchor_copyout(ruleset, rule, anchor_call, sizeof(anchor_call)); MPASS(error == 0);