git: 8716d8c7d97e - main - pf: configurable action on limiter exceeded

From: Kristof Provost <kp_at_FreeBSD.org>
Date: Wed, 14 Jan 2026 08:06:17 UTC
The branch main has been updated by kp:

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

commit 8716d8c7d97eec231820ecd1dc50c67beb95d58c
Author:     Kristof Provost <kp@FreeBSD.org>
AuthorDate: 2026-01-12 19:37:08 +0000
Commit:     Kristof Provost <kp@FreeBSD.org>
CommitDate: 2026-01-14 06:44:43 +0000

    pf: configurable action on limiter exceeded
    
    This change extends pf(4) limiters so administrator
    can specify action the rule executes when limit is
    reached. By default when limit is reached the limiter
    overrides action specified by rule to no-match.
    If administrator wants to block packet instead then
    rule with limiter should be changed to:
    
       pass in from any to any state limiter test (block)
    
    OK dlg@
    
    Obtained from:  OpenBSD, sashan <sashan@openbsd.org>, 04394254d9
    Sponsored by:   Rubicon Communications, LLC ("Netgate")
---
 lib/libpfctl/libpfctl.c          | 12 ++++--
 lib/libpfctl/libpfctl.h          | 10 ++++-
 sbin/pfctl/parse.y               | 83 +++++++++++++++++++++++++++-------------
 sbin/pfctl/pfctl_parser.c        | 12 ++++--
 sbin/pfctl/tests/files/pf1076.ok |  2 +-
 sbin/pfctl/tests/files/pf1077.ok |  2 +-
 share/man/man5/pf.conf.5         | 28 ++++++++++----
 sys/net/pfvar.h                  | 11 +++++-
 sys/netpfil/pf/pf.c              | 43 +++++++++++++++++----
 sys/netpfil/pf/pf.h              |  5 +++
 sys/netpfil/pf/pf_ioctl.c        | 20 ++++++++++
 sys/netpfil/pf/pf_nl.c           | 12 ++++--
 sys/netpfil/pf/pf_nl.h           |  6 ++-
 13 files changed, 183 insertions(+), 63 deletions(-)

diff --git a/lib/libpfctl/libpfctl.c b/lib/libpfctl/libpfctl.c
index a5abe1cadd64..63f61932519c 100644
--- a/lib/libpfctl/libpfctl.c
+++ b/lib/libpfctl/libpfctl.c
@@ -1313,8 +1313,10 @@ 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_add_msg_attr_u8(nw, PF_RT_STATE_LIMIT, r->statelim.id);
+	snl_add_msg_attr_u32(nw, PF_RT_STATE_LIMIT_ACTION, r->statelim.limiter_action);
+	snl_add_msg_attr_u8(nw, PF_RT_SOURCE_LIMIT, r->sourcelim.id);
+	snl_add_msg_attr_u32(nw, PF_RT_SOURCE_LIMIT_ACTION, r->sourcelim.limiter_action);
 
 	snl_end_attr_nested(nw, off);
 }
@@ -1707,8 +1709,10 @@ 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 },
+	{ .type = PF_RT_STATE_LIMIT, .off = _OUT(r.statelim.id), .cb = snl_attr_get_uint8 },
+	{ .type = PF_RT_SOURCE_LIMIT, .off = _OUT(r.sourcelim.id), .cb = snl_attr_get_uint8 },
+	{ .type = PF_RT_STATE_LIMIT_ACTION, .off = _OUT(r.statelim.limiter_action), .cb = snl_attr_get_uint32 },
+	{ .type = PF_RT_SOURCE_LIMIT_ACTION, .off = _OUT(r.sourcelim.limiter_action), .cb = snl_attr_get_uint32 },
 };
 #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 670688893a6a..d55267e56b4c 100644
