aboutsummaryrefslogtreecommitdiff
path: root/sys/netpfil
diff options
context:
space:
mode:
authorKristof Provost <kp@FreeBSD.org>2025-12-30 19:06:48 +0000
committerKristof Provost <kp@FreeBSD.org>2026-01-14 06:44:38 +0000
commit4616481212302b5d875cfc7a00766af017318f7f (patch)
tree96d5023704b950ed22ce7dbe8648149cfc8be725 /sys/netpfil
parentc498eaa2f9090d7bdc6456181d8bf74869288bbb (diff)
Diffstat (limited to 'sys/netpfil')
-rw-r--r--sys/netpfil/pf/pf.c647
-rw-r--r--sys/netpfil/pf/pf_ioctl.c922
-rw-r--r--sys/netpfil/pf/pf_nl.c4
-rw-r--r--sys/netpfil/pf/pf_nl.h2
-rw-r--r--sys/netpfil/pf/pf_table.c20
5 files changed, 1595 insertions, 0 deletions
diff --git a/sys/netpfil/pf/pf.c b/sys/netpfil/pf/pf.c
index 60ca9039e9ce..88b110d744ee 100644
--- a/sys/netpfil/pf/pf.c
+++ b/sys/netpfil/pf/pf.c
@@ -360,6 +360,8 @@ static int pf_tcp_track_sloppy(struct pf_kstate *,
struct pf_pdesc *, u_short *,
struct pf_state_peer *, struct pf_state_peer *,
u_int8_t, u_int8_t);
+static __inline int pf_synproxy_ack(struct pf_krule *, struct pf_pdesc *,
+ struct pf_kstate **, struct pf_rule_actions *);
static int pf_test_state(struct pf_kstate **, struct pf_pdesc *,
u_short *);
int pf_icmp_state_lookup(struct pf_state_key_cmp *,
@@ -426,6 +428,269 @@ static __inline void pf_set_protostate(struct pf_kstate *, int, u_int8_t);
int in4_cksum(struct mbuf *m, u_int8_t nxt, int off, int len);
+static inline int
+pf_statelim_id_cmp(const struct pf_statelim *a, const struct pf_statelim *b)
+{
+ if (a->pfstlim_id > b->pfstlim_id)
+ return (1);
+ if (a->pfstlim_id < b->pfstlim_id)
+ return (-1);
+
+ return (0);
+}
+
+RB_GENERATE(pf_statelim_id_tree, pf_statelim, pfstlim_id_tree,
+ pf_statelim_id_cmp);
+
+static inline int
+pf_statelim_nm_cmp(const struct pf_statelim *a, const struct pf_statelim *b)
+{
+ return (strncmp(a->pfstlim_nm, b->pfstlim_nm, sizeof(a->pfstlim_nm)));
+}
+
+RB_GENERATE(pf_statelim_nm_tree, pf_statelim, pfstlim_nm_tree,
+ pf_statelim_nm_cmp);
+
+VNET_DEFINE(struct pf_statelim_id_tree, pf_statelim_id_tree_active);
+VNET_DEFINE(struct pf_statelim_list, pf_statelim_list_active);
+VNET_DEFINE(struct pf_statelim_id_tree, pf_statelim_id_tree_inactive);
+VNET_DEFINE(struct pf_statelim_nm_tree, pf_statelim_nm_tree_inactive);
+VNET_DEFINE(struct pf_statelim_list, pf_statelim_list_inactive);
+
+static inline int
+pf_sourcelim_id_cmp(const struct pf_sourcelim *a, const struct pf_sourcelim *b)
+{
+ if (a->pfsrlim_id > b->pfsrlim_id)
+ return (1);
+ if (a->pfsrlim_id < b->pfsrlim_id)
+ return (-1);
+
+ return (0);
+}
+
+RB_GENERATE(pf_sourcelim_id_tree, pf_sourcelim, pfsrlim_id_tree,
+ pf_sourcelim_id_cmp);
+
+static inline int
+pf_sourcelim_nm_cmp(const struct pf_sourcelim *a, const struct pf_sourcelim *b)
+{
+ return (strncmp(a->pfsrlim_nm, b->pfsrlim_nm, sizeof(a->pfsrlim_nm)));
+}
+
+RB_GENERATE(pf_sourcelim_nm_tree, pf_sourcelim, pfsrlim_nm_tree,
+ pf_sourcelim_nm_cmp);
+
+static inline int
+pf_source_cmp(const struct pf_source *a, const struct pf_source *b)
+{
+ if (a->pfsr_af > b->pfsr_af)
+ return (1);
+ if (a->pfsr_af < b->pfsr_af)
+ return (-1);
+ if (a->pfsr_rdomain > b->pfsr_rdomain)
+ return (1);
+ if (a->pfsr_rdomain < b->pfsr_rdomain)
+ return (-1);
+
+ return (pf_addr_cmp(&a->pfsr_addr, &b->pfsr_addr, a->pfsr_af));
+}
+
+RB_GENERATE(pf_source_tree, pf_source, pfsr_tree, pf_source_cmp);
+
+static inline int
+pf_source_ioc_cmp(const struct pf_source *a, const struct pf_source *b)
+{
+ size_t i;
+
+ if (a->pfsr_af > b->pfsr_af)
+ return (1);
+ if (a->pfsr_af < b->pfsr_af)
+ return (-1);
+ if (a->pfsr_rdomain > b->pfsr_rdomain)
+ return (1);
+ if (a->pfsr_rdomain < b->pfsr_rdomain)
+ return (-1);
+
+ for (i = 0; i < nitems(a->pfsr_addr.addr32); i++) {
+ uint32_t wa = ntohl(a->pfsr_addr.addr32[i]);
+ uint32_t wb = ntohl(b->pfsr_addr.addr32[i]);
+
+ if (wa > wb)
+ return (1);
+ if (wa < wb)
+ return (-1);
+ }
+
+ return (0);
+}
+
+RB_GENERATE(pf_source_ioc_tree, pf_source, pfsr_ioc_tree, pf_source_ioc_cmp);
+
+VNET_DEFINE(struct pf_sourcelim_id_tree, pf_sourcelim_id_tree_active);
+VNET_DEFINE(struct pf_sourcelim_list, pf_sourcelim_list_active);
+
+VNET_DEFINE(struct pf_sourcelim_id_tree, pf_sourcelim_id_tree_inactive);
+VNET_DEFINE(struct pf_sourcelim_nm_tree, pf_sourcelim_nm_tree_inactive);
+VNET_DEFINE(struct pf_sourcelim_list, pf_sourcelim_list_inactive);
+
+static inline struct pf_statelim *
+pf_statelim_find(uint32_t id)
+{
+ struct pf_statelim key;
+
+ /* only the id is used in cmp, so don't have to zero all the things */
+ key.pfstlim_id = id;
+
+ return (RB_FIND(pf_statelim_id_tree,
+ &V_pf_statelim_id_tree_active, &key));
+}
+
+static inline struct pf_sourcelim *
+pf_sourcelim_find(uint32_t id)
+{
+ struct pf_sourcelim key;
+
+ /* only the id is used in cmp, so don't have to zero all the things */
+ key.pfsrlim_id = id;
+
+ return (RB_FIND(pf_sourcelim_id_tree,
+ &V_pf_sourcelim_id_tree_active, &key));
+}
+
+struct pf_source_list pf_source_gc = TAILQ_HEAD_INITIALIZER(pf_source_gc);
+
+static void
+pf_source_purge(void)
+{
+ struct pf_source *sr, *nsr;
+
+ TAILQ_FOREACH_SAFE(sr, &pf_source_gc, pfsr_empty_gc, nsr) {
+ struct pf_sourcelim *srlim = sr->pfsr_parent;
+
+ if (time_uptime <= sr->pfsr_empty_ts +
+ srlim->pfsrlim_rate.seconds + 1)
+ continue;
+
+ TAILQ_REMOVE(&pf_source_gc, sr, pfsr_empty_gc);
+
+ RB_REMOVE(pf_source_tree, &srlim->pfsrlim_sources, sr);
+ RB_REMOVE(pf_source_ioc_tree, &srlim->pfsrlim_ioc_sources, sr);
+ srlim->pfsrlim_nsources--;
+
+ free(sr, M_PF_SOURCE_LIM);
+ }
+}
+
+static void
+pf_source_pfr_addr(struct pfr_addr *p, const struct pf_source *sr)
+{
+ struct pf_sourcelim *srlim = sr->pfsr_parent;
+
+ memset(p, 0, sizeof(*p));
+
+ p->pfra_af = sr->pfsr_af;
+ switch (sr->pfsr_af) {
+ case AF_INET:
+ p->pfra_net = srlim->pfsrlim_ipv4_prefix;
+ p->pfra_ip4addr = sr->pfsr_addr.v4;
+ break;
+#ifdef INET6
+ case AF_INET6:
+ p->pfra_net = srlim->pfsrlim_ipv6_prefix;
+ p->pfra_ip6addr = sr->pfsr_addr.v6;
+ break;
+#endif /* INET6 */
+ }
+}
+
+static void
+pf_source_used(struct pf_source *sr)
+{
+ struct pf_sourcelim *srlim = sr->pfsr_parent;
+ struct pfr_ktable *t;
+ unsigned int used;
+
+ used = sr->pfsr_inuse++;
+ sr->pfsr_rate_ts += srlim->pfsrlim_rate_token;
+
+ if (used == 0)
+ TAILQ_REMOVE(&pf_source_gc, sr, pfsr_empty_gc);
+ else if ((t = srlim->pfsrlim_overload.table) != NULL &&
+ used >= srlim->pfsrlim_overload.hwm && !sr->pfsr_intable) {
+ struct pfr_addr p;
+
+ pf_source_pfr_addr(&p, sr);
+
+ pfr_insert_kentry(t, &p, time_second);
+ sr->pfsr_intable = 1;
+ }
+}
+
+static void
+pf_source_rele(struct pf_source *sr)
+{
+ struct pf_sourcelim *srlim = sr->pfsr_parent;
+ struct pfr_ktable *t;
+ unsigned int used;
+
+ used = --sr->pfsr_inuse;
+
+ t = srlim->pfsrlim_overload.table;
+ if (t != NULL && sr->pfsr_intable &&
+ used < srlim->pfsrlim_overload.lwm) {
+ struct pfr_addr p;
+
+ pf_source_pfr_addr(&p, sr);
+
+ pfr_remove_kentry(t, &p);
+ sr->pfsr_intable = 0;
+ }
+
+ if (used == 0) {
+ TAILQ_INSERT_TAIL(&pf_source_gc, sr, pfsr_empty_gc);
+ sr->pfsr_empty_ts = time_uptime + srlim->pfsrlim_rate.seconds;
+ }
+}
+
+static inline void
+pf_source_key(struct pf_sourcelim *srlim, struct pf_source *key,
+ sa_family_t af, const struct pf_addr *addr)
+{
+ size_t i;
+
+ /* only af+addr is used for lookup. */
+ key->pfsr_af = af;
+ key->pfsr_rdomain = 0;
+ switch (af) {
+ case AF_INET:
+ key->pfsr_addr.addr32[0] =
+ srlim->pfsrlim_ipv4_mask.v4.s_addr &
+ addr->v4.s_addr;
+
+ for (i = 1; i < nitems(key->pfsr_addr.addr32); i++)
+ key->pfsr_addr.addr32[i] = htonl(0);
+ break;
+#ifdef INET6
+ case AF_INET6:
+ for (i = 0; i < nitems(key->pfsr_addr.addr32); i++) {
+ key->pfsr_addr.addr32[i] =
+ srlim->pfsrlim_ipv6_mask.addr32[i] &
+ addr->addr32[i];
+ }
+ break;
+#endif
+ default:
+ unhandled_af(af);
+ /* NOTREACHED */
+ }
+}
+
+static inline struct pf_source *
+pf_source_find(struct pf_sourcelim *srlim, struct pf_source *key)
+{
+ return (RB_FIND(pf_source_tree, &srlim->pfsrlim_sources, key));
+}
+
extern int pf_end_threads;
extern struct proc *pf_purge_proc;
@@ -519,6 +784,8 @@ BOUND_IFACE(struct pf_kstate *st, struct pf_pdesc *pd)
MALLOC_DEFINE(M_PFHASH, "pf_hash", "pf(4) hash header structures");
MALLOC_DEFINE(M_PF_RULE_ITEM, "pf_krule_item", "pf(4) rule items");
+MALLOC_DEFINE(M_PF_STATE_LINK, "pf_state_link", "pf(4) state links");
+MALLOC_DEFINE(M_PF_SOURCE_LIM, "pf_source_lim", "pf(4) source limiter");
VNET_DEFINE(struct pf_keyhash *, pf_keyhash);
VNET_DEFINE(struct pf_idhash *, pf_idhash);
VNET_DEFINE(struct pf_srchash *, pf_srchash);
@@ -1295,6 +1562,22 @@ pf_initialize(void)
/* Unlinked, but may be referenced rules. */
TAILQ_INIT(&V_pf_unlinked_rules);
+
+ /* State limiters */
+ RB_INIT(&V_pf_statelim_id_tree_inactive);
+ RB_INIT(&V_pf_statelim_nm_tree_inactive);
+ TAILQ_INIT(&V_pf_statelim_list_inactive);
+
+ RB_INIT(&V_pf_statelim_id_tree_active);
+ TAILQ_INIT(&V_pf_statelim_list_active);
+
+ /* Source limiters */
+ RB_INIT(&V_pf_sourcelim_id_tree_active);
+ TAILQ_INIT(&V_pf_sourcelim_list_active);
+
+ RB_INIT(&V_pf_sourcelim_id_tree_inactive);
+ RB_INIT(&V_pf_sourcelim_nm_tree_inactive);
+ TAILQ_INIT(&V_pf_sourcelim_list_inactive);
}
void
@@ -2680,6 +2963,7 @@ pf_purge_thread(void *unused __unused)
pf_purge_expired_fragments();
pf_purge_expired_src_nodes();
pf_purge_unlinked_rules();
+ pf_source_purge();
pfi_kkif_purge();
}
CURVNET_RESTORE();
@@ -2712,6 +2996,7 @@ pf_unload_vnet_purge(void)
pf_purge_expired_states(0, V_pf_hashmask);
pf_purge_fragments(UINT_MAX);
pf_purge_expired_src_nodes();
+ pf_source_purge();
/*
* Now all kifs & rules should be unreferenced,
@@ -2817,6 +3102,7 @@ int
pf_remove_state(struct pf_kstate *s)
{
struct pf_idhash *ih = &V_pf_idhash[PF_IDHASH(s)];
+ struct pf_state_link *pfl;
NET_EPOCH_ASSERT();
PF_HASHROW_ASSERT(ih);
@@ -2858,6 +3144,63 @@ pf_remove_state(struct pf_kstate *s)
s->key[PF_SK_STACK]->proto == IPPROTO_TCP)
pf_set_protostate(s, PF_PEER_BOTH, TCPS_CLOSED);
+ while ((pfl = SLIST_FIRST(&s->linkage)) != NULL) {
+ struct pf_state_link_list *list;
+ unsigned int gen;
+
+ SLIST_REMOVE_HEAD(&s->linkage, pfl_linkage);
+
+ switch (pfl->pfl_type) {
+ case PF_STATE_LINK_TYPE_STATELIM: {
+ struct pf_statelim *stlim;
+
+ stlim = pf_statelim_find(s->statelim);
+ KASSERT(stlim != NULL,
+ ("pf_state %p pfl %p cannot find statelim %u", s,
+ pfl, s->statelim));
+
+ gen = pf_statelim_enter(stlim);
+ stlim->pfstlim_inuse--;
+ pf_statelim_leave(stlim, gen);
+
+ list = &stlim->pfstlim_states;
+ break;
+ }
+ case PF_STATE_LINK_TYPE_SOURCELIM: {
+ struct pf_sourcelim *srlim;
+ struct pf_source key, *sr;
+
+ srlim = pf_sourcelim_find(s->sourcelim);
+ KASSERT(srlim != NULL,
+ ("pf_state %p pfl %p cannot find sourcelim %u", s,
+ pfl, s->sourcelim));
+
+ pf_source_key(srlim, &key, s->key[PF_SK_WIRE]->af,
+ &s->key[PF_SK_WIRE]->addr[0 /* XXX or 1? */]);
+
+ sr = pf_source_find(srlim, &key);
+ KASSERT(sr != NULL,
+ ("pf_state %p pfl %p cannot find source in %u", s,
+ pfl, s->sourcelim));
+
+ gen = pf_sourcelim_enter(srlim);
+ srlim->pfsrlim_counters.inuse--;
+ pf_sourcelim_leave(srlim, gen);
+ pf_source_rele(sr);
+
+ list = &sr->pfsr_states;
+ break;
+ }
+ default:
+ panic("%s: unexpected link type on pfl %p", __func__,
+ pfl);
+ }
+
+ PF_STATE_LOCK_ASSERT(s);
+ TAILQ_REMOVE(list, pfl, pfl_link);
+ free(pfl, M_PF_STATE_LINK);
+ }
+
PF_HASHROW_UNLOCK(ih);
pf_detach_state(s);
@@ -5656,6 +5999,11 @@ pf_match_rule(struct pf_test_ctx *ctx, struct pf_kruleset *ruleset,
r = TAILQ_FIRST(ruleset->rules[PF_RULESET_FILTER].active.ptr);
while (r != NULL) {
+ struct pf_statelim *stlim = NULL;
+ struct pf_sourcelim *srlim = NULL;
+ struct pf_source *sr = NULL;
+ unsigned int gen;
+
if (ctx->pd->related_rule) {
*ctx->rm = ctx->pd->related_rule;
break;
@@ -5757,6 +6105,124 @@ 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);
+
+ /*
+ * Treat a missing limiter like an exhausted limiter.
+ * There is no "backend" to get a resource out of
+ * so the rule can't create state.
+ */
+ PF_TEST_ATTRIB(stlim == NULL, TAILQ_NEXT(r, entries));
+
+ /*
+ * An overcommitted pool means this rule
+ * can't create state.
+ */
+ if (stlim->pfstlim_inuse >= stlim->pfstlim_limit) {
+ gen = pf_statelim_enter(stlim);
+ stlim->pfstlim_counters.hardlimited++;
+ pf_statelim_leave(stlim, gen);
+ r = TAILQ_NEXT(r, entries);
+ continue;
+ }
+
+ /*
+ * Is access to the pool rate limited?
+ */
+ if (stlim->pfstlim_rate.limit != 0) {
+ struct timespec ts;
+ getnanouptime(&ts);
+ uint64_t diff = SEC_TO_NSEC(ts.tv_sec) +
+ ts.tv_nsec - stlim->pfstlim_rate_ts;
+
+ if (diff < stlim->pfstlim_rate_token) {
+ gen = pf_statelim_enter(stlim);
+ stlim->pfstlim_counters.ratelimited++;
+ pf_statelim_leave(stlim, gen);
+ r = TAILQ_NEXT(r, entries);
+ continue;
+ }
+
+ if (diff > stlim->pfstlim_rate_bucket) {
+ stlim->pfstlim_rate_ts =
+ SEC_TO_NSEC(ts.tv_sec) + ts.tv_nsec -
+ stlim->pfstlim_rate_bucket;
+ }
+ }
+ }
+
+ if (r->sourcelim != PF_SOURCELIM_ID_NONE) {
+ struct pf_source key;
+
+ srlim = pf_sourcelim_find(r->sourcelim);
+
+ /*
+ * Treat a missing pool like an overcommitted pool.
+ * There is no "backend" to get a resource out of
+ * so the rule can't create state.
+ */
+ PF_TEST_ATTRIB(srlim == NULL, TAILQ_NEXT(r, entries));
+
+ pf_source_key(srlim, &key, ctx->pd->af,
+ ctx->pd->src);
+ sr = pf_source_find(srlim, &key);
+ if (sr != NULL) {
+ /*
+ * An overcommitted limiter means this rule
+ * can't create state.
+ */
+ if (sr->pfsr_inuse >= srlim->pfsrlim_limit) {
+ sr->pfsr_counters.hardlimited++;
+ gen = pf_sourcelim_enter(srlim);
+ srlim->pfsrlim_counters.hardlimited++;
+ pf_sourcelim_leave(srlim, gen);
+ r = TAILQ_NEXT(r, entries);
+ continue;
+ }
+
+ /*
+ * Is access to the pool rate limited?
+ */
+ if (srlim->pfsrlim_rate.limit != 0) {
+ struct timespec ts;
+ getnanouptime(&ts);
+ uint64_t diff = SEC_TO_NSEC(ts.tv_sec) +
+ ts.tv_nsec - sr->pfsr_rate_ts;
+
+ if (diff < srlim->pfsrlim_rate_token) {
+ sr->pfsr_counters.ratelimited++;
+ gen = pf_sourcelim_enter(srlim);
+ srlim->pfsrlim_counters
+ .ratelimited++;
+ pf_sourcelim_leave(srlim, gen);
+ r = TAILQ_NEXT(r, entries);
+ continue;
+ }
+
+ if (diff > srlim->pfsrlim_rate_bucket) {
+ sr->pfsr_rate_ts =
+ SEC_TO_NSEC(ts.tv_sec) + ts.tv_nsec -
+ srlim->pfsrlim_rate_bucket;
+ }
+ }
+ } else {
+ /*
+ * a new source entry will (should)
+ * admit a state.
+ */
+
+ if (srlim->pfsrlim_nsources >=
+ srlim->pfsrlim_entries) {
+ gen = pf_sourcelim_enter(srlim);
+ srlim->pfsrlim_counters.addrlimited++;
+ pf_sourcelim_leave(srlim, gen);
+ r = TAILQ_NEXT(r, entries);
+ continue;
+ }
+ }
+ }
+
/* must be last! */
if (r->pktrate.limit) {
PF_TEST_ATTRIB((pf_check_threshold(&r->pktrate)),
@@ -5833,6 +6299,13 @@ pf_match_rule(struct pf_test_ctx *ctx, struct pf_kruleset *ruleset,
* ruleset, where anchor belongs to.
*/
ctx->arsm = ctx->aruleset;
+ /*
+ * state/source pools
+ */
+
+ ctx->statelim = stlim;
+ ctx->sourcelim = srlim;
+ ctx->source = sr;
}
if (pd->act.log & PF_LOG_MATCHES)
pf_log_matches(pd, r, ctx->a, ruleset, match_rules);
@@ -6085,6 +6558,13 @@ pf_test_rule(struct pf_krule **rm, struct pf_kstate **sm,
return (action);
}
+ if (pd->proto == IPPROTO_TCP &&
+ r->keep_state == PF_STATE_SYNPROXY && pd->dir == PF_IN) {
+ action = pf_synproxy_ack(r, pd, sm, &ctx.act);
+ if (action != PF_PASS)
+ goto cleanup; /* PF_SYNPROXY_DROP */
+ }
+
nat64 = pd->af != pd->naf;
if (nat64) {
int ret;
@@ -6157,6 +6637,10 @@ pf_create_state(struct pf_krule *r, struct pf_test_ctx *ctx,
{
struct pf_pdesc *pd = ctx->pd;
struct pf_kstate *s = NULL;
+ struct pf_statelim *stlim = NULL;
+ struct pf_sourcelim *srlim = NULL;
+ struct pf_source *sr = NULL;
+ struct pf_state_link *pfl;
struct pf_ksrc_node *sns[PF_SN_MAX] = { NULL };
/*
* XXXKS: The hash for PF_SN_LIMIT and PF_SN_ROUTE should be the same
@@ -6219,6 +6703,7 @@ pf_create_state(struct pf_krule *r, struct pf_test_ctx *ctx,
s->nat_rule = ctx->nr;
s->anchor = ctx->a;
s->match_rules = *match_rules;
+ SLIST_INIT(&s->linkage);
memcpy(&s->act, &pd->act, sizeof(struct pf_rule_actions));
if (pd->act.allow_opts)
@@ -6334,6 +6819,98 @@ pf_create_state(struct pf_krule *r, struct pf_test_ctx *ctx,
KASSERT((ctx->sk != NULL && ctx->nk != NULL), ("%s: nr %p sk %p, nk %p",
__func__, ctx->nr, ctx->sk, ctx->nk));
+ stlim = ctx->statelim;
+ if (stlim != NULL) {
+ unsigned int gen;
+
+ pfl = malloc(sizeof(*pfl), M_PF_STATE_LINK, M_NOWAIT);
+ if (pfl == NULL) {
+ REASON_SET(&ctx->reason, PFRES_MEMORY);
+ goto csfailed;
+ }
+
+ gen = pf_statelim_enter(stlim);
+ stlim->pfstlim_counters.admitted++;
+ stlim->pfstlim_inuse++;
+ pf_statelim_leave(stlim, gen);
+
+ stlim->pfstlim_rate_ts += stlim->pfstlim_rate_token;
+
+ s->statelim = stlim->pfstlim_id;
+ pfl->pfl_state = s;
+ pfl->pfl_type = PF_STATE_LINK_TYPE_STATELIM;
+
+ TAILQ_INSERT_TAIL(&stlim->pfstlim_states, pfl, pfl_link);
+ SLIST_INSERT_HEAD(&s->linkage, pfl, pfl_linkage);
+ }
+
+ srlim = ctx->sourcelim;
+ if (srlim != NULL) {
+ struct pf_source *sr = ctx->source;
+ unsigned int gen;
+
+ if (sr == NULL) {
+ sr = malloc(sizeof(*sr), M_PF_SOURCE_LIM, M_NOWAIT | M_ZERO);
+ if (sr == NULL) {
+ gen = pf_sourcelim_enter(srlim);
+ srlim->pfsrlim_counters.addrnomem++;
+ pf_sourcelim_leave(srlim, gen);
+ REASON_SET(&ctx->reason, PFRES_MEMORY);
+ goto csfailed;
+ }
+
+ sr->pfsr_parent = srlim;
+ pf_source_key(srlim, sr, ctx->pd->af, ctx->pd->src);
+ TAILQ_INIT(&sr->pfsr_states);
+
+ if (RB_INSERT(pf_source_tree, &srlim->pfsrlim_sources,
+ sr) != NULL) {
+ panic("%s: source pool %u (%p) "
+ "insert collision %p?!",
+ __func__, srlim->pfsrlim_id, srlim, sr);
+ }
+
+ if (RB_INSERT(pf_source_ioc_tree,
+ &srlim->pfsrlim_ioc_sources, sr) != NULL) {
+ panic("%s: source pool %u (%p) ioc "
+ "insert collision (%p)?!",
+ __func__, srlim->pfsrlim_id, srlim, sr);
+ }
+
+ sr->pfsr_empty_ts = time_uptime;
+ TAILQ_INSERT_TAIL(&pf_source_gc, sr, pfsr_empty_gc);
+
+ gen = pf_sourcelim_enter(srlim);
+ srlim->pfsrlim_nsources++;
+ srlim->pfsrlim_counters.addrallocs++;
+ pf_sourcelim_leave(srlim, gen);
+ } else {
+ MPASS(sr->pfsr_parent == srlim);
+ }
+
+ pfl = malloc(sizeof(*pfl), M_PF_STATE_LINK, M_NOWAIT);
+ if (pfl == NULL) {
+ REASON_SET(&ctx->reason, PFRES_MEMORY);
+ goto csfailed;
+ }
+
+ pf_source_used(sr);
+
+ sr->pfsr_counters.admitted++;
+
+ gen = pf_sourcelim_enter(srlim);
+ srlim->pfsrlim_counters.inuse++;
+ srlim->pfsrlim_counters.admitted++;
+ pf_sourcelim_leave(srlim, gen);
+
+ s->sourcelim = srlim->pfsrlim_id;
+ pfl->pfl_state = s;
+ pfl->pfl_type = PF_STATE_LINK_TYPE_SOURCELIM;
+
+ TAILQ_INSERT_TAIL(&sr->pfsr_states, pfl, pfl_link);
+ SLIST_INSERT_HEAD(&s->linkage, pfl, pfl_linkage);
+ }
+
/* Swap sk/nk for PF_OUT. */
if (pf_state_insert(BOUND_IFACE(s, pd), pd->kif,
(pd->dir == PF_IN) ? ctx->sk : ctx->nk,
@@ -6400,6 +6977,44 @@ csfailed:
drop:
if (s != NULL) {
+ struct pf_state_link *npfl;
+
+ SLIST_FOREACH_SAFE(pfl, &s->linkage, pfl_linkage, npfl) {
+ struct pf_state_link_list *list;
+ unsigned int gen;
+
+ /* who needs KASSERTS when we have NULL derefs */
+
+ switch (pfl->pfl_type) {
+ case PF_STATE_LINK_TYPE_STATELIM:
+ gen = pf_statelim_enter(stlim);
+ stlim->pfstlim_inuse--;
+ pf_statelim_leave(stlim, gen);
+
+ stlim->pfstlim_rate_ts -=
+ stlim->pfstlim_rate_token;
+ list = &stlim->pfstlim_states;
+ break;
+ case PF_STATE_LINK_TYPE_SOURCELIM:
+ gen = pf_sourcelim_enter(srlim);
+ srlim->pfsrlim_counters.inuse--;
+ pf_sourcelim_leave(srlim, gen);
+
+ sr->pfsr_rate_ts -= srlim->pfsrlim_rate_token;
+ pf_source_rele(sr);
+
+ list = &sr->pfsr_states;
+ break;
+ default:
+ panic("%s: unexpected link type on pfl %p",
+ __func__, pfl);
+ }
+
+ TAILQ_REMOVE(list, pfl, pfl_link);
+ PF_STATE_LOCK_ASSERT(s);
+ free(pfl, M_PF_STATE_LINK);
+ }
+
pf_src_tree_remove_state(s);
s->timeout = PFTM_UNLINKED;
pf_free_state(s);
@@ -7164,6 +7779,38 @@ pf_synproxy(struct pf_pdesc *pd, struct pf_kstate *state, u_short *reason)
return (PF_PASS);
}
+static __inline int
+pf_synproxy_ack(struct pf_krule *r, struct pf_pdesc *pd, struct pf_kstate **sm,
+ struct pf_rule_actions *act)
+{
+ struct tcphdr *th = &pd->hdr.tcp;
+ struct pf_kstate *s;
+ u_int16_t mss;
+ int rtid;
+ u_short reason;
+
+ if ((th->th_flags & (TH_SYN | TH_ACK)) != TH_SYN)
+ return (PF_PASS);
+
+ s = *sm;
+ rtid = act->rtableid;
+
+ pf_set_protostate(s, PF_PEER_SRC, PF_TCPS_PROXY_SRC);
+ s->src.seqhi = arc4random();
+ /* Find mss option */
+ mss = pf_get_mss(pd);
+ mss = pf_calc_mss(pd->src, pd->af, rtid, mss);
+ mss = pf_calc_mss(pd->dst, pd->af, rtid, mss);
+ s->src.mss = mss;
+
+ pf_send_tcp(r, pd->af, pd->dst, pd->src, th->th_dport,
+ th->th_sport, s->src.seqhi, ntohl(th->th_seq) + 1,
+ TH_SYN | TH_ACK, 0, s->src.mss, 0, 1, 0, 0, r->rtableid, NULL);
+
+ REASON_SET(&reason, PFRES_SYNPROXY);
+ return (PF_SYNPROXY_DROP);
+}
+
static int
pf_test_state(struct pf_kstate **state, struct pf_pdesc *pd, u_short *reason)
{
diff --git a/sys/netpfil/pf/pf_ioctl.c b/sys/netpfil/pf/pf_ioctl.c
index ca1815984797..0bc8b30181d6 100644
--- a/sys/netpfil/pf/pf_ioctl.c
+++ b/sys/netpfil/pf/pf_ioctl.c
@@ -136,6 +136,12 @@ static int pf_import_kaltq(struct pfioc_altq_v1 *,
struct pf_altq *, size_t);
#endif /* ALTQ */
+static void pf_statelim_commit(void);
+static void pf_statelim_rollback(void);
+static int pf_sourcelim_check(void);
+static void pf_sourcelim_commit(void);
+static void pf_sourcelim_rollback(void);
+
VNET_DEFINE(struct pf_krule, pf_default_rule);
static __inline int pf_krule_compare(struct pf_krule *,
@@ -187,6 +193,7 @@ VNET_DEFINE(uma_zone_t, pf_tag_z);
static MALLOC_DEFINE(M_PFALTQ, "pf_altq", "pf(4) altq configuration db");
static MALLOC_DEFINE(M_PFRULE, "pf_rule", "pf(4) rules");
MALLOC_DEFINE(M_PF, "pf", "pf(4)");
+MALLOC_DEFINE(M_PF_STATE_LIM, "pf_state_lim", "pf(4) state limiter");
#if (PF_QNAME_SIZE != PF_TAG_NAME_SIZE)
#error PF_QNAME_SIZE must be equal to PF_TAG_NAME_SIZE
@@ -1318,6 +1325,12 @@ pf_rollback_rules(u_int32_t ticket, int rs_num, char *anchor)
rs->rules[rs_num].inactive.rcount--;
}
rs->rules[rs_num].inactive.open = 0;
+
+ if (anchor[0])
+ return (0);
+
+ pf_statelim_rollback();
+ pf_sourcelim_rollback();
return (0);
}
@@ -1437,6 +1450,7 @@ pf_commit_rules(u_int32_t ticket, int rs_num, char *anchor)
struct pf_krule_global *old_tree;
int error;
u_int32_t old_rcount;
+ bool is_main_ruleset = anchor[0] == '\0';
PF_RULES_WASSERT();
@@ -1449,6 +1463,9 @@ pf_commit_rules(u_int32_t ticket, int rs_num, char *anchor)
/* Calculate checksum for the main ruleset */
if (rs == &pf_main_ruleset) {
+ error = pf_sourcelim_check();
+ if (error != 0)
+ return (error);
error = pf_setup_pfsync_matching(rs);
if (error != 0)
return (error);
@@ -1507,6 +1524,13 @@ pf_commit_rules(u_int32_t ticket, int rs_num, char *anchor)
pf_remove_if_empty_kruleset(rs);
pf_rule_tree_free(old_tree);
+ /* statelim/sourcelim/queue defs only in the main ruleset */
+ if (! is_main_ruleset || rs_num != PF_RULESET_FILTER)
+ return (0);
+
+ pf_statelim_commit();
+ pf_sourcelim_commit();
+
return (0);
}
@@ -1589,6 +1613,844 @@ pf_addr_copyout(struct pf_addr_wrap *addr)
}
}
+static int
+pf_statelim_add(const struct pfioc_statelim *ioc)
+{
+ struct pf_statelim *pfstlim;
+ int error;
+ size_t namelen;
+
+ if (ioc->id < PF_STATELIM_ID_MIN ||
+ ioc->id > PF_STATELIM_ID_MAX)
+ return (EINVAL);
+
+ if (ioc->limit < PF_STATELIM_LIMIT_MIN ||
+ ioc->limit > PF_STATELIM_LIMIT_MAX)
+ return (EINVAL);
+
+ if ((ioc->rate.limit == 0) != (ioc->rate.seconds == 0))
+ return (EINVAL);
+
+ namelen = strnlen(ioc->name, sizeof(ioc->name));
+ if (namelen == sizeof(ioc->name))
+ return (EINVAL);
+
+ pfstlim = malloc(sizeof(*pfstlim), M_PF_STATE_LIM, M_WAITOK | M_ZERO);
+ if (pfstlim == NULL)
+ return (ENOMEM);
+
+ pfstlim->pfstlim_id = ioc->id;
+ memcpy(pfstlim->pfstlim_nm, ioc->name, namelen);
+ pfstlim->pfstlim_limit = ioc->limit;
+ pfstlim->pfstlim_rate.limit = ioc->rate.limit;
+ pfstlim->pfstlim_rate.seconds = ioc->rate.seconds;
+
+ if (pfstlim->pfstlim_rate.limit) {
+ uint64_t bucket = SEC_TO_NSEC(pfstlim->pfstlim_rate.seconds);
+ struct timespec ts;
+
+ getnanouptime(&ts);
+
+ pfstlim->pfstlim_rate_ts = SEC_TO_NSEC(ts.tv_sec) + ts.tv_nsec -
+ bucket;
+ pfstlim->pfstlim_rate_token = bucket /
+ pfstlim->pfstlim_rate.limit;
+ pfstlim->pfstlim_rate_bucket = bucket;
+ }
+
+ TAILQ_INIT(&pfstlim->pfstlim_states);
+ mtx_init(&pfstlim->pfstlim_lock, "pf state limit", NULL, MTX_DEF);
+
+ PF_RULES_WLOCK();
+ if (ioc->ticket != pf_main_ruleset.rules[PF_RULESET_FILTER].inactive.ticket) {
+ error = EBUSY;
+ goto unlock;
+ }
+
+ if (RB_INSERT(pf_statelim_id_tree, &V_pf_statelim_id_tree_inactive,
+ pfstlim) != NULL) {
+ error = EBUSY;
+ goto unlock;
+ }
+
+ if (RB_INSERT(pf_statelim_nm_tree, &V_pf_statelim_nm_tree_inactive,
+ pfstlim) != NULL) {
+ RB_REMOVE(pf_statelim_id_tree, &V_pf_statelim_id_tree_inactive,
+ pfstlim);
+ error = EBUSY;
+ goto unlock;
+ }
+
+ TAILQ_INSERT_HEAD(&V_pf_statelim_list_inactive, pfstlim, pfstlim_list);
+
+ PF_RULES_WUNLOCK();
+
+ return (0);
+
+unlock:
+ PF_RULES_WUNLOCK();
+
+ /* free: */
+ free(pfstlim, M_PF_STATE_LIM);
+
+ return (error);
+}
+
+static void
+pf_statelim_unlink(struct pf_statelim *pfstlim,
+ struct pf_state_link_list *garbage)
+{
+ struct pf_state_link *pfl;
+
+
+ /* unwire the links */
+ TAILQ_FOREACH(pfl, &pfstlim->pfstlim_states, pfl_link) {
+ struct pf_kstate *s = pfl->pfl_state;
+
+ /* if !rmst */
+ PF_STATE_LOCK(s);
+ s->statelim = 0;
+ SLIST_REMOVE(&s->linkage, pfl, pf_state_link, pfl_linkage);
+ PF_STATE_UNLOCK(s);
+ }
+
+ /* take the list away */
+ TAILQ_CONCAT(garbage, &pfstlim->pfstlim_states, pfl_link);
+ pfstlim->pfstlim_inuse = 0;
+}
+
+void
+pf_statelim_commit(void)
+{
+ struct pf_statelim *pfstlim, *npfstlim, *opfstlim;
+ struct pf_statelim_list l = TAILQ_HEAD_INITIALIZER(l);
+ struct pf_state_link_list garbage = TAILQ_HEAD_INITIALIZER(garbage);
+ struct pf_state_link *pfl, *npfl;
+
+ PF_RULES_WASSERT();
+
+ /* merge the new statelims into the current set */
+
+ /* start with an empty active list */
+ TAILQ_CONCAT(&l, &V_pf_statelim_list_active, pfstlim_list);
+
+ /* beware, the inactive bits gets messed up here */
+
+ /* try putting pending statelims into the active tree */
+ TAILQ_FOREACH_SAFE(pfstlim, &V_pf_statelim_list_inactive, pfstlim_list,
+ npfstlim) {
+ opfstlim = RB_INSERT(pf_statelim_id_tree,
+ &V_pf_statelim_id_tree_active, pfstlim);
+ if (opfstlim != NULL) {
+ /* this statelim already exists, merge */
+ opfstlim->pfstlim_limit = pfstlim->pfstlim_limit;
+ opfstlim->pfstlim_rate.limit =
+ pfstlim->pfstlim_rate.limit;
+ opfstlim->pfstlim_rate.seconds =
+ pfstlim->pfstlim_rate.seconds;
+
+ opfstlim->pfstlim_rate_ts = pfstlim->pfstlim_rate_ts;
+ opfstlim->pfstlim_rate_token =
+ pfstlim->pfstlim_rate_token;
+ opfstlim->pfstlim_rate_bucket =
+ pfstlim->pfstlim_rate_bucket;
+
+ memcpy(opfstlim->pfstlim_nm, pfstlim->pfstlim_nm,
+ sizeof(opfstlim->pfstlim_nm));
+
+ /* use the existing statelim instead */
+ free(pfstlim, M_PF_STATE_LIM);
+ TAILQ_REMOVE(&l, opfstlim, pfstlim_list);
+ pfstlim = opfstlim;
+ }
+
+ TAILQ_INSERT_TAIL(&V_pf_statelim_list_active, pfstlim,
+ pfstlim_list);
+ }
+
+ /* clean up the now unused statelims from the old set */
+ TAILQ_FOREACH_SAFE(pfstlim, &l, pfstlim_list, npfstlim) {
+ pf_statelim_unlink(pfstlim, &garbage);
+
+ RB_REMOVE(pf_statelim_id_tree, &V_pf_statelim_id_tree_active,
+ pfstlim);
+
+ free(pfstlim, M_PF_STATE_LIM);
+ }
+
+ /* fix up the inactive tree */
+ RB_INIT(&V_pf_statelim_id_tree_inactive);
+ RB_INIT(&V_pf_statelim_nm_tree_inactive);
+ TAILQ_INIT(&V_pf_statelim_list_inactive);
+
+ TAILQ_FOREACH_SAFE(pfl, &garbage, pfl_link, npfl)
+ free(pfl, M_PF_STATE_LINK);
+}
+
+static void
+pf_sourcelim_unlink(struct pf_sourcelim *pfsrlim,
+ struct pf_state_link_list *garbage)
+{
+ extern struct pf_source_list pf_source_gc;
+ struct pf_source *pfsr;
+ struct pf_state_link *pfl;
+
+ PF_RULES_WASSERT();
+
+ while ((pfsr = RB_ROOT(&pfsrlim->pfsrlim_sources)) != NULL) {
+ RB_REMOVE(pf_source_tree, &pfsrlim->pfsrlim_sources, pfsr);
+ RB_REMOVE(pf_source_ioc_tree, &pfsrlim->pfsrlim_ioc_sources,
+ pfsr);
+ if (pfsr->pfsr_inuse == 0)
+ TAILQ_REMOVE(&pf_source_gc, pfsr, pfsr_empty_gc);
+
+ /* unwire the links */
+ TAILQ_FOREACH(pfl, &pfsr->pfsr_states, pfl_link) {
+ struct pf_kstate *s = pfl->pfl_state;
+
+ PF_STATE_LOCK(s);
+ /* if !rmst */
+ s->sourcelim = 0;
+ SLIST_REMOVE(&s->linkage, pfl, pf_state_link,
+ pfl_linkage);
+ PF_STATE_UNLOCK(s);
+ }
+
+ /* take the list away */
+ TAILQ_CONCAT(garbage, &pfsr->pfsr_states, pfl_link);
+
+ free(pfsr, M_PF_SOURCE_LIM);
+ }
+}
+
+int
+pf_sourcelim_check(void)
+{
+ struct pf_sourcelim *pfsrlim, *npfsrlim;
+
+ PF_RULES_WASSERT();
+
+ /* check if we can merge */
+
+ TAILQ_FOREACH(pfsrlim, &V_pf_sourcelim_list_inactive, pfsrlim_list) {
+ npfsrlim = RB_FIND(pf_sourcelim_id_tree,
+ &V_pf_sourcelim_id_tree_active, pfsrlim);
+
+ /* new config, no conflict */
+ if (npfsrlim == NULL)
+ continue;
+
+ /* nothing is tracked at the moment, no conflict */
+ if (RB_EMPTY(&npfsrlim->pfsrlim_sources))
+ continue;
+
+ if (strcmp(npfsrlim->pfsrlim_overload.name,
+ pfsrlim->pfsrlim_overload.name) != 0)
+ return (EBUSY);
+
+ /*
+ * we should allow the prefixlens to get shorter
+ * and merge pf_source entries.
+ */
+
+ if ((npfsrlim->pfsrlim_ipv4_prefix !=
+ pfsrlim->pfsrlim_ipv4_prefix) ||
+ (npfsrlim->pfsrlim_ipv6_prefix !=
+ pfsrlim->pfsrlim_ipv6_prefix))
+ return (EBUSY);
+ }
+
+ return (0);
+}
+
+void
+pf_sourcelim_commit(void)
+{
+ struct pf_sourcelim *pfsrlim, *npfsrlim, *opfsrlim;
+ struct pf_sourcelim_list l = TAILQ_HEAD_INITIALIZER(l);
+ struct pf_state_link_list garbage = TAILQ_HEAD_INITIALIZER(garbage);
+ struct pf_state_link *pfl, *npfl;
+
+ PF_RULES_WASSERT();
+
+ /* merge the new sourcelims into the current set */
+
+ /* start with an empty active list */
+ TAILQ_CONCAT(&l, &V_pf_sourcelim_list_active, pfsrlim_list);
+
+ /* beware, the inactive bits gets messed up here */
+
+ /* try putting pending sourcelims into the active tree */
+ TAILQ_FOREACH_SAFE(pfsrlim, &V_pf_sourcelim_list_inactive, pfsrlim_list,
+ npfsrlim) {
+ opfsrlim = RB_INSERT(pf_sourcelim_id_tree,
+ &V_pf_sourcelim_id_tree_active, pfsrlim);
+ if (opfsrlim != NULL) {
+ /* this sourcelim already exists, merge */
+ opfsrlim->pfsrlim_entries = pfsrlim->pfsrlim_entries;
+ opfsrlim->pfsrlim_limit = pfsrlim->pfsrlim_limit;
+ opfsrlim->pfsrlim_ipv4_prefix =
+ pfsrlim->pfsrlim_ipv4_prefix;
+ opfsrlim->pfsrlim_ipv6_prefix =
+ pfsrlim->pfsrlim_ipv6_prefix;
+ opfsrlim->pfsrlim_rate.limit =
+ pfsrlim->pfsrlim_rate.limit;
+ opfsrlim->pfsrlim_rate.seconds =
+ pfsrlim->pfsrlim_rate.seconds;
+
+ opfsrlim->pfsrlim_ipv4_mask =
+ pfsrlim->pfsrlim_ipv4_mask;
+ opfsrlim->pfsrlim_ipv6_mask =
+ pfsrlim->pfsrlim_ipv6_mask;
+
+ /* keep the existing pfstlim_rate_ts */
+
+ opfsrlim->pfsrlim_rate_token =
+ pfsrlim->pfsrlim_rate_token;
+ opfsrlim->pfsrlim_rate_bucket =
+ pfsrlim->pfsrlim_rate_bucket;
+
+ if (opfsrlim->pfsrlim_overload.table != NULL) {
+ pfr_detach_table(
+ opfsrlim->pfsrlim_overload.table);
+ }
+
+ strlcpy(opfsrlim->pfsrlim_overload.name,
+ pfsrlim->pfsrlim_overload.name,
+ sizeof(opfsrlim->pfsrlim_overload.name));
+ opfsrlim->pfsrlim_overload.hwm =
+ pfsrlim->pfsrlim_overload.hwm;
+ opfsrlim->pfsrlim_overload.lwm =
+ pfsrlim->pfsrlim_overload.lwm;
+ opfsrlim->pfsrlim_overload.table =
+ pfsrlim->pfsrlim_overload.table,
+
+ memcpy(opfsrlim->pfsrlim_nm, pfsrlim->pfsrlim_nm,
+ sizeof(opfsrlim->pfsrlim_nm));
+
+ /* use the existing sourcelim instead */
+ free(pfsrlim, M_PF_SOURCE_LIM);
+ TAILQ_REMOVE(&l, opfsrlim, pfsrlim_list);
+ pfsrlim = opfsrlim;
+ }
+
+ TAILQ_INSERT_TAIL(&V_pf_sourcelim_list_active, pfsrlim,
+ pfsrlim_list);
+ }
+
+ /* clean up the now unused sourcelims from the old set */
+ TAILQ_FOREACH_SAFE(pfsrlim, &l, pfsrlim_list, npfsrlim) {
+ pf_sourcelim_unlink(pfsrlim, &garbage);
+
+ RB_REMOVE(pf_sourcelim_id_tree, &V_pf_sourcelim_id_tree_active,
+ pfsrlim);
+
+ if (pfsrlim->pfsrlim_overload.table != NULL)
+ pfr_detach_table(pfsrlim->pfsrlim_overload.table);
+
+ free(pfsrlim, M_PF_SOURCE_LIM);
+ }
+
+ /* fix up the inactive tree */
+ RB_INIT(&V_pf_sourcelim_id_tree_inactive);
+ RB_INIT(&V_pf_sourcelim_nm_tree_inactive);
+ TAILQ_INIT(&V_pf_sourcelim_list_inactive);
+
+ TAILQ_FOREACH_SAFE(pfl, &garbage, pfl_link, npfl)
+ free(pfl, M_PF_STATE_LINK);
+}
+
+void
+pf_statelim_rollback(void)
+{
+ struct pf_statelim *pfstlim, *npfstlim;
+
+ PF_RULES_WASSERT();
+
+ TAILQ_FOREACH_SAFE(pfstlim, &V_pf_statelim_list_inactive, pfstlim_list,
+ npfstlim)
+ free(pfstlim, M_PF_STATE_LIM);
+
+ TAILQ_INIT(&V_pf_statelim_list_inactive);
+ RB_INIT(&V_pf_statelim_id_tree_inactive);
+ RB_INIT(&V_pf_statelim_nm_tree_inactive);
+}
+
+static struct pf_statelim *
+pf_statelim_rb_find(struct pf_statelim_id_tree *tree, struct pf_statelim *key)
+{
+ PF_RULES_ASSERT();
+
+ return (RB_FIND(pf_statelim_id_tree, tree, key));
+}
+
+static struct pf_statelim *
+pf_statelim_rb_nfind(struct pf_statelim_id_tree *tree, struct pf_statelim *key)
+{
+ PF_RULES_ASSERT();
+
+ return (RB_NFIND(pf_statelim_id_tree, tree, key));
+}
+
+static int
+pf_statelim_get(struct pfioc_statelim *ioc,
+ struct pf_statelim *(*rbt_op)(struct pf_statelim_id_tree *,
+ struct pf_statelim *))
+{
+ struct pf_statelim key = { .pfstlim_id = ioc->id };
+ struct pf_statelim *pfstlim;
+ int error = 0;
+ PF_RULES_RLOCK_TRACKER;
+
+ PF_RULES_RLOCK();
+
+ pfstlim = (*rbt_op)(&V_pf_statelim_id_tree_active, &key);
+ if (pfstlim == NULL) {
+ error = ENOENT;
+ goto unlock;
+ }
+
+ ioc->id = pfstlim->pfstlim_id;
+ ioc->limit = pfstlim->pfstlim_limit;
+ ioc->rate.limit = pfstlim->pfstlim_rate.limit;
+ ioc->rate.seconds = pfstlim->pfstlim_rate.seconds;
+ CTASSERT(sizeof(ioc->name) == sizeof(pfstlim->pfstlim_nm));
+ memcpy(ioc->name, pfstlim->pfstlim_nm, sizeof(ioc->name));
+
+ ioc->inuse = pfstlim->pfstlim_inuse;
+ ioc->admitted = pfstlim->pfstlim_counters.admitted;
+ ioc->hardlimited = pfstlim->pfstlim_counters.hardlimited;
+ ioc->ratelimited = pfstlim->pfstlim_counters.ratelimited;
+
+unlock:
+ PF_RULES_RUNLOCK();
+
+ return (error);
+}
+
+static int
+pf_sourcelim_add(const struct pfioc_sourcelim *ioc)
+{
+ struct pf_sourcelim *pfsrlim;
+ int error;
+ size_t namelen, tablelen;
+ unsigned int prefix;
+ size_t i;
+
+ if (ioc->id < PF_SOURCELIM_ID_MIN ||
+ ioc->id > PF_SOURCELIM_ID_MAX)
+ return (EINVAL);
+
+ if (ioc->entries < 1)
+ return (EINVAL);
+
+ if (ioc->limit < 1)
+ return (EINVAL);
+
+ if ((ioc->rate.limit == 0) != (ioc->rate.seconds == 0))
+ return (EINVAL);
+
+ if (ioc->inet_prefix > 32)
+ return (EINVAL);
+ if (ioc->inet6_prefix > 128)
+ return (EINVAL);
+
+ namelen = strnlen(ioc->name, sizeof(ioc->name));
+ if (namelen == sizeof(ioc->name))
+ return (EINVAL);
+
+ tablelen = strnlen(ioc->overload_tblname,
+ sizeof(ioc->overload_tblname));
+ if (tablelen == sizeof(ioc->overload_tblname))
+ return (EINVAL);
+ if (tablelen != 0) {
+ if (ioc->overload_hwm == 0)
+ return (EINVAL);
+
+ /*
+ * this is stupid, but not harmful?
+ *
+ * if (ioc->states < ioc->overload_hwm)
+ * return (EINVAL);
+ */
+
+ if (ioc->overload_hwm < ioc->overload_lwm)
+ return (EINVAL);
+ }
+
+ pfsrlim = malloc(sizeof(*pfsrlim), M_PF_SOURCE_LIM, M_WAITOK | M_ZERO);
+ if (pfsrlim == NULL)
+ return (ENOMEM);
+
+ pfsrlim->pfsrlim_id = ioc->id;
+ pfsrlim->pfsrlim_entries = ioc->entries;
+ pfsrlim->pfsrlim_limit = ioc->limit;
+ pfsrlim->pfsrlim_ipv4_prefix = ioc->inet_prefix;
+ pfsrlim->pfsrlim_ipv6_prefix = ioc->inet6_prefix;
+ pfsrlim->pfsrlim_rate.limit = ioc->rate.limit;
+ pfsrlim->pfsrlim_rate.seconds = ioc->rate.seconds;
+ memcpy(pfsrlim->pfsrlim_overload.name, ioc->overload_tblname, tablelen);
+ pfsrlim->pfsrlim_overload.hwm = ioc->overload_hwm;
+ pfsrlim->pfsrlim_overload.lwm = ioc->overload_lwm;
+ memcpy(pfsrlim->pfsrlim_nm, ioc->name, namelen);
+
+ if (pfsrlim->pfsrlim_rate.limit) {
+ uint64_t bucket = pfsrlim->pfsrlim_rate.seconds * 1000000000ULL;
+
+ pfsrlim->pfsrlim_rate_token = bucket /
+ pfsrlim->pfsrlim_rate.limit;
+ pfsrlim->pfsrlim_rate_bucket = bucket;
+ }
+
+ pfsrlim->pfsrlim_ipv4_mask.v4.s_addr = htonl(
+ 0xffffffff << (32 - pfsrlim->pfsrlim_ipv4_prefix));
+
+ prefix = pfsrlim->pfsrlim_ipv6_prefix;
+ for (i = 0; i < nitems(pfsrlim->pfsrlim_ipv6_mask.addr32); i++) {
+ if (prefix == 0) {
+ /* the memory is already zeroed */
+ break;
+ }
+ if (prefix < 32) {
+ pfsrlim->pfsrlim_ipv6_mask.addr32[i] = htonl(
+ 0xffffffff << (32 - prefix));
+ break;
+ }
+
+ pfsrlim->pfsrlim_ipv6_mask.addr32[i] = htonl(0xffffffff);
+ prefix -= 32;
+ }
+
+ RB_INIT(&pfsrlim->pfsrlim_sources);
+ mtx_init(&pfsrlim->pfsrlim_lock, "pf source limit", NULL, MTX_DEF);
+
+ PF_RULES_WLOCK();
+ if (ioc->ticket != pf_main_ruleset.rules[PF_RULESET_FILTER].inactive.ticket) {
+ error = EBUSY;
+ goto unlock;
+ }
+
+ if (pfsrlim->pfsrlim_overload.name[0] != '\0') {
+ pfsrlim->pfsrlim_overload.table = pfr_attach_table(
+ &pf_main_ruleset, pfsrlim->pfsrlim_overload.name);
+ if (pfsrlim->pfsrlim_overload.table == NULL) {
+ error = EINVAL;
+ goto unlock;
+ }
+ }
+
+ if (RB_INSERT(pf_sourcelim_id_tree, &V_pf_sourcelim_id_tree_inactive,
+ pfsrlim) != NULL) {
+ error = EBUSY;
+ goto unlock;
+ }
+
+ if (RB_INSERT(pf_sourcelim_nm_tree, &V_pf_sourcelim_nm_tree_inactive,
+ pfsrlim) != NULL) {
+ RB_INSERT(pf_sourcelim_nm_tree, &V_pf_sourcelim_nm_tree_inactive,
+ pfsrlim);
+ error = EBUSY;
+ goto unlock;
+ }
+
+ TAILQ_INSERT_HEAD(&V_pf_sourcelim_list_inactive, pfsrlim, pfsrlim_list);
+
+ PF_RULES_WUNLOCK();
+
+ return (0);
+
+unlock:
+ PF_RULES_WUNLOCK();
+ /* free: */
+ free(pfsrlim, M_PF_SOURCE_LIM);
+
+ return (error);
+}
+
+void
+pf_sourcelim_rollback(void)
+{
+ struct pf_sourcelim *pfsrlim, *npfsrlim;
+
+ PF_RULES_WASSERT();
+
+ TAILQ_FOREACH_SAFE(pfsrlim, &V_pf_sourcelim_list_inactive, pfsrlim_list,
+ npfsrlim) {
+ if (pfsrlim->pfsrlim_overload.table != NULL)
+ pfr_detach_table(pfsrlim->pfsrlim_overload.table);
+
+ free(pfsrlim, M_PF_SOURCE_LIM);
+ }
+
+ TAILQ_INIT(&V_pf_sourcelim_list_inactive);
+ RB_INIT(&V_pf_sourcelim_id_tree_inactive);
+ RB_INIT(&V_pf_sourcelim_nm_tree_inactive);
+}
+
+static struct pf_sourcelim *
+pf_sourcelim_rb_find(struct pf_sourcelim_id_tree *tree,
+ struct pf_sourcelim *key)
+{
+ PF_RULES_ASSERT();
+ return (RB_FIND(pf_sourcelim_id_tree, tree, key));
+}
+
+static struct pf_sourcelim *
+pf_sourcelim_rb_nfind(struct pf_sourcelim_id_tree *tree,
+ struct pf_sourcelim *key)
+{
+ PF_RULES_ASSERT();
+ return (RB_NFIND(pf_sourcelim_id_tree, tree, key));
+}
+
+static int
+pf_sourcelim_get(struct pfioc_sourcelim *ioc,
+ struct pf_sourcelim *(*rbt_op)(struct pf_sourcelim_id_tree *,
+ struct pf_sourcelim *))
+{
+ struct pf_sourcelim key = { .pfsrlim_id = ioc->id };
+ struct pf_sourcelim *pfsrlim;
+ int error = 0;
+ PF_RULES_RLOCK_TRACKER;
+
+ PF_RULES_RLOCK();
+#if 0
+ if (ioc->ticket != pf_main_ruleset.rules.active.ticket) {
+ error = EBUSY;
+ goto unlock;
+ }
+#endif
+
+ pfsrlim = (*rbt_op)(&V_pf_sourcelim_id_tree_active, &key);
+ if (pfsrlim == NULL) {
+ error = ESRCH;
+ goto unlock;
+ }
+
+ ioc->id = pfsrlim->pfsrlim_id;
+ ioc->entries = pfsrlim->pfsrlim_entries;
+ ioc->limit = pfsrlim->pfsrlim_limit;
+ ioc->inet_prefix = pfsrlim->pfsrlim_ipv4_prefix;
+ ioc->inet6_prefix = pfsrlim->pfsrlim_ipv6_prefix;
+ ioc->rate.limit = pfsrlim->pfsrlim_rate.limit;
+ ioc->rate.seconds = pfsrlim->pfsrlim_rate.seconds;
+
+ CTASSERT(sizeof(ioc->overload_tblname) ==
+ sizeof(pfsrlim->pfsrlim_overload.name));
+ memcpy(ioc->overload_tblname, pfsrlim->pfsrlim_overload.name,
+ sizeof(pfsrlim->pfsrlim_overload.name));
+ ioc->overload_hwm = pfsrlim->pfsrlim_overload.hwm;
+ ioc->overload_lwm = pfsrlim->pfsrlim_overload.lwm;
+
+ CTASSERT(sizeof(ioc->name) == sizeof(pfsrlim->pfsrlim_nm));
+ memcpy(ioc->name, pfsrlim->pfsrlim_nm, sizeof(ioc->name));
+ /* XXX overload table thing */
+
+ ioc->nentries = pfsrlim->pfsrlim_nsources;
+
+ ioc->inuse = pfsrlim->pfsrlim_counters.inuse;
+ ioc->addrallocs = pfsrlim->pfsrlim_counters.addrallocs;
+ ioc->addrnomem = pfsrlim->pfsrlim_counters.addrnomem;
+ ioc->admitted = pfsrlim->pfsrlim_counters.admitted;
+ ioc->addrlimited = pfsrlim->pfsrlim_counters.addrlimited;
+ ioc->hardlimited = pfsrlim->pfsrlim_counters.hardlimited;
+ ioc->ratelimited = pfsrlim->pfsrlim_counters.ratelimited;
+
+unlock:
+ PF_RULES_RUNLOCK();
+
+ return (error);
+}
+
+static struct pf_source *
+pf_source_rb_find(struct pf_source_ioc_tree *tree,
+ struct pf_source *key)
+{
+ PF_RULES_ASSERT();
+
+ return (RB_FIND(pf_source_ioc_tree, tree, key));
+}
+
+static struct pf_source *
+pf_source_rb_nfind(struct pf_source_ioc_tree *tree,
+ struct pf_source *key)
+{
+ PF_RULES_ASSERT();
+
+ return (RB_NFIND(pf_source_ioc_tree, tree, key));
+}
+
+static int
+pf_source_get(struct pfioc_source *ioc,
+ struct pf_source *(*rbt_op)(struct pf_source_ioc_tree *,
+ struct pf_source *))
+{
+ struct pf_sourcelim plkey = { .pfsrlim_id = ioc->id };
+ struct pfioc_source_entry e, *uentry;
+ struct pf_source key;
+ struct pf_sourcelim *pfsrlim;
+ struct pf_source *pfsr;
+ size_t used = 0, len = ioc->entrieslen;
+ int error = 0;
+ PF_RULES_RLOCK_TRACKER;
+
+ if (ioc->entry_size != sizeof(e))
+ return (EINVAL);
+ if (len < sizeof(e))
+ return (EMSGSIZE);
+
+ error = copyin(ioc->key, &e, sizeof(e));
+ if (error != 0)
+ return (error);
+
+ PF_RULES_RLOCK();
+
+#if 0
+ if (ioc->ticket != pf_main_ruleset.rules.active.ticket) {
+ error = EBUSY;
+ goto unlock;
+ }
+#endif
+
+ pfsrlim = pf_sourcelim_rb_find(&V_pf_sourcelim_id_tree_active, &plkey);
+ if (pfsrlim == NULL) {
+ error = ESRCH;
+ goto unlock;
+ }
+
+ key.pfsr_af = e.af;
+ key.pfsr_rdomain = e.rdomain;
+ key.pfsr_addr = e.addr;
+ pfsr = (*rbt_op)(&pfsrlim->pfsrlim_ioc_sources, &key);
+ if (pfsr == NULL) {
+ error = ENOENT;
+ goto unlock;
+ }
+
+ memset(&e, 0, sizeof(e));
+
+ uentry = ioc->entries;
+ for (;;) {
+ e.af = pfsr->pfsr_af;
+ e.rdomain = pfsr->pfsr_rdomain;
+ e.addr = pfsr->pfsr_addr;
+
+ e.inuse = pfsr->pfsr_inuse;
+ e.admitted = pfsr->pfsr_counters.admitted;
+ e.hardlimited = pfsr->pfsr_counters.hardlimited;
+ e.ratelimited = pfsr->pfsr_counters.ratelimited;
+
+ error = copyout(&e, uentry, sizeof(e));
+ if (error != 0)
+ goto unlock;
+
+ used += sizeof(e);
+ if (used == len)
+ break;
+
+ pfsr = RB_NEXT(pf_source_ioc_tree, srlim->pfsrlim_ioc_sources, pfsr);
+ if (pfsr == NULL)
+ break;
+
+ if ((len - used) < sizeof(e)) {
+ error = EMSGSIZE;
+ goto unlock;
+ }
+
+ uentry++;
+ }
+ MPASS(error == 0);
+
+ ioc->inet_prefix = pfsrlim->pfsrlim_ipv4_prefix;
+ ioc->inet6_prefix = pfsrlim->pfsrlim_ipv6_prefix;
+ ioc->limit = pfsrlim->pfsrlim_limit;
+
+ ioc->entrieslen = used;
+
+unlock:
+ PF_RULES_RUNLOCK();
+
+ return (error);
+}
+
+static int
+pf_source_clr(struct pfioc_source_kill *ioc)
+{
+ extern struct pf_source_list pf_source_gc;
+ struct pf_sourcelim plkey = {
+ .pfsrlim_id = ioc->id,
+ };
+ struct pf_source skey = {
+ .pfsr_af = ioc->af,
+ .pfsr_rdomain = ioc->rdomain,
+ .pfsr_addr = ioc->addr,
+ };
+ struct pf_sourcelim *pfsrlim;
+ struct pf_source *pfsr;
+ struct pf_state_link *pfl, *npfl;
+ int error = 0;
+ unsigned int gen;
+
+ if (ioc->rmstates) {
+ /* XXX userland wants the states removed too */
+ return (EOPNOTSUPP);
+ }
+
+ PF_RULES_WLOCK();
+
+#if 0
+ if (ioc->ticket != pf_main_ruleset.rules.active.ticket) {
+ error = EBUSY;
+ goto unlock;
+ }
+#endif
+
+ pfsrlim = pf_sourcelim_rb_find(&V_pf_sourcelim_id_tree_active, &plkey);
+ if (pfsrlim == NULL) {
+ error = ESRCH;
+ goto unlock;
+ }
+
+ pfsr = pf_source_rb_find(&pfsrlim->pfsrlim_ioc_sources, &skey);
+ if (pfsr == NULL) {
+ error = ENOENT;
+ goto unlock;
+ }
+
+ RB_REMOVE(pf_source_tree, &pfsrlim->pfsrlim_sources, pfsr);
+ RB_REMOVE(pf_source_ioc_tree, &pfsrlim->pfsrlim_ioc_sources, pfsr);
+ if (pfsr->pfsr_inuse == 0)
+ TAILQ_REMOVE(&pf_source_gc, pfsr, pfsr_empty_gc);
+
+ gen = pf_sourcelim_enter(pfsrlim);
+ pfsrlim->pfsrlim_nsources--;
+ pfsrlim->pfsrlim_counters.inuse -= pfsr->pfsr_inuse;
+ pf_sourcelim_leave(pfsrlim, gen);
+
+ /* unwire the links */
+ TAILQ_FOREACH(pfl, &pfsr->pfsr_states, pfl_link) {
+ struct pf_kstate *st = pfl->pfl_state;
+
+ /* if !rmst */
+ st->sourcelim = 0;
+ SLIST_REMOVE(&st->linkage, pfl, pf_state_link, pfl_linkage);
+ }
+
+ PF_RULES_WUNLOCK();
+
+ TAILQ_FOREACH_SAFE(pfl, &pfsr->pfsr_states, pfl_link, npfl)
+ free(pfl, M_PF_STATE_LINK);
+
+ free(pfsr, M_PF_SOURCE_LIM);
+
+ return (0);
+
+unlock:
+ PF_RULES_WUNLOCK();
+
+ return (error);
+}
+
static void
pf_src_node_copy(const struct pf_ksrc_node *in, struct pf_src_node *out)
{
@@ -2181,6 +3043,18 @@ pf_ioctl_addrule(struct pf_krule *rule, uint32_t ticket,
if (pf_validate_range(rule->dst.port_op, rule->dst.port))
ERROUT_UNLOCKED(EINVAL);
+ if (rule->statelim != PF_STATELIM_ID_NONE) {
+ if (rule->statelim < PF_STATELIM_ID_MIN ||
+ rule->statelim > PF_STATELIM_ID_MAX)
+ ERROUT_UNLOCKED(EINVAL);
+ }
+
+ if (rule->sourcelim != PF_SOURCELIM_ID_NONE) {
+ if (rule->sourcelim < PF_SOURCELIM_ID_MIN ||
+ rule->sourcelim > PF_SOURCELIM_ID_MAX)
+ ERROUT_UNLOCKED(EINVAL);
+ }
+
if (rule->ifname[0])
kif = pf_kkif_create(M_WAITOK);
if (rule->rcv_ifname[0])
@@ -3002,6 +3876,12 @@ pfioctl(struct cdev *dev, u_long cmd, caddr_t addr, int flags, struct thread *td
case DIOCGETTIMEOUT:
case DIOCCLRRULECTRS:
case DIOCGETLIMIT:
+ case DIOCGETSTATELIM:
+ case DIOCGETNSTATELIM:
+ case DIOCGETSOURCELIM:
+ case DIOCGETNSOURCELIM:
+ case DIOCGETSOURCE:
+ case DIOCGETNSOURCE:
case DIOCGETALTQSV0:
case DIOCGETALTQSV1:
case DIOCGETALTQV0:
@@ -3061,6 +3941,12 @@ pfioctl(struct cdev *dev, u_long cmd, caddr_t addr, int flags, struct thread *td
#endif
case DIOCGETTIMEOUT:
case DIOCGETLIMIT:
+ case DIOCGETSTATELIM:
+ case DIOCGETNSTATELIM:
+ case DIOCGETSOURCELIM:
+ case DIOCGETNSOURCELIM:
+ case DIOCGETSOURCE:
+ case DIOCGETNSOURCE:
case DIOCGETALTQSV0:
case DIOCGETALTQSV1:
case DIOCGETALTQV0:
@@ -4349,6 +5235,42 @@ DIOCGETSTATESV2_full:
break;
}
+ case DIOCADDSTATELIM:
+ error = pf_statelim_add((struct pfioc_statelim *)addr);
+ break;
+ case DIOCGETSTATELIM:
+ error = pf_statelim_get((struct pfioc_statelim *)addr,
+ pf_statelim_rb_find);
+ break;
+ case DIOCGETNSTATELIM:
+ error = pf_statelim_get((struct pfioc_statelim *)addr,
+ pf_statelim_rb_nfind);
+ break;
+
+ case DIOCADDSOURCELIM:
+ error = pf_sourcelim_add((struct pfioc_sourcelim *)addr);
+ break;
+ case DIOCGETSOURCELIM:
+ error = pf_sourcelim_get((struct pfioc_sourcelim *)addr,
+ pf_sourcelim_rb_find);
+ break;
+ case DIOCGETNSOURCELIM:
+ error = pf_sourcelim_get((struct pfioc_sourcelim *)addr,
+ pf_sourcelim_rb_nfind);
+ break;
+
+ case DIOCGETSOURCE:
+ error = pf_source_get((struct pfioc_source *)addr,
+ pf_source_rb_find);
+ break;
+ case DIOCGETNSOURCE:
+ error = pf_source_get((struct pfioc_source *)addr,
+ pf_source_rb_nfind);
+ break;
+ case DIOCCLRSOURCE:
+ error = pf_source_clr((struct pfioc_source_kill *)addr);
+ break;
+
case DIOCCLRRULECTRS: {
/* obsoleted by DIOCGETRULE with action=PF_GET_CLR_CNTR */
struct pf_kruleset *ruleset = &pf_main_ruleset;
diff --git a/sys/netpfil/pf/pf_nl.c b/sys/netpfil/pf/pf_nl.c
index 1c8a1f95b650..6e19beec5bfe 100644
--- a/sys/netpfil/pf/pf_nl.c
+++ b/sys/netpfil/pf/pf_nl.c
@@ -784,6 +784,8 @@ 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 },
};
NL_DECLARE_ATTR_PARSER(rule_parser, nla_p_rule);
#undef _OUT
@@ -1041,6 +1043,8 @@ 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);
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 216f3d13db32..d45766b91a30 100644
--- a/sys/netpfil/pf/pf_nl.h
+++ b/sys/netpfil/pf/pf_nl.h
@@ -290,6 +290,8 @@ 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 */
};
enum pf_addrule_type_t {
diff --git a/sys/netpfil/pf/pf_table.c b/sys/netpfil/pf/pf_table.c
index 0e2b9fe1cac8..650334c45db3 100644
--- a/sys/netpfil/pf/pf_table.c
+++ b/sys/netpfil/pf/pf_table.c
@@ -882,6 +882,26 @@ pfr_insert_kentry(struct pfr_ktable *kt, struct pfr_addr *ad, time_t tzero)
return (0);
}
+int
+pfr_remove_kentry(struct pfr_ktable *kt, struct pfr_addr *ad)
+{
+ struct pfr_kentryworkq workq = SLIST_HEAD_INITIALIZER(workq);
+ struct pfr_kentry *p;
+
+ p = pfr_lookup_addr(kt, ad, 1);
+ if (p == NULL || p->pfrke_not)
+ return (ESRCH);
+
+ if (p->pfrke_mark)
+ return (0);
+
+ p->pfrke_mark = 1;
+ SLIST_INSERT_HEAD(&workq, p, pfrke_workq);
+ pfr_remove_kentries(kt, &workq);
+
+ return (0);
+}
+
static void
pfr_remove_kentries(struct pfr_ktable *kt,
struct pfr_kentryworkq *workq)