Index | Thread | Search

From:
Alexandr Nedvedicky <sashan@fastmail.net>
Subject:
Re: pf(4) add timeout option to ip address tables
To:
tech@openbsd.org
Date:
Wed, 24 Jun 2026 23:47:43 +0200

Download raw body.

Thread
Hello,

sorry to keeping you waiting for update. I had no time to get
back to it until here at g2k26.

updated diff is something what I'd like to get tested. the
changes to manual page are yet to be polished. the same goes
to code, some comments are still too verbose.

this is the sample ruleset I was using to test my changes.

     1  table <minute> timeout(60)
     2  
     3  pass all
     4  
     5  block in on vio2
     6  block return in quick on vio2 from <minute> to any
     7  pass in on vio2 from 10.188.211.0/24 to any keep state \
	    (max-src-conn-rate 1/1, overload <minute>)

at line 1 table 'minute' with timeout set to 60secs is defined.
The table minute is then used by block rule at number 6 and
overload action in pass rule at line 7.

as soon as max-src-conn-rate 1/1 kicks in the offending IP address
is added to overload table. after 60secs timeout elapses the
address no longer matches at block rule at line 6. the remote IP
address is then free to create yet another state.

the expiration timer is reset whenever packet matches the address,
this keeps offending IP addresses in table, so the block rule
remains in effect. the traffic gets unblocked after offending
IP address is silent for desired timeout (60secs in this case).

One can use 'pfctl -t minute -T show' to see content of minute
table:

    pf# ./pfctl -t minute -T show    
       10.188.211.51        ttl     54

output above shows there is on IP address in overload table which
is going to expire in 54secs. The output of command above can
be redirected to file:

    pf# ./pfctl -t minute -T show > /tmp/minute.tab

If slightly modified ruleset is used:

     1  table <minute> timeout(60) file "/tmp/minute.tab"
     2  
     3  pass all
     4  
     5  block in on vio2
     6  block return in quick on vio2 from <minute> to any
     7  pass in on vio2 from 10.188.211.0/24 to any keep state \
	    (max-src-conn-rate 1/1, overload <minute>)

then saved addresses can be loaded to table just as shown here:

    # ./pfctl -t minute -T show
       10.188.211.51        ttl     56
    # ./pfctl -t minute -T show
       10.188.211.51        ttl     48
    # ./pfctl -t minute -T show > /tmp/minute.tab
    # ./pfctl -Fa # tables must be flushed, otherwise restore
		    from /tmp/minute.tab does not happen
    1 tables deleted.
    rules cleared
    7 states cleared
    source tracking entries cleared
    pf: statistics cleared
    pf: interface flags reset
    # ./pfctl -f pf-restore.conf                                                 
    # ./pfctl -t minute -T show
       10.188.211.51        ttl     27

note after restoring the table the does not start at 60secs
but at the saved value.

The expired addresses are not removed immediately. as shown here
using verbose output:

    # ./pfctl -v -t minute -T show
       10.188.211.51
	    Cleared:     Wed Jun 24 23:11:26 2026
	    Expired:     1029s ago

The diff below does 'very lazy' removal process. after packet hits
expired address the table is flagged to be purged. Then the periodic 
timer/purge thread steps in and removes all expired entries.

So my question now is if people here find it useful, or if there
is something what's missing and should be improved before I'll
start chasing for OKs.

thanks and
regards
sashan

--------8<---------------8<---------------8<------------------8<--------
diff --git a/sbin/pfctl/parse.y b/sbin/pfctl/parse.y
index 92764edcf3b..afb6dce4eef 100644
--- a/sbin/pfctl/parse.y
+++ b/sbin/pfctl/parse.y
@@ -348,6 +348,7 @@ struct queue_opts {
 struct table_opts {
 	int			flags;
 	int			init_addr;
+	uint32_t		timeout;
 	struct node_tinithead	init_nodes;
 } table_opts;
 
@@ -1401,6 +1402,16 @@ table_opt	: STRING		{
 			    entries);
 			table_opts.init_addr = 1;
 		}
+		| TIMEOUT '(' NUMBER ')' {
+			/*
+			 * timeout tables are intended for 'overload' action in
+			 * rules and limiters. They are not supposed to be
+			 * either constant nor manged from command line
+			 * (persistent). Also no support for counters.
+			 */
+			table_opts.flags = PFR_TFLAG_TIMEOUT;
+			table_opts.timeout = $3;
+		}
 		;
 
 tablespec	: xhost	optweight		{
@@ -4508,7 +4519,7 @@ process_tabledef(char *name, struct table_opts *opts, int popts)
 	}
 	if (pf->opts & PF_OPT_VERBOSE)
 		print_tabledef(name, opts->flags, opts->init_addr,
-		    &opts->init_nodes);
+		    opts->timeout, &opts->init_nodes);
 	if (!(pf->opts & PF_OPT_NOACTION) ||
 	    (pf->opts & PF_OPT_DUMMYACTION))
 		warn_duplicate_tables(name, pf->anchor->path);