--- a/lib/libpfctl/libpfctl.h
+++ b/lib/libpfctl/libpfctl.h
@@ -249,8 +249,14 @@ struct pfctl_rule {
 	struct pf_rule_gid	 gid;
 	char			 rcv_ifname[IFNAMSIZ];
 	bool			 rcvifnot;
-	uint8_t			 statelim;
-	uint8_t			 sourcelim;
+	struct {
+		uint8_t		 id;
+		int		 limiter_action;
+	}			 statelim;
+	struct {
+		uint8_t		 id;
+		int		 limiter_action;
+	}			 sourcelim;
 
 	uint32_t		 rule_flag;
 	uint8_t			 action;
diff --git a/sbin/pfctl/parse.y b/sbin/pfctl/parse.y
index ded74a6391f1..67e0d30890a8 100644
--- a/sbin/pfctl/parse.y
+++ b/sbin/pfctl/parse.y
@@ -257,6 +257,11 @@ struct redirspec {
 	bool			 binat;
 };
 
+struct limiterspec {
+	uint32_t	id;
+	int			limiter_action;
+};
+
 static struct filter_opts {
 	int			 marker;
 #define FOM_FLAGS	0x0001
@@ -287,8 +292,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 limiterspec		 statelim;
+	struct limiterspec		 sourcelim;
 	struct {
 		int			 action;
 		struct node_state_opt	*options;
@@ -566,6 +571,7 @@ typedef struct {
 		struct statelim_opts	*statelim_opts;
 		struct sourcelim_opts	*sourcelim_opts;
 		struct pfctl_watermarks	*watermarks;
+		struct limiterspec		 limiterspec;
 	} v;
 	int lineno;
 } YYSTYPE;
@@ -600,7 +606,7 @@ 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	LIMITER ID RATE SOURCE ENTRIES ABOVE BELOW MASK NOMATCH
 %token	<v.string>		STRING
 %token	<v.number>		NUMBER
 %token	<v.i>			PORTBINARY
@@ -664,8 +670,8 @@ int	parseport(char *, struct range *r, int);
 %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.number>			statelim_id sourcelim_id limiter_opt limiter_opt_spec
+%type	<v.limiterspec>		statelim_filter_opt sourcelim_filter_opt
 %type	<v.statelim_opts>	statelim_opts
 %type	<v.sourcelim_opts>	sourcelim_opts
 %%
@@ -2515,20 +2521,22 @@ statelim_opt		: statelim_id {
 		;
 
 statelim_filter_opt
-		: statelim_nm {
+		: STATE LIMITER STRING limiter_opt_spec {
 			struct pfctl_statelim *stlim;
 
-			stlim = pfctl_get_statelim_nm(pf, $1);
-			free($1);
+			stlim = pfctl_get_statelim_nm(pf, $3);
+			free($3);
 			if (stlim == NULL) {
 				yyerror("state limiter not found");
 				YYERROR;
 			}
 
-			$$ = stlim->ioc.id;
+			$$.id = stlim->ioc.id;
+			$$.limiter_action = $4;
 		}
-		| STATE LIMITER statelim_id {
-			$$ = $3;
+		| STATE LIMITER statelim_id limiter_opt_spec {
+			$$.id = $3;
+			$$.limiter_action = $4;
 		}
 		;
 
@@ -2760,20 +2768,34 @@ sourcelim_opt_below
 		;
 
 sourcelim_filter_opt
-		: sourcelim_nm {
+		: SOURCE LIMITER STRING limiter_opt_spec {
 			struct pfctl_sourcelim *srlim;
 
-			srlim = pfctl_get_sourcelim_nm(pf, $1);
-			free($1);
+			srlim = pfctl_get_sourcelim_nm(pf, $3);
+			free($3);
 			if (srlim == NULL) {
 				yyerror("source limiter not found");
 				YYERROR;
 			}
 
-			$$ = srlim->ioc.id;
+			$$.id = srlim->ioc.id;
+			$$.limiter_action = $4;
 		}
-		| SOURCE LIMITER sourcelim_id {
-			$$ = $3;
+		| SOURCE LIMITER sourcelim_id limiter_opt_spec {
+			$$.id = $3;
+			$$.limiter_action = $4;
+		}
+		;
+
+limiter_opt_spec: /* empty */ { $$ = PF_LIMITER_NOMATCH; }
+		| '(' limiter_opt ')' { $$ = $2; }
+		;
+
+limiter_opt:   BLOCK {
+			$$ = PF_LIMITER_BLOCK;
+		}
+		| NOMATCH {
+			$$ = PF_LIMITER_NOMATCH;
 		}
 		;
 
@@ -3169,16 +3191,20 @@ 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.statelim.id = PF_STATELIM_ID_NONE;
+				filter_opts.statelim.limiter_action = PF_LIMITER_NOMATCH;
+				filter_opts.sourcelim.id = PF_SOURCELIM_ID_NONE;
+				filter_opts.sourcelim.limiter_action = PF_LIMITER_NOMATCH;
 				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.statelim.id = PF_STATELIM_ID_NONE;
+			filter_opts.statelim.limiter_action = PF_LIMITER_NOMATCH;
+			filter_opts.sourcelim.id = PF_SOURCELIM_ID_NONE;
+			filter_opts.sourcelim.limiter_action = PF_LIMITER_NOMATCH;
 			filter_opts.rtableid = -1;
 			$$ = filter_opts;
 		}
@@ -3323,14 +3349,14 @@ filter_opt	: USER uids {
 				filter_opts.prob = 1;
 		}
 		| statelim_filter_opt {
-			if (filter_opts.statelim != PF_STATELIM_ID_NONE) {
+			if (filter_opts.statelim.id != 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) {
+			if (filter_opts.sourcelim.id != PF_SOURCELIM_ID_NONE) {
 				yyerror("source limiter already specified");
 				YYERROR;
 			}
@@ -7175,6 +7201,7 @@ lookup(char *s)
 		{ "nat-to",		NATTO},
 		{ "no",			NO},
 		{ "no-df",		NODF},
+		{ "no-match",	NOMATCH},
 		{ "no-route",		NOROUTE},
 		{ "no-sync",		NOSYNC},
 		{ "on",			ON},
@@ -8202,11 +8229,11 @@ 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) {
+	if (opts->statelim.id != 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) {
+	if (opts->sourcelim.id != PF_SOURCELIM_ID_NONE && r->action != PF_PASS) {
 		yyerror("source limiter only applies to pass rules");
 		return (1);
 	}
@@ -8215,8 +8242,10 @@ filteropts_to_rule(struct pfctl_rule *r, struct filter_opts *opts)
 	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->statelim.id = opts->statelim.id;
+	r->statelim.limiter_action = opts->statelim.limiter_action;
+	r->sourcelim.id = opts->sourcelim.id;
+	r->sourcelim.limiter_action = opts->sourcelim.limiter_action;
 	r->rtableid = opts->rtableid;
 	r->ridentifier = opts->ridentifier;
 	r->max_pkt_size = opts->max_pkt_size;
diff --git a/sbin/pfctl/pfctl_parser.c b/sbin/pfctl/pfctl_parser.c
index f85c50652944..78a1034a3b43 100644
--- a/sbin/pfctl/pfctl_parser.c
+++ b/sbin/pfctl/pfctl_parser.c
@@ -1112,7 +1112,7 @@ print_rule(struct pfctl_rule *r, const char *anchor_call, int opts, int numeric)
 		}
 		printf(" probability %s%%", buf);
 	}
-	if (r->statelim != PF_STATELIM_ID_NONE) {
+	if (r->statelim.id != PF_STATELIM_ID_NONE) {
 #if 0 /* XXX need pf to find statelims */
 		struct pfctl_statelim *stlim =
 		    pfctl_get_statelim_id(pf, r->statelim);
@@ -1121,9 +1121,11 @@ print_rule(struct pfctl_rule *r, const char *anchor_call, int opts, int numeric)
 			printf(" state limiter %s", stlim->ioc.name);
 		else
 #endif
-		printf(" state limiter id %u", r->statelim);
+		printf(" state limiter id %u (%s)", r->statelim.id,
+		    (r->statelim.limiter_action == PF_LIMITER_BLOCK) ?
+		    "block" : "no-match");
 	}
-	if (r->sourcelim != PF_SOURCELIM_ID_NONE) {
+	if (r->sourcelim.id != PF_SOURCELIM_ID_NONE) {
 #if 0 /* XXX need pf to find sourcelims */
 		struct pfctl_sourcelim *srlim =
 		    pfctl_get_sourcelim_id(pf, r->sourcelim);
@@ -1132,7 +1134,9 @@ print_rule(struct pfctl_rule *r, const char *anchor_call, int opts, int numeric)
 			printf(" source limiter %s", srlim->ioc.name);
 		else
 #endif
-		printf(" source limiter id %u", r->sourcelim);
+		printf(" source limiter id %u (%s)", r->sourcelim.id,
+		    (r->sourcelim.limiter_action == PF_LIMITER_BLOCK) ?
+		    "block" : "no-match");
 	}
 
 	ropts = 0;
diff --git a/sbin/pfctl/tests/files/pf1076.ok b/sbin/pfctl/tests/files/pf1076.ok
index def9533b1e60..9f1a8c8fb5cf 100644
--- a/sbin/pfctl/tests/files/pf1076.ok
+++ b/sbin/pfctl/tests/files/pf1076.ok
@@ -1,2 +1,2 @@
 state limiter dns-server id 1 limit 1000 rate 1/10
-pass in proto tcp from any to any port = domain flags S/SA keep state state limiter id 1
+pass in proto tcp from any to any port = domain flags S/SA keep state state limiter id 1 (no-match)
diff --git a/sbin/pfctl/tests/files/pf1077.ok b/sbin/pfctl/tests/files/pf1077.ok
index e52afb6bff9c..dc8882e1b87b 100644
--- a/sbin/pfctl/tests/files/pf1077.ok
+++ b/sbin/pfctl/tests/files/pf1077.ok
@@ -1,2 +1,2 @@
 source limiter dns-server id 1 limit 2 states 3 rate 4/5 inet mask 16
-pass in proto tcp from any to any port = domain flags S/SA keep state source limiter id 1
+pass in proto tcp from any to any port = domain flags S/SA keep state source limiter id 1 (no-match)
diff --git a/share/man/man5/pf.conf.5 b/share/man/man5/pf.conf.5
index 6a778eed2214..aa3899e48596 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 December 30, 2025
+.Dd January 12, 2026
 .Dt PF.CONF 5
 .Os
 .Sh NAME
@@ -2365,20 +2365,28 @@ For example, the following rule will drop 20% of incoming ICMP packets:
 .Bd -literal -offset indent
 block in proto icmp probability 20%
 .Ed
-.It Cm state limiter Ar name
+.It Cm state limiter Ar name Oo Cm (limiter options) Oc
 Use the specified state limiter to restrict the creation of states
 by this rule.
-If capacity is not available, the rule does not match and evaluation
-of the ruleset continues.
+By default if capacity is not available, the rule is ignored
+and ruleset evaluation continues with next rule..
+Use
+.Ic block
+option to change default behavior such packet is blocked
+when limit is reached.
 See the
 .Sx State Limiters
 section for more information.
 .Pp
-.It Cm source limiter Ar name
+.It Cm source limiter Ar name Oo Cm (limiter options) Oc
 Use the specified source limiter to restrict the creation of states
 by this rule.
-If capacity is not available, the rule does not match and evaluation
-of the ruleset continues.
+By default if capacity is not available, the rule is ignored
+and ruleset evaluation continues with next rule..
+Use
+.Ic block
+option to change default behavior such packet is blocked
+when limit is reached.
 See the
 .Sx Source Limiters
 section for more information.
@@ -3614,7 +3622,10 @@ filteropt      = user | group | flags | icmp-type | icmp6-type | "tos" tos |
                  "max-pkt-size" number |
                  "queue" ( string | "(" string [ [ "," ] string ] ")" ) |
                  "rtable" number | "probability" number"%" | "prio" number |
-                 "state limiter" name | "source limiter" name |
+                 "state limiter" name |
+                 "state limiter" name "(" limiter-opts ")" |
+                 "source limiter" name |
+                 "source limiter" name "(" limiter-opts ")" | "prio" number |
                  "dnpipe" ( number | "(" number "," number ")" ) |
                  "dnqueue" ( number | "(" number "," number ")" ) |
                  "ridentifier" number |
@@ -3794,6 +3805,7 @@ realtime-sc    = "realtime" sc-spec
 upperlimit-sc  = "upperlimit" sc-spec
 sc-spec        = ( bandwidth-spec |
                  "(" bandwidth-spec number bandwidth-spec ")" )
+limiter-opts   = "block" | "no-match"
 include        = "include" filename
 .Ed
 .Sh FILES
diff --git a/sys/net/pfvar.h b/sys/net/pfvar.h
index 5329c5ebdd9e..eb17c4ff5ef0 100644
--- a/sys/net/pfvar.h
+++ b/sys/net/pfvar.h
@@ -896,8 +896,14 @@ struct pf_krule {
 	u_int8_t		 set_prio[2];
 	sa_family_t		 naf;
 	u_int8_t		 rcvifnot;
-	uint8_t			 statelim;
-	uint8_t			 sourcelim;
+	struct {
+		uint8_t		 id;
+		int		 limiter_action;
+	}			 statelim;
+	struct {
+		uint8_t		 id;
+		int		 limiter_action;
+	}			 sourcelim;
 
 	struct {
 		struct pf_addr		addr;
@@ -1433,6 +1439,7 @@ struct pf_test_ctx {
 	int			 state_icmp;
 	int			 tag;
 	int			 rewrite;
+	int			 limiter_drop;
 	u_short			 reason;
 	struct pf_src_node	*sns[PF_SN_MAX];
 	struct pf_krule		*nr;
diff --git a/sys/netpfil/pf/pf.c b/sys/netpfil/pf/pf.c
index 79948b218428..13e2f5bb77f2 100644
--- a/sys/netpfil/pf/pf.c
+++ b/sys/netpfil/pf/pf.c
@@ -6105,8 +6105,8 @@ 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));
-		if (r->statelim != PF_STATELIM_ID_NONE) {
-			stlim = pf_statelim_find(r->statelim);
+		if (r->statelim.id != PF_STATELIM_ID_NONE) {
+			stlim = pf_statelim_find(r->statelim.id);
 
 			/*
 			 * Treat a missing limiter like an exhausted limiter.
@@ -6123,6 +6123,11 @@ pf_match_rule(struct pf_test_ctx *ctx, struct pf_kruleset *ruleset,
 				gen = pf_statelim_enter(stlim);
 				stlim->pfstlim_counters.hardlimited++;
 				pf_statelim_leave(stlim, gen);
+				if (r->statelim.limiter_action == PF_LIMITER_BLOCK) {
+					ctx->limiter_drop = 1;
+					REASON_SET(&ctx->reason, PFRES_MAXSTATES);
+					break;  /* stop rule processing */
+				}
 				r = TAILQ_NEXT(r, entries);
 				continue;
 			}
@@ -6140,6 +6145,14 @@ pf_match_rule(struct pf_test_ctx *ctx, struct pf_kruleset *ruleset,
 					gen = pf_statelim_enter(stlim);
 					stlim->pfstlim_counters.ratelimited++;
 					pf_statelim_leave(stlim, gen);
+					if (r->statelim.limiter_action ==
+					    PF_LIMITER_BLOCK) {
+						ctx->limiter_drop = 1;
+						REASON_SET(&ctx->reason,
+						    PFRES_MAXSTATES);
+						/* stop rule processing */
+						break;
+					}
 					r = TAILQ_NEXT(r, entries);
 					continue;
 				}
@@ -6152,10 +6165,10 @@ pf_match_rule(struct pf_test_ctx *ctx, struct pf_kruleset *ruleset,
 			}
 		}
 
-		if (r->sourcelim != PF_SOURCELIM_ID_NONE) {
+		if (r->sourcelim.id != PF_SOURCELIM_ID_NONE) {
 			struct pf_source key;
 
-			srlim = pf_sourcelim_find(r->sourcelim);
+			srlim = pf_sourcelim_find(r->sourcelim.id);
 
 			/*
 			 * Treat a missing pool like an overcommitted pool.
@@ -6177,6 +6190,14 @@ pf_match_rule(struct pf_test_ctx *ctx, struct pf_kruleset *ruleset,
 					gen = pf_sourcelim_enter(srlim);
 					srlim->pfsrlim_counters.hardlimited++;
 					pf_sourcelim_leave(srlim, gen);
+					if (r->sourcelim.limiter_action ==
+					    PF_LIMITER_BLOCK) {
+						ctx->limiter_drop = 1;
+						REASON_SET(&ctx->reason,
+						    PFRES_SRCLIMIT);
+						/* stop rule processing */
+						break;
+					}
 					r = TAILQ_NEXT(r, entries);
 					continue;
 				}
@@ -6196,6 +6217,14 @@ pf_match_rule(struct pf_test_ctx *ctx, struct pf_kruleset *ruleset,
 						srlim->pfsrlim_counters
 						    .ratelimited++;
 						pf_sourcelim_leave(srlim, gen);
+						if (r->sourcelim.limiter_action ==
+						    PF_LIMITER_BLOCK) {
+							ctx->limiter_drop = 1;
+							REASON_SET(&ctx->reason,
+							    PFRES_SRCLIMIT);
+							/* stop rules */
+							break;
+						}
 						r = TAILQ_NEXT(r, entries);
 						continue;
 					}
@@ -6460,10 +6489,8 @@ pf_test_rule(struct pf_krule **rm, struct pf_kstate **sm,
 	} else {
 		ruleset = &pf_main_ruleset;
 		rv = pf_match_rule(&ctx, ruleset, match_rules);
-		if (rv == PF_TEST_FAIL) {
-			/*
-			 * Reason has been set in pf_match_rule() already.
-			 */
+		if (rv == PF_TEST_FAIL || ctx.limiter_drop == 1) {
+			REASON_SET(reason, ctx.reason);
 			goto cleanup;
 		}
 
diff --git a/sys/netpfil/pf/pf.h b/sys/netpfil/pf/pf.h
index 333e5b53b0a8..4c950c7eab9c 100644
--- a/sys/netpfil/pf/pf.h
+++ b/sys/netpfil/pf/pf.h
@@ -501,6 +501,11 @@ struct pf_osfp_ioctl {
 #define	PF_ANCHOR_HIWAT		512
 #define	PF_OPTIMIZER_TABLE_PFX	"__automatic_"
 
+enum {
+	PF_LIMITER_NOMATCH,
+	PF_LIMITER_BLOCK
+};
+
 struct pf_rule {
 	struct pf_rule_addr	 src;
 	struct pf_rule_addr	 dst;
diff --git a/sys/netpfil/pf/pf_ioctl.c b/sys/netpfil/pf/pf_ioctl.c
index ddca4fae940b..5261b6e5ab41 100644
--- a/sys/netpfil/pf/pf_ioctl.c
+++ b/sys/netpfil/pf/pf_ioctl.c
@@ -2922,6 +2922,23 @@ pf_validate_range(uint8_t op, uint16_t port[2])
 	return 0;
 }
 
+static int
+pf_chk_limiter_action(int limiter_action)
+{
+	int rv;
+
+	switch (limiter_action) {
+	case PF_LIMITER_NOMATCH:
+	case PF_LIMITER_BLOCK:
+		rv = 0;
+		break;
+	default:
+		rv = 1;
+	}
+
+	return (rv);
+}
+
 int
 pf_ioctl_addrule(struct pf_krule *rule, uint32_t ticket,
     uint32_t pool_ticket, const char *anchor, const char *anchor_call,
@@ -2946,6 +2963,9 @@ pf_ioctl_addrule(struct pf_krule *rule, uint32_t ticket,
 		ERROUT_UNLOCKED(EINVAL);
 	if (pf_validate_range(rule->dst.port_op, rule->dst.port))
 		ERROUT_UNLOCKED(EINVAL);
+	if (pf_chk_limiter_action(rule->statelim.limiter_action) ||
+	    pf_chk_limiter_action(rule->sourcelim.limiter_action))
+		ERROUT_UNLOCKED(EINVAL);
 
 	if (rule->ifname[0])
 		kif = pf_kkif_create(M_WAITOK);
diff --git a/sys/netpfil/pf/pf_nl.c b/sys/netpfil/pf/pf_nl.c
index 9522fad10839..7a7655d7d9c8 100644
--- a/sys/netpfil/pf/pf_nl.c
+++ b/sys/netpfil/pf/pf_nl.c
@@ -784,8 +784,10 @@ static const struct nlattr_parser nla_p_rule[] = {
 	{ .type = PF_RT_MAX_PKT_SIZE, .off = _OUT(max_pkt_size), .cb = nlattr_get_uint16 },
 	{ .type = PF_RT_TYPE_2, .off = _OUT(type), .cb = nlattr_get_uint16 },
 	{ .type = PF_RT_CODE_2, .off = _OUT(code), .cb = nlattr_get_uint16 },
-	{ .type = PF_RT_STATE_LIMIT, .off = _OUT(statelim), .cb = nlattr_get_uint8 },
-	{ .type = PF_RT_SOURCE_LIMIT, .off = _OUT(sourcelim), .cb = nlattr_get_uint8 },
+	{ .type = PF_RT_STATE_LIMIT, .off = _OUT(statelim.id), .cb = nlattr_get_uint8 },
+	{ .type = PF_RT_SOURCE_LIMIT, .off = _OUT(sourcelim.id), .cb = nlattr_get_uint8 },
+	{ .type = PF_RT_STATE_LIMIT_ACTION, .off = _OUT(statelim.limiter_action), .cb = nlattr_get_uint32 },
+	{ .type = PF_RT_SOURCE_LIMIT_ACTION, .off = _OUT(sourcelim.limiter_action), .cb = nlattr_get_uint32 },
 };
 NL_DECLARE_ATTR_PARSER(rule_parser, nla_p_rule);
 #undef _OUT
@@ -1043,8 +1045,10 @@ pf_handle_getrule(struct nlmsghdr *hdr, struct nl_pstate *npt)
 	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);
 	nlattr_add_time_t(nw, PF_RT_EXPTIME, time_second - (time_uptime - rule->exptime));
-	nlattr_add_u8(nw, PF_RT_STATE_LIMIT, rule->statelim);
-	nlattr_add_u8(nw, PF_RT_SOURCE_LIMIT, rule->sourcelim);
+	nlattr_add_u8(nw, PF_RT_STATE_LIMIT, rule->statelim.id);
+	nlattr_add_u32(nw, PF_RT_STATE_LIMIT_ACTION, rule->statelim.limiter_action);
+	nlattr_add_u8(nw, PF_RT_SOURCE_LIMIT, rule->sourcelim.id);
+	nlattr_add_u32(nw, PF_RT_SOURCE_LIMIT_ACTION, rule->sourcelim.limiter_action);
 
 	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 696b81f9434e..84e9d3a97303 100644
--- a/sys/netpfil/pf/pf_nl.h
+++ b/sys/netpfil/pf/pf_nl.h
@@ -299,8 +299,10 @@ enum pf_rule_type_t {
 	PF_RT_TYPE_2		= 84, /* u16 */
 	PF_RT_CODE_2		= 85, /* u16 */
 	PF_RT_EXPTIME		= 86, /* time_t */
-	PF_RT_STATE_LIMIT	= 87, /* uint8_t */
-	PF_RT_SOURCE_LIMIT	= 88, /* uint8_t */
+	PF_RT_STATE_LIMIT	= 87, /* u8 */
+	PF_RT_SOURCE_LIMIT	= 88, /* u8 */
+	PF_RT_STATE_LIMIT_ACTION = 89, /* u32 */
+	PF_RT_SOURCE_LIMIT_ACTION = 90, /* u32 */
 };
 
 enum pf_addrule_type_t {