Index | Thread | Search

From:
Alexandr Nedvedicky <sashan@fastmail.net>
Subject:
pf(4) add timeout option to ip address tables
To:
tech@openbsd.org
Date:
Mon, 11 May 2026 03:05:27 +0200

Download raw body.

Thread
Hello,

diff below should help people who use 'overload' action in their
firewall configuration. This is how pf.conf(5) describes the
overload option:

     Because the 3-way handshake ensures that the source address is not being
     spoofed, more aggressive action can be taken based on these limits.  With
     the overload <table> state option, source IP addresses which hit either
     of the limits on established connections will be added to the named
     table.  This table can be used in the ruleset to block further activity
     from the offending host, redirect it to a tarpit process, or restrict its
     bandwidth.

As you can see pf(4) keeps adding addresses to table. Administrator
must clear the table which is used by 'overload' option.

The newly added 'Source Limiter' suffers from the same issue.
Source limiter may add the source IP address which exceeds
the limit to table. However administrator can not define
any duration how long the IP address should be kept in
table referred by limiter.

Diff below adds 'timeout' option for table, so administrator
can define duration in seconds for how long the IP address
is kept in table.

I think it's been pointed out by dlg@ long time ago similar
feature is missing in pf(4).

OK ?

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_optimize.c b/sbin/pfctl/pfctl_optimize.c
index fff28c97f3f..d4a78af1297 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,
@@ -1288,7 +1288,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..cd3be69a590 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);
diff --git a/sbin/pfctl/pfctl_parser.h b/sbin/pfctl/pfctl_parser.h
index c65a805ad90..c569530c332 100644
--- a/sbin/pfctl/pfctl_parser.h
+++ b/sbin/pfctl/pfctl_parser.h
@@ -276,17 +276,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..3eb25beedd0 100644
--- a/sbin/pfctl/pfctl_table.c
+++ b/sbin/pfctl/pfctl_table.c
@@ -57,7 +57,7 @@ extern void	usage(void);
 static void	print_table(struct pfr_table *, int, int);
 static void	print_tstats(struct pfr_tstats *, int);
 static int	load_addr(struct pfr_buffer *, int, char *[], char *, int, int);
-static void	print_addrx(struct pfr_addr *, struct pfr_addr *, int);
+static void	print_addrx(struct pfr_addr *, struct pfr_addr *, int, int);
 static void	print_astats(struct pfr_astats *, int);
 static void	xprintf(int, const char *, ...);
 static void	print_iface(struct pfi_kif *, int);
@@ -208,7 +208,7 @@ pfctl_table(int argc, char *argv[], char *tname, const char *command,
 				if (opts & PF_OPT_VERBOSE2 ||
 				    a->pfra_fback != PFR_FB_NONE)
 					print_addrx(a, NULL,
-					    opts & PF_OPT_USEDNS);
+					    opts & PF_OPT_USEDNS, 0);
 	} else if (!strcmp(command, "delete")) {
 		b.pfrb_type = PFRB_ADDRS;
 		if (load_addr(&b, argc, argv, file, 0, opts))
@@ -223,7 +223,7 @@ pfctl_table(int argc, char *argv[], char *tname, const char *command,
 				if (opts & PF_OPT_VERBOSE2 ||
 				    a->pfra_fback != PFR_FB_NONE)
 					print_addrx(a, NULL,
-					    opts & PF_OPT_USEDNS);
+					    opts & PF_OPT_USEDNS, 0);
 	} else if (!strcmp(command, "replace")) {
 		b.pfrb_type = PFRB_ADDRS;
 		if (load_addr(&b, argc, argv, file, 0, opts))
@@ -255,7 +255,7 @@ pfctl_table(int argc, char *argv[], char *tname, const char *command,
 				if (opts & PF_OPT_VERBOSE2 ||
 				    a->pfra_fback != PFR_FB_NONE)
 					print_addrx(a, NULL,
-					    opts & PF_OPT_USEDNS);
+					    opts & PF_OPT_USEDNS, 0);
 	} else if (!strcmp(command, "expire")) {
 		const char		*errstr;
 		u_int			 lifetime;
@@ -294,7 +294,7 @@ pfctl_table(int argc, char *argv[], char *tname, const char *command,
 				if (opts & PF_OPT_VERBOSE2 ||
 				    a->pfra_fback != PFR_FB_NONE)
 					print_addrx(a, NULL,
-					    opts & PF_OPT_USEDNS);
+					    opts & PF_OPT_USEDNS, 0);
 	} else if (!strcmp(command, "show")) {
 		b.pfrb_type = (opts & PF_OPT_VERBOSE) ?
 			PFRB_ASTATS : PFRB_ADDRS;
@@ -316,7 +316,7 @@ pfctl_table(int argc, char *argv[], char *tname, const char *command,
 			if (opts & PF_OPT_VERBOSE)
 				print_astats(p, opts & PF_OPT_USEDNS);
 			else
-				print_addrx(p, NULL, opts & PF_OPT_USEDNS);
+				print_addrx(p, NULL, opts & PF_OPT_USEDNS, 0);
 	} else if (!strcmp(command, "test")) {
 		b.pfrb_type = PFRB_ADDRS;
 		b2.pfrb_type = PFRB_ADDRS;
@@ -336,12 +336,12 @@ pfctl_table(int argc, char *argv[], char *tname, const char *command,
 			PFRB_FOREACH(a, &b)
 				if (a->pfra_fback == PFR_FB_MATCH)
 					print_addrx(a, NULL,
-					    opts & PF_OPT_USEDNS);
+					    opts & PF_OPT_USEDNS, 0);
 		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 & PF_OPT_USEDNS, 0);
 			}
 		}
 		if (nmatch < b.pfrb_size)
