Skip site navigation (1)Skip section navigation (2)
Date:      Wed, 25 Jun 2025 17:56:40 GMT
From:      Kristof Provost <kp@FreeBSD.org>
To:        src-committers@FreeBSD.org, dev-commits-src-all@FreeBSD.org, dev-commits-src-main@FreeBSD.org
Subject:   git: ff11f1c8c76c - main - pf: add a generic packet rate matching filter
Message-ID:  <202506251756.55PHueAK035359@gitrepo.freebsd.org>

next in thread | raw e-mail | index | archive | help
The branch main has been updated by kp:

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

commit ff11f1c8c76c053b442f1f1df97272939fbf5afc
Author:     Kristof Provost <kp@FreeBSD.org>
AuthorDate: 2025-06-03 07:15:21 +0000
Commit:     Kristof Provost <kp@FreeBSD.org>
CommitDate: 2025-06-25 17:56:23 +0000

    pf: add a generic packet rate matching filter
    
    allows things like
    pass in proto icmp max-pkt-rate 100/10
    all packets matching the rule in the direction the state was created are
    taken into consideration (typically: requests, but not replies).
    Just like with the other max-*, the rule stops matching if the maximum is
    reached, so in typical scenarios the default block rule would kick in then.
    with input from Holger Mikolon
    ok mikeb
    
    Obtained from:  OpenBSD, henning <henning@openbsd.org>, 5a4ae9a9cb
    Sponsored by:   Rubicon Communications, LLC ("Netgate")
    Differential Revision:  https://reviews.freebsd.org/D50798
---
 lib/libpfctl/libpfctl.c   | 34 ++++++++++++++++++++++++----------
 lib/libpfctl/libpfctl.h   | 15 ++++++++-------
 sbin/pfctl/parse.y        | 24 +++++++++++++++++++++++-
 sbin/pfctl/pfctl_parser.c |  3 +++
 share/man/man5/pf.conf.5  | 19 ++++++++++++++++++-
 sys/net/pfvar.h           |  1 +
 sys/netpfil/pf/pf.c       | 11 +++++++++++
 sys/netpfil/pf/pf_ioctl.c |  4 +++-
 sys/netpfil/pf/pf_nl.c    | 13 +++++++++++++
 sys/netpfil/pf/pf_nl.h    |  1 +
 10 files changed, 105 insertions(+), 20 deletions(-)

diff --git a/lib/libpfctl/libpfctl.c b/lib/libpfctl/libpfctl.c
index cbc193268505..4789448d2a37 100644
--- a/lib/libpfctl/libpfctl.c
+++ b/lib/libpfctl/libpfctl.c
@@ -1208,6 +1208,19 @@ snl_add_msg_attr_uid(struct snl_writer *nw, uint32_t type, const struct pf_rule_
 	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_pf_rule(struct snl_writer *nw, uint32_t type, const struct pfct
 	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 @@ static const struct snl_attr_parser ap_rule_uid[] = {
 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 @@ static struct snl_attr_parser ap_getrule[] = {
 	{ .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 @@ pfctl_get_ruleset(struct pfctl_handle *h, const char *path, uint32_t nr, struct
 	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/lib/libpfctl/libpfctl.h b/lib/libpfctl/libpfctl.h
index 4d481f436674..7de7a08e90bf 100644
--- a/lib/libpfctl/libpfctl.h
+++ b/lib/libpfctl/libpfctl.h
@@ -159,6 +159,13 @@ struct pfctl_rules_info {
 	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_rule {
 		struct pfctl_pool	 rdr;
 	};
 	struct pfctl_pool	 route;
+	struct pfctl_threshold	 pktrate;
 
 	uint64_t		 evaluations;
 	uint64_t		 packets[2];
@@ -396,13 +404,6 @@ struct pfctl_syncookies {
 	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/sbin/pfctl/parse.y b/sbin/pfctl/parse.y
index 1b137eecfa47..5d4d2b86fd58 100644
--- a/sbin/pfctl/parse.y
+++ b/sbin/pfctl/parse.y
@@ -308,6 +308,10 @@ static struct filter_opts {
 	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 @@ int	parseport(char *, struct range *r, int);
 %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	<v.string>		STRING
 %token	<v.number>		NUMBER
 %token	<v.i>			PORTBINARY
@@ -1012,6 +1016,8 @@ anchorrule	: ANCHOR anchorname dir quick interface af proto fromto
 			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 @@ pfrule		: action dir logquick interface route af proto fromto
 
 			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_opt	: USER uids {
 			}
 			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 @@ lookup(char *s)
 		{ "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
index 2d88c6d00605..32e98eb20b7c 100644
--- a/sbin/pfctl/pfctl_parser.c
+++ b/sbin/pfctl/pfctl_parser.c
@@ -1007,6 +1007,9 @@ print_rule(struct pfctl_rule *r, const char *anchor_call, int verbose, int numer
 		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
index edcb335619ba..010096369c83 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 June 12, 2025
+.Dd June 17, 2025
 .Dt PF.CONF 5
 .Os
 .Sh NAME
@@ -2216,6 +2216,22 @@ directive occurs only at configuration file parse time, not during runtime.
 .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 )
@@ -3388,6 +3404,7 @@ filteropt      = user | group | flags | icmp-type | icmp6-type | "tos" tos |
                  "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
index 8afba0525351..33574dbd5c2a 100644
--- a/sys/net/pfvar.h
+++ b/sys/net/pfvar.h
@@ -821,6 +821,7 @@ struct pf_krule {
 	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
index 09762abb2a16..908f1b83e542 100644
--- a/sys/netpfil/pf/pf.c
+++ b/sys/netpfil/pf/pf.c
@@ -445,6 +445,12 @@ VNET_DEFINE(struct pf_limit, pf_limits[PF_LIMIT_MAX]);
 		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_match_rule(struct pf_test_ctx *ctx, struct pf_kruleset *ruleset)
 		    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
index c312ad001a60..05a7e1311ad8 100644
--- a/sys/netpfil/pf/pf_ioctl.c
+++ b/sys/netpfil/pf/pf_ioctl.c
@@ -2156,7 +2156,6 @@ pf_ioctl_addrule(struct pf_krule *rule, uint32_t ticket,
 
 	if (rule->rtableid > 0 && rule->rtableid >= rt_numfibs)
 		error = EBUSY;
-
 #ifdef ALTQ
 	/* set queue IDs */
 	if (rule->qname[0] != 0) {
@@ -2181,6 +2180,9 @@ pf_ioctl_addrule(struct pf_krule *rule, uint32_t ticket,
 		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.c b/sys/netpfil/pf/pf_nl.c
index a975501794e6..48cba96b04b0 100644
--- a/sys/netpfil/pf/pf_nl.c
+++ b/sys/netpfil/pf/pf_nl.c
@@ -51,6 +51,9 @@
 #include <netlink/netlink_debug.h>
 _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 @@ nlattr_add_timeout(struct nl_writer *nw, int attrtype, uint32_t *timeout)
 	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 @@ static const struct nlattr_parser nla_p_rule[] = {
 	{ .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 @@ pf_handle_getrule(struct nlmsghdr *hdr, struct nl_pstate *npt)
 	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);
diff --git a/sys/netpfil/pf/pf_nl.h b/sys/netpfil/pf/pf_nl.h
index 0f2f0b01415c..97ef574995f5 100644
--- a/sys/netpfil/pf/pf_nl.h
+++ b/sys/netpfil/pf/pf_nl.h
@@ -278,6 +278,7 @@ enum pf_rule_type_t {
 	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 {



Want to link to this message? Use this URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?202506251756.55PHueAK035359>