@@ -4533,7 +4544,8 @@ process_tabledef(char *name, struct table_opts *opts, int popts)
 
 	if (!(pf->opts & PF_OPT_NOACTION) &&
 	    pfctl_define_table(name, opts->flags, opts->init_addr,
-	    pf->anchor->path, &ab, pf->anchor->ruleset.tticket, ukt)) {
+	    pf->anchor->path, opts->timeout, &ab, pf->anchor->ruleset.tticket,
+	    ukt)) {
 		yyerror("cannot define table %s: %s", name,
 		    pf_strerror(errno));
 		goto _error;
@@ -5027,7 +5039,7 @@ collapse_redirspec(struct pf_pool *rpool, struct pf_rule *r,
 		if (pf->opts & PF_OPT_VERBOSE)
 			print_tabledef(tbl->pt_name,
 			    PFR_TFLAG_CONST | tbl->pt_flags,
-			    1, &tbl->pt_nodes);
+			    1, 0, &tbl->pt_nodes);
 
 		memset(&rpool->addr, 0, sizeof(rpool->addr));
 		rpool->addr.type = PF_ADDR_TABLE;
diff --git a/sbin/pfctl/pfctl.h b/sbin/pfctl/pfctl.h
index 2c159b7e5d2..3dd03e03e8c 100644
--- a/sbin/pfctl/pfctl.h
+++ b/sbin/pfctl/pfctl.h
@@ -39,6 +39,12 @@
 #define DBGPRINT(...)	(void)(0)
 #endif
 
+enum {
+	PFR_ATTR_NONE,
+	PFR_ATTR_WEIGHT,
+	PFR_ATTR_TTL
+};
+
 enum pfctl_show { PFCTL_SHOW_RULES, PFCTL_SHOW_LABELS, PFCTL_SHOW_NOTHING };
 
 enum {	PFRB_TABLES = 1, PFRB_TSTATS, PFRB_ADDRS, PFRB_ASTATS,
diff --git a/sbin/pfctl/pfctl_optimize.c b/sbin/pfctl/pfctl_optimize.c
index 81d1bd03d98..7c475754271 100644
--- a/sbin/pfctl/pfctl_optimize.c
+++ b/sbin/pfctl/pfctl_optimize.c
@@ -564,7 +564,7 @@ combine_rules(struct pfctl *pf, struct superblock *block)
 
 			if (pf->opts & PF_OPT_VERBOSE)
 				print_tabledef(p1->por_src_tbl->pt_name,
-				    PFR_TFLAG_CONST, 1,
+				    PFR_TFLAG_CONST, 1, 0,
 				    &p1->por_src_tbl->pt_nodes);
 
 			memset(&p1->por_rule.src.addr, 0,
@@ -595,7 +595,7 @@ combine_rules(struct pfctl *pf, struct superblock *block)
 
 			if (pf->opts & PF_OPT_VERBOSE)
 				print_tabledef(p1->por_dst_tbl->pt_name,
-				    PFR_TFLAG_CONST, 1,
+				    PFR_TFLAG_CONST, 1, 0,
 				    &p1->por_dst_tbl->pt_nodes);
 
 			memset(&p1->por_rule.dst.addr, 0,
@@ -1289,7 +1289,7 @@ again:
 	tablenum++;
 
 	if (pfctl_define_table(tbl->pt_name, PFR_TFLAG_CONST | tbl->pt_flags, 1,
-	    pf->astack[0]->path, tbl->pt_buf, pf->astack[0]->ruleset.tticket,
+	    pf->astack[0]->path, 0, tbl->pt_buf, pf->astack[0]->ruleset.tticket,
 	    NULL)) {
 		warn("failed to create table %s in %s",
 		    tbl->pt_name, pf->astack[0]->name);
diff --git a/sbin/pfctl/pfctl_parser.c b/sbin/pfctl/pfctl_parser.c
index f47ee3e8568..0c3037b2df9 100644
--- a/sbin/pfctl/pfctl_parser.c
+++ b/sbin/pfctl/pfctl_parser.c
@@ -1229,7 +1229,7 @@ print_rule(struct pfctl *pf, struct pf_rule *r, const char *anchor_call,
 }
 
 void
-print_tabledef(const char *name, int flags, int addrs,
+print_tabledef(const char *name, int flags, int addrs, uint32_t timeout,
     struct node_tinithead *nodes)
 {
 	struct node_tinit	*ti, *nti;
@@ -1242,6 +1242,8 @@ print_tabledef(const char *name, int flags, int addrs,
 		printf(" persist");
 	if (flags & PFR_TFLAG_COUNTERS)
 		printf(" counters");
+	if (flags & PFR_TFLAG_TIMEOUT)
+		printf(" timeout(%u)", timeout);
 	SIMPLEQ_FOREACH(ti, nodes, entries) {
 		if (ti->file) {
 			printf(" file \"%s\"", ti->file);
@@ -1894,15 +1896,20 @@ append_addr(struct pfr_buffer *b, char *s, int test, int opts)
 	const char		*errstr;
 	int			 rv, not = 0, i = 0;
 	u_int16_t		 weight;
+	u_int32_t		 ttl;
 
 	/* skip weight if given */
 	if (strcmp(s, "weight") == 0) {
-		expect = 1;
+		expect = PFR_ATTR_WEIGHT;
 		return (1); /* expecting further call */
+	} else if (strcmp(s, "ttl") == 0) {
+		expect = PFR_ATTR_TTL;
+		return (1);
 	}
 
 	/* check if previous host is set */
-	if (expect) {
+	switch (expect) {
+	case PFR_ATTR_WEIGHT:
 		/* parse and append load balancing weight */
 		weight = strtonum(s, 1, USHRT_MAX, &errstr);
 		if (errstr) {
@@ -1917,8 +1924,24 @@ append_addr(struct pfr_buffer *b, char *s, int test, int opts)
 				}
 			}
 		}
-		expect = 0;
+		expect = PFR_ATTR_NONE;
+		return (0);
+	case PFR_ATTR_TTL:
+		ttl = strtonum(s, 1, UINT_MAX, &errstr);
+		if (errstr) {
+			fprintf(stderr, "failed to convert ttl %s\n", s);
+			return (-1);
+		}
+		if (previous != -1) {
+			PFRB_FOREACH(a, b) {
+				if (++i >= previous)
+					a->pfra_expire = ttl;
+			}
+		}
+		expect = PFR_ATTR_NONE;
 		return (0);
+	default:
+		(void)(0);
 	}
 
 	for (r = s; *r == '!'; r++)
diff --git a/sbin/pfctl/pfctl_parser.h b/sbin/pfctl/pfctl_parser.h
index c65a805ad90..475e37ced8e 100644
--- a/sbin/pfctl/pfctl_parser.h
+++ b/sbin/pfctl/pfctl_parser.h
@@ -53,6 +53,7 @@
 #define PF_OPT_PORTNAMES	0x08000
 #define PF_OPT_IGNFAIL		0x10000
 #define PF_OPT_CALLSHOW		0x20000
+#define PF_OPT_NOTTL		0x40000
 
 #define PF_TH_ALL		0xFF
 
@@ -276,17 +277,19 @@ int	pfctl_load_queues(struct pfctl *);
 int	pfctl_add_queue(struct pfctl *, struct pf_queuespec *);
 struct pfctl_qsitem *	pfctl_find_queue(char *, struct pf_qihead *);
 
-void	print_pool(struct pf_pool *, u_int16_t, u_int16_t, sa_family_t, int, int);
+void	print_pool(struct pf_pool *, u_int16_t, u_int16_t, sa_family_t, int,
+	    int);
 void	print_src_node(struct pf_src_node *, int);
 void	print_statelim(const struct pfioc_statelim *);
 void	print_sourcelim(const struct pfioc_sourcelim *);
 void	print_rule(struct pfctl *pf, struct pf_rule *, const char *, int);
-void	print_tabledef(const char *, int, int, struct node_tinithead *);
+void	print_tabledef(const char *, int, int, uint32_t,
+	    struct node_tinithead *);
 void	print_status(struct pf_status *, struct pfctl_watermarks *, int);
 void	print_queuespec(struct pf_queuespec *);
 
-int	pfctl_define_table(char *, int, int, const char *, struct pfr_buffer *,
-	    u_int32_t, struct pfr_uktable *);
+int	pfctl_define_table(char *, int, int, const char *, u_int32_t,
+	    struct pfr_buffer *, u_int32_t, struct pfr_uktable *);
 void	pfctl_expand_label_nr(struct pf_rule *, unsigned int);
 
 void		 pfctl_clear_fingerprints(int, int);
diff --git a/sbin/pfctl/pfctl_table.c b/sbin/pfctl/pfctl_table.c
index e460adf5f23..65b8a1f1a0a 100644
--- a/sbin/pfctl/pfctl_table.c
+++ b/sbin/pfctl/pfctl_table.c
@@ -207,8 +207,7 @@ pfctl_table(int argc, char *argv[], char *tname, const char *command,
 			PFRB_FOREACH(a, &b)
 				if (opts & PF_OPT_VERBOSE2 ||
 				    a->pfra_fback != PFR_FB_NONE)
-					print_addrx(a, NULL,
-					    opts & PF_OPT_USEDNS);
+					print_addrx(a, NULL, opts);
 	} else if (!strcmp(command, "delete")) {
 		b.pfrb_type = PFRB_ADDRS;
 		if (load_addr(&b, argc, argv, file, 0, opts))
@@ -222,8 +221,7 @@ pfctl_table(int argc, char *argv[], char *tname, const char *command,
 			PFRB_FOREACH(a, &b)
 				if (opts & PF_OPT_VERBOSE2 ||
 				    a->pfra_fback != PFR_FB_NONE)
-					print_addrx(a, NULL,
-					    opts & PF_OPT_USEDNS);
+					print_addrx(a, NULL, opts);
 	} else if (!strcmp(command, "replace")) {
 		b.pfrb_type = PFRB_ADDRS;
 		if (load_addr(&b, argc, argv, file, 0, opts))
@@ -254,8 +252,7 @@ pfctl_table(int argc, char *argv[], char *tname, const char *command,
 			PFRB_FOREACH(a, &b)
 				if (opts & PF_OPT_VERBOSE2 ||
 				    a->pfra_fback != PFR_FB_NONE)
-					print_addrx(a, NULL,
-					    opts & PF_OPT_USEDNS);
+					print_addrx(a, NULL, opts);
 	} else if (!strcmp(command, "expire")) {
 		const char		*errstr;
 		u_int			 lifetime;
@@ -293,8 +290,7 @@ pfctl_table(int argc, char *argv[], char *tname, const char *command,
 			PFRB_FOREACH(a, &b2)
 				if (opts & PF_OPT_VERBOSE2 ||
 				    a->pfra_fback != PFR_FB_NONE)
-					print_addrx(a, NULL,
-					    opts & PF_OPT_USEDNS);
+					print_addrx(a, NULL, opts);
 	} else if (!strcmp(command, "show")) {
 		b.pfrb_type = (opts & PF_OPT_VERBOSE) ?
 			PFRB_ASTATS : PFRB_ADDRS;
@@ -314,9 +310,9 @@ pfctl_table(int argc, char *argv[], char *tname, const char *command,
 		}
 		PFRB_FOREACH(p, &b)
 			if (opts & PF_OPT_VERBOSE)
-				print_astats(p, opts & PF_OPT_USEDNS);
+				print_astats(p, opts);
 			else
-				print_addrx(p, NULL, opts & PF_OPT_USEDNS);
+				print_addrx(p, NULL, opts);
 	} else if (!strcmp(command, "test")) {
 		b.pfrb_type = PFRB_ADDRS;
 		b2.pfrb_type = PFRB_ADDRS;
@@ -335,13 +331,12 @@ pfctl_table(int argc, char *argv[], char *tname, const char *command,
 		if ((opts & PF_OPT_VERBOSE) && !(opts & PF_OPT_VERBOSE2))
 			PFRB_FOREACH(a, &b)
 				if (a->pfra_fback == PFR_FB_MATCH)
-					print_addrx(a, NULL,
-					    opts & PF_OPT_USEDNS);
+					print_addrx(a, NULL, opts);
 		if (opts & PF_OPT_VERBOSE2) {
 			a2 = NULL;
 			PFRB_FOREACH(a, &b) {
 				a2 = pfr_buf_next(&b2, a2);
-				print_addrx(a2, a, opts & PF_OPT_USEDNS);
+				print_addrx(a2, a, opts);
 			}
 		}
 		if (nmatch < b.pfrb_size)
@@ -359,8 +354,7 @@ pfctl_table(int argc, char *argv[], char *tname, const char *command,
 			PFRB_FOREACH(a, &b)
 				if (opts & PF_OPT_VERBOSE2 ||
 				    a->pfra_fback != PFR_FB_NONE)
-					print_addrx(a, NULL,
-					    opts & PF_OPT_USEDNS);
+					print_addrx(a, NULL, opts);
 	} else if (!strcmp(command, "zero")) {
 		flags |= PFR_FLAG_ADDRSTOO;
 		RVTEST(pfr_clr_tstats(&table, 1, &nzero, flags));
@@ -383,18 +377,21 @@ print_table(struct pfr_table *ta, int verbose, int debug)
 	if (!debug && !(ta->pfrt_flags & PFR_TFLAG_ACTIVE))
 		return;
 	if (verbose)
-		printf("%c%c%c%c%c%c%c\t",
+		printf("%c%c%c%c%c%c%c%c\t",
 		    (ta->pfrt_flags & PFR_TFLAG_CONST) ? 'c' : '-',
 		    (ta->pfrt_flags & PFR_TFLAG_PERSIST) ? 'p' : '-',
 		    (ta->pfrt_flags & PFR_TFLAG_ACTIVE) ? 'a' : '-',
 		    (ta->pfrt_flags & PFR_TFLAG_INACTIVE) ? 'i' : '-',
 		    (ta->pfrt_flags & PFR_TFLAG_REFERENCED) ? 'r' : '-',
 		    (ta->pfrt_flags & PFR_TFLAG_REFDANCHOR) ? 'h' : '-',
+		    (ta->pfrt_flags & PFR_TFLAG_TIMEOUT) ? 't' : '-',
 		    (ta->pfrt_flags & PFR_TFLAG_COUNTERS) ? 'C' : '-');
 
 	printf("%s", ta->pfrt_name);
 	if (ta->pfrt_anchor[0] != '\0')
 		printf("@%s", ta->pfrt_anchor);
+	if (verbose && ta->pfrt_flags & PFR_TFLAG_TIMEOUT)
+		printf(" timeout(%u)", ta->pfrt_timeout);
 
 	printf("\n");
 }
@@ -452,11 +449,21 @@ load_addr(struct pfr_buffer *b, int argc, char *argv[], char *file,
 }
 
 void
-print_addrx(struct pfr_addr *ad, struct pfr_addr *rad, int dns)
+print_addrx(struct pfr_addr *ad, struct pfr_addr *rad, int opts)
 {
 	char		ch, buf[256] = "{error}";
 	char		fb[] = { ' ', 'M', 'A', 'D', 'C', 'Z', 'X', ' ', 'Y', ' ' };
 	unsigned int	fback, hostnet;
+	int		dns = (opts & PF_OPT_USEDNS);
+	int		nottl = (opts & PF_OPT_NOTTL);
+	time_t		now = time(NULL);
+
+	/*
+	 * do not print expired addresses unless caller asks to
+	 * ignore expiration timer.
+	 */
+	if (!nottl && ad->pfra_expire && ad->pfra_expire < now)
+		return;
 
 	fback = (rad != NULL) ? rad->pfra_fback : ad->pfra_fback;
 	ch = (fback < sizeof(fb)/sizeof(*fb)) ? fb[fback] : '?';
@@ -499,22 +506,44 @@ print_addrx(struct pfr_addr *ad, struct pfr_addr *rad, int dns)
 	}
 	if (ad->pfra_ifname[0] != '\0')
 		printf("@%s", ad->pfra_ifname);
+
+	/*
+	 * suppress printing of TTL when caller wants expiration
+	 * timer to be ignored.
+	 */
+	if (!nottl && ad->pfra_expire && ad->pfra_expire > now)
+		printf("\tttl\t%llu", ad->pfra_expire - now);
+
 	printf("\n");
 }
 
 void
-print_astats(struct pfr_astats *as, int dns)
+print_astats(struct pfr_astats *as, int opts)
 {
-	time_t	 time = as->pfras_tzero;
+	time_t	 tzero = as->pfras_tzero;
 	int	 dir, op;
 	char	*ct;
+	time_t	 now = time(NULL);
 
-	ct = ctime(&time);
-	print_addrx(&as->pfras_a, NULL, dns);
+	ct = ctime(&tzero);
+	/*
+	 * suppress printing of TTL for addresses with expiration
+	 * timer set. The TTL is printed here in verbose fashion.
+	 */
+	print_addrx(&as->pfras_a, NULL, opts | PF_OPT_NOTTL);
 	if (ct)
-		printf("\tCleared:     %s", ctime(&time));
+		printf("\tCleared:     %s", ct);
 	else
-		printf("\tCleared:     %lld\n", time);
+		printf("\tCleared:     %lld\n", tzero);
+
+	if (as->pfras_a.pfra_expire) {
+		if (as->pfras_a.pfra_expire < now)
+			printf("\tExpired:     %llds ago\n",
+			    now - as->pfras_a.pfra_expire);
+		else
+			printf("\tExpires in:  %llds\n",
+			    as->pfras_a.pfra_expire - now);
+	}
 	if (as->pfras_a.pfra_states)
 		printf("\tActive States:      %d\n", as->pfras_a.pfra_states);
 	if (as->pfras_a.pfra_type == PFRKE_COST)
@@ -533,7 +562,8 @@ print_astats(struct pfr_astats *as, int dns)
 
 int
 pfctl_define_table(char *name, int flags, int addrs, const char *anchor,
-    struct pfr_buffer *ab, u_int32_t ticket, struct pfr_uktable *ukt)
+    u_int32_t timeout, struct pfr_buffer *ab, u_int32_t ticket,
+    struct pfr_uktable *ukt)
 {
 	struct pfr_table tbl_buf;
 	struct pfr_table *tbl;
@@ -563,8 +593,9 @@ pfctl_define_table(char *name, int flags, int addrs, const char *anchor,
 	    sizeof(tbl->pfrt_anchor)) >= sizeof(tbl->pfrt_anchor))
 		errx(1, "%s: strlcpy", __func__);
 	tbl->pfrt_flags = flags;
-	DBGPRINT("%s %s@%s [%x]\n", __func__, tbl->pfrt_name,
-	    tbl->pfrt_anchor, tbl->pfrt_flags);
+	tbl->pfrt_timeout = timeout;
+	DBGPRINT("%s %s@%s [%x] (%u)\n", __func__, tbl->pfrt_name,
+	    tbl->pfrt_anchor, tbl->pfrt_flags, tbl->pfrt_timeout);
 
 	/*
 	 * non-root anchors processed by parse.y are loaded to kernel later.
diff --git a/share/man/man5/pf.conf.5 b/share/man/man5/pf.conf.5
index 3a383b23517..59ece54039b 100644
--- a/share/man/man5/pf.conf.5
+++ b/share/man/man5/pf.conf.5
@@ -1822,6 +1822,15 @@ The
 flag forces the kernel to keep the table even when no rules refer to it.
 If the flag is not set, the kernel will automatically remove the table
 when the last rule referring to it is flushed.
+.It timeout
+The
+.Cm timeout
+option sets timeout (in seconds) for which the IP address is kept in table.
+This is meant for tables used by
+.Cm overload
+action either in rule or in limiter.
+After the timeout elapses the IP address is removed from the table where
+timeout is set.
 .El
 .Pp
 This example creates a table called
@@ -3021,7 +3030,8 @@ antispoof-rule = "antispoof" [ "log" ] [ "quick" ]
 table-rule     = "table" "<" string ">" [ tableopts ]
 tableopts      = tableopt [ tableopts ]
 tableopt       = "persist" | "const" | "counters" |
-                 "file" string | "{" [ tableaddrs ] "}"
+                 "timeout" "(" number ")" | "file" string |
+                 "{" [ tableaddrs ] "}"
 tableaddrs     = tableaddr-spec [ [ "," ] tableaddrs ]
 tableaddr-spec = [ "!" ] tableaddr [ "/" mask-bits ]
 tableaddr      = hostname | ifspec | "self" |
diff --git a/sys/net/pf.c b/sys/net/pf.c
index 3ec43778805..702bd278ef3 100644
--- a/sys/net/pf.c
+++ b/sys/net/pf.c
@@ -1967,6 +1967,7 @@ pf_purge(void *null)
 
 	pf_purge_expired_src_nodes();
 	pf_source_purge();
+	pfr_purge_overload();
 
 	PF_UNLOCK();
 
diff --git a/sys/net/pf_table.c b/sys/net/pf_table.c
index 45c5533e55b..2da3a7c46c3 100644
--- a/sys/net/pf_table.c
+++ b/sys/net/pf_table.c
@@ -118,7 +118,8 @@ struct pfr_walktree {
 		PFRW_GET_ADDRS,
 		PFRW_GET_ASTATS,
 		PFRW_POOL_GET,
-		PFRW_DYNADDR_UPDATE
+		PFRW_DYNADDR_UPDATE,
+		PFRW_ENQUEUE_EXPIRED
 	}	 pfrw_op;
 	union {
 		struct pfr_addr		*pfrw1_addr;
@@ -129,6 +130,7 @@ struct pfr_walktree {
 	}	 pfrw_1;
 	int	 pfrw_free;
 	int	 pfrw_flags;
+	time_t	 pfrw_now;
 };
 #define pfrw_addr	pfrw_1.pfrw1_addr
 #define pfrw_astats	pfrw_1.pfrw1_astats
@@ -959,6 +961,7 @@ pfr_create_kentry(struct pfr_addr *ad)
 
 	switch (ke->pfrke_type) {
 	case PFRKE_PLAIN:
+		((struct pfr_kentry *)ke)->pfrke_expire = ad->pfra_expire;
 		break;
 	case PFRKE_COST:
 		((struct pfr_kentry_cost *)ke)->weight = ad->pfra_weight;
@@ -1016,6 +1019,8 @@ pfr_create_kentry_unlocked(struct pfr_addr *ad, int flags)
 
 	switch (ke->pfrke_type) {
 	case PFRKE_PLAIN:
+		((struct pfr_kentry_cost *)ke)->pfrke_expire = ad->pfra_expire;
+		break;
 		break;
 	case PFRKE_COST:
 		((struct pfr_kentry_cost *)ke)->weight = ad->pfra_weight;
@@ -1121,6 +1126,8 @@ pfr_insert_kentries(struct pfr_ktable *kt,
 			break;
 		}
 		p->pfrke_tzero = tzero;
+		if (kt->pfrkt_flags & PFR_TFLAG_TIMEOUT && p->pfrke_expire)
+			p->pfrke_expire += tzero;
 		++n;
 		if (p->pfrke_type == PFRKE_COST)
 			kt->pfrkt_refcntcost++;
@@ -1137,8 +1144,15 @@ pfr_insert_kentry(struct pfr_ktable *kt, struct pfr_addr *ad, time_t tzero)
 	int			 rv;
 
 	p = pfr_lookup_addr(kt, ad, 1);
-	if (p != NULL)
+	if (p != NULL) {
+		/*
+		 * we typically arrive here on behalf of overload action,
+		 * just reset timer to keep offending IP address in table.
+		 */
+		if (kt->pfrkt_flags & PFR_TFLAG_TIMEOUT)
+			p->pfrke_expire = tzero + kt->pfrkt_timeout;
 		return (0);
+	}
 	p = pfr_create_kentry(ad);
 	if (p == NULL)
 		return (EINVAL);
@@ -1148,6 +1162,8 @@ pfr_insert_kentry(struct pfr_ktable *kt, struct pfr_addr *ad, time_t tzero)
 		return (rv);
 
 	p->pfrke_tzero = tzero;
+	if (kt->pfrkt_flags & PFR_TFLAG_TIMEOUT)
+		p->pfrke_expire = tzero + kt->pfrkt_timeout;
 	if (p->pfrke_type == PFRKE_COST)
 		kt->pfrkt_refcntcost++;
 	kt->pfrkt_cnt++;
@@ -1353,6 +1369,7 @@ pfr_copyout_addr(struct pfr_addr *ad, struct pfr_kentry *ke)
 	ad->pfra_af = ke->pfrke_af;
 	ad->pfra_net = ke->pfrke_net;
 	ad->pfra_type = ke->pfrke_type;
+	ad->pfra_expire = ke->pfrke_expire;
 	if (ke->pfrke_flags & PFRKE_FLAG_NOT)
 		ad->pfra_not = 1;
 
@@ -1473,6 +1490,12 @@ pfr_walktree(struct radix_node *rn, void *arg, u_int id)
 			unhandled_af(ke->pfrke_af);
 		}
 		break;
+	case PFRW_ENQUEUE_EXPIRED:
+		if (ke->pfrke_expire != 0 && ke->pfrke_expire < w->pfrw_now) {
+			SLIST_INSERT_HEAD(w->pfrw_workq, ke, pfrke_workq);
+			w->pfrw_cnt++;
+		}
+		break;
 	}
 	return (0);
 }
@@ -2253,6 +2276,28 @@ void
 pfr_setflags_ktable(struct pfr_ktable *kt, int newf)
 {
 	struct pfr_kentryworkq	addrq;
+	int keep_timeout = kt->pfrkt_flags & PFR_TFLAG_TIMEOUT;
+
+	/*
+	 * the keep_timeout flag is poor man's solution to table flags
+	 * limitations. PFR_TFLAG_TIMEOUT is user defined flag and as
+	 * such must be covered by PFR_TFLAG_USRMASK so the table can
+	 * pass validation in pfr_validate_table() when it arrives via
+	 * ioctl(2).
+	 *
+	 * when pfr_ina_define() creates table on behalf of pf.conf parser,
+	 * the table is marked as inactive (PFR_TFLAG_ACTIVE flag is not set).
+	 * Here all inactive tables loose all user defined flags when this
+	 * line is executed for inactive table:
+	 *	newf &= ~PFR_TFLAG_USRMASK;
+	 *	kt->pfrkt_flags: 0x88	newf: 0x98                                                               
+	 *
+	 * However the PFR_TFLAG_TIMEOUT must be kept around for
+	 * pfr_insert_kentries() which is executed later when IP addresses
+	 * are being loaded to table. The _TIMEOUT flag tells
+	 * pfr_insert_kentries() to arm expiration timer for addresses that
+	 * are loaded.
+	 */
 
 	if (!(newf & PFR_TFLAG_REFERENCED) &&
 	    !(newf & PFR_TFLAG_REFDANCHOR) &&
@@ -2279,7 +2324,7 @@ pfr_setflags_ktable(struct pfr_ktable *kt, int newf)
 		pfr_destroy_ktable(kt->pfrkt_shadow, 1);
 		kt->pfrkt_shadow = NULL;
 	}
-	kt->pfrkt_flags = newf;
+	kt->pfrkt_flags = newf | keep_timeout;
 }
 
 void
@@ -2421,6 +2466,25 @@ pfr_lookup_table(struct pfr_table *tbl)
 	    (struct pfr_ktable *)tbl));
 }
 
+int
+pfr_kentry_expired(struct pfr_ktable *kt, struct pfr_kentry *ke)
+{
+	time_t	now;
+	int	expired;
+
+	if (kt->pfrkt_flags & PFR_TFLAG_TIMEOUT && ke->pfrke_expire != 0) {
+		now = gettime();
+		expired = (ke->pfrke_expire < now);
+		/*
+		 * reset timer to keep offending IP address in.
+		 */
+		ke->pfrke_expire = now + kt->pfrkt_timeout;
+	} else
+		expired = 0;
+
+	return (expired);
+}
+
 int
 pfr_match_addr(struct pfr_ktable *kt, struct pf_addr *a, sa_family_t af)
 {
@@ -2430,10 +2494,15 @@ pfr_match_addr(struct pfr_ktable *kt, struct pf_addr *a, sa_family_t af)
 	ke = pfr_kentry_byaddr(kt, a, af, 0);
 
 	match = (ke && !(ke->pfrke_flags & PFRKE_FLAG_NOT));
-	if (match)
+	if (match && pfr_kentry_expired(kt, ke) == 0)
 		kt->pfrkt_match++;
-	else
+	else {
 		kt->pfrkt_nomatch++;
+		if (ke != NULL) {
+			match = 0;
+			kt->pfrkt_flags |= PFR_TFLAG_NEED_PURGE;
+		}
+	}
 
 	return (match);
 }
@@ -2913,3 +2982,25 @@ pfr_ktable_select_active(struct pfr_ktable *kt)
 
 	return (kt);
 }
+
+void
+pfr_purge_overload(void)
+{
+	struct pfr_ktable	*kt;
+	struct pfr_kentryworkq	 workq;
+	struct pfr_walktree	 w;
+
+	RB_FOREACH(kt, pfr_ktablehead, &pfr_ktables) {
+		if (kt->pfrkt_flags & PFR_TFLAG_NEED_PURGE) {
+			bzero(&w, sizeof(w));
+			w.pfrw_op = PFRW_ENQUEUE_EXPIRED;
+			w.pfrw_now = gettime();
+			w.pfrw_workq = &workq;
+			SLIST_INIT(&workq);
+			rn_walktree(kt->pfrkt_ip4, pfr_walktree, &w);
+			rn_walktree(kt->pfrkt_ip6, pfr_walktree, &w);
+			pfr_remove_kentries(kt, &workq);
+			kt->pfrkt_flags &= ~PFR_TFLAG_NEED_PURGE;
+		}
+	}
+}
diff --git a/sys/net/pfvar.h b/sys/net/pfvar.h
index 750fe0ef144..5cc0bb8c3c9 100644
--- a/sys/net/pfvar.h
+++ b/sys/net/pfvar.h
@@ -872,15 +872,18 @@ RB_PROTOTYPE(pf_anchor_node, pf_anchor, entry_node, pf_anchor_compare)
 #define PFR_TFLAG_REFERENCED	0x00000010
 #define PFR_TFLAG_REFDANCHOR	0x00000020
 #define PFR_TFLAG_COUNTERS	0x00000040
+#define PFR_TFLAG_TIMEOUT	0x00000080
+#define PFR_TFLAG_NEED_PURGE	0x00000100
 /* Adjust masks below when adding flags. */
-#define PFR_TFLAG_USRMASK	0x00000043
-#define PFR_TFLAG_SETMASK	0x0000003C
-#define PFR_TFLAG_ALLMASK	0x0000007F
+#define PFR_TFLAG_USRMASK	0x000000C3
+#define PFR_TFLAG_SETMASK	0x0000013C
+#define PFR_TFLAG_ALLMASK	0x000001FF
 
 struct pfr_table {
 	char			 pfrt_anchor[PATH_MAX];
 	char			 pfrt_name[PF_TABLE_NAME_SIZE];
 	u_int32_t		 pfrt_flags;
+	u_int32_t		 pfrt_timeout;
 	u_int8_t		 pfrt_fback;
 };
 
@@ -894,6 +897,7 @@ struct pfr_addr {
 		struct in6_addr	 _pfra_ip6addr;
 	}		 pfra_u;
 	char		 pfra_ifname[IFNAMSIZ];
+	time_t		 pfra_expire;
 	u_int32_t	 pfra_states;
 	u_int16_t	 pfra_weight;
 	u_int8_t	 pfra_af;
@@ -932,6 +936,7 @@ struct pfr_tstats {
 };
 #define	pfrts_name	pfrts_t.pfrt_name
 #define pfrts_flags	pfrts_t.pfrt_flags
+#define pfrts_timeout	pfrts_t.pfrt_timeout
 
 struct pfr_kcounters {
 	u_int64_t		 pfrkc_packets[PFR_DIR_MAX][PFR_OP_ADDR_MAX];
@@ -958,6 +963,7 @@ struct _pfr_kentry {
 	SLIST_ENTRY(pfr_kentry)	 _pfrke_ioq;
 	struct pfr_kcounters	*_pfrke_counters;
 	time_t			 _pfrke_tzero;
+	time_t			 _pfrke_expire;
 	u_int8_t		 _pfrke_af;
 	u_int8_t		 _pfrke_net;
 	u_int8_t		 _pfrke_flags;
@@ -981,6 +987,7 @@ struct pfr_kentry {
 #define pfrke_ioq	u._ke._pfrke_ioq
 #define pfrke_counters	u._ke._pfrke_counters
 #define pfrke_tzero	u._ke._pfrke_tzero
+#define pfrke_expire	u._ke._pfrke_expire
 #define pfrke_af	u._ke._pfrke_af
 #define pfrke_net	u._ke._pfrke_net
 #define pfrke_flags	u._ke._pfrke_flags
@@ -1047,6 +1054,7 @@ struct pfr_ktable {
 #define pfrkt_match	pfrkt_ts.pfrts_match
 #define pfrkt_nomatch	pfrkt_ts.pfrts_nomatch
 #define pfrkt_tzero	pfrkt_ts.pfrts_tzero
+#define pfrkt_timeout	pfrkt_ts.pfrts_timeout
 
 RB_HEAD(pfi_ifhead, pfi_kif);
 
@@ -1895,6 +1903,7 @@ int	pfr_ina_define(struct pfr_table *, struct pfr_addr *, int, int *,
 	    int *, u_int32_t, int);
 struct pfr_ktable
 	*pfr_ktable_select_active(struct pfr_ktable *);
+void	pfr_purge_overload(void);
 
 extern struct pfi_kif		*pfi_all;