@@ -360,7 +360,7 @@ pfctl_table(int argc, char *argv[], char *tname, const char *command,
 				if (opts & PF_OPT_VERBOSE2 ||
 				    a->pfra_fback != PFR_FB_NONE)
 					print_addrx(a, NULL,
-					    opts & PF_OPT_USEDNS);
+					    opts & PF_OPT_USEDNS, 0);
 	} else if (!strcmp(command, "zero")) {
 		flags |= PFR_FLAG_ADDRSTOO;
 		RVTEST(pfr_clr_tstats(&table, 1, &nzero, flags));
@@ -383,18 +383,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 +455,19 @@ 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 dns, int verbose)
 {
 	char		ch, buf[256] = "{error}";
 	char		fb[] = { ' ', 'M', 'A', 'D', 'C', 'Z', 'X', ' ', 'Y', ' ' };
 	unsigned int	fback, hostnet;
+	time_t		now = time(NULL);
+
+	/*
+	 * do not print expired addresses, those are printed in verbose output
+	 * only.
+	 */
+	if (verbose == 0 && ad->pfra_expire != 0 && ad->pfra_expire < now)
+		return;
 
 	fback = (rad != NULL) ? rad->pfra_fback : ad->pfra_fback;
 	ch = (fback < sizeof(fb)/sizeof(*fb)) ? fb[fback] : '?';
@@ -499,6 +510,13 @@ print_addrx(struct pfr_addr *ad, struct pfr_addr *rad, int dns)
 	}
 	if (ad->pfra_ifname[0] != '\0')
 		printf("@%s", ad->pfra_ifname);
+	if (verbose != 0 && ad->pfra_expire != 0) {
+		if (ad->pfra_expire < now)
+			printf("\t[ expired %llds ago ]",
+			    now - ad->pfra_expire);
+		else
+			printf("\t[ expires in %llds ]", ad->pfra_expire - now);
+	}
 	printf("\n");
 }
 
@@ -510,7 +528,7 @@ print_astats(struct pfr_astats *as, int dns)
 	char	*ct;
 
 	ct = ctime(&time);
-	print_addrx(&as->pfras_a, NULL, dns);
+	print_addrx(&as->pfras_a, NULL, dns, 1);
 	if (ct)
 		printf("\tCleared:     %s", ctime(&time));
 	else
@@ -533,7 +551,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 +582,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..51182660df7 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
@@ -1137,8 +1139,11 @@ 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) {
+		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 +1153,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 +1360,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;
 
@@ -1408,7 +1416,6 @@ pfr_walktree(struct radix_node *rn, void *arg, u_int id)
 	case PFRW_GET_ADDRS:
 		if (w->pfrw_free-- > 0) {
 			struct pfr_addr ad;
-
 			pfr_copyout_addr(&ad, ke);
 			if (copyout(&ad, w->pfrw_addr, sizeof(ad)))
 				return (EFAULT);
@@ -1473,6 +1480,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);
 }
@@ -2421,6 +2434,21 @@ pfr_lookup_table(struct pfr_table *tbl)
 	    (struct pfr_ktable *)tbl));
 }
 
+int
+pfr_kentry_expired(struct pfr_kentry *ke)
+{
+	time_t	now;
+	int	expired;
+
+	if (ke->pfrke_expire != 0) {
+		now = gettime();
+		expired = (ke->pfrke_expire < now);
+	} else
+		expired = 0;
+
+	return (expired);
+}
+
 int
 pfr_match_addr(struct pfr_ktable *kt, struct pf_addr *a, sa_family_t af)
 {
@@ -2430,10 +2458,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(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 +2946,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;