Date: Thu, 13 Feb 2025 15:00:49 GMT From: Kajetan Staszkiewicz <ks@FreeBSD.org> To: src-committers@FreeBSD.org, dev-commits-src-all@FreeBSD.org, dev-commits-src-main@FreeBSD.org Subject: git: 07e070ef0869 - main - pf: Add support for multiple source node types Message-ID: <202502131500.51DF0nbT046085@gitrepo.freebsd.org>
next in thread | raw e-mail | index | archive | help
The branch main has been updated by ks: URL: https://cgit.FreeBSD.org/src/commit/?id=07e070ef086997590cd6d9d47908885c12947bd2 commit 07e070ef086997590cd6d9d47908885c12947bd2 Author: Kajetan Staszkiewicz <ks@FreeBSD.org> AuthorDate: 2025-02-07 11:40:09 +0000 Commit: Kajetan Staszkiewicz <ks@FreeBSD.org> CommitDate: 2025-02-13 14:59:12 +0000 pf: Add support for multiple source node types For every state pf creates up to two source nodes: a limiting one struct pf_kstate -> src_node and a NAT one struct pf_kstate -> nat_src_node. The limiting source node is tracking information needed for limits using max-src-states and max-src-nodes and the NAT source node is tracking NAT rules only. On closer inspection some issues emerge: - For route-to rules the redirection decision is stored in the limiting source node. Thus sticky-address and source limiting can't be used separately. - Global source tracking, as promised in the man page, is totally absent from the code. Pfctl is capable of setting flags PFRULE_SRCTRACK (enable source tracking) and PFRULE_RULESRCTRACK (make source tracking per rule). The kernel code checks PFRULE_SRCTRACK but ignores PFRULE_RULESRCTRACK. That makes source tracking work per-rule only. This patch is based on OpenBSD approach where source nodes have a type and each state has an array of source node pointers indexed by source node type instead of just two pointers. The conditions for limiting are applied only to source nodes of PF_SN_LIMIT type. For global limit tracking source nodes are attached to the default rule. Reviewed by: kp Approved by: kp (mentor) Sponsored by: InnoGames GmbH Differential Revision: https://reviews.freebsd.org/D39880 --- lib/libpfctl/libpfctl.c | 5 + lib/libpfctl/libpfctl.h | 3 + sbin/pfctl/pf_print_state.c | 13 ++- sbin/pfctl/pfctl.c | 9 ++ sbin/pfctl/pfctl_parser.c | 2 + sys/net/pfvar.h | 30 ++++-- sys/netpfil/pf/pf.c | 216 +++++++++++++++++++++----------------- sys/netpfil/pf/pf.h | 6 ++ sys/netpfil/pf/pf_ioctl.c | 40 ++++--- sys/netpfil/pf/pf_lb.c | 35 +++--- sys/netpfil/pf/pf_nl.c | 25 ++++- sys/netpfil/pf/pf_nl.h | 5 + sys/netpfil/pf/pf_nv.c | 10 +- tests/sys/netpfil/pf/src_track.sh | 155 ++++++++++++++++++++++++++- 14 files changed, 405 insertions(+), 149 deletions(-) diff --git a/lib/libpfctl/libpfctl.c b/lib/libpfctl/libpfctl.c index fe63c91c1174..e93c79758428 100644 --- a/lib/libpfctl/libpfctl.c +++ b/lib/libpfctl/libpfctl.c @@ -1665,6 +1665,9 @@ static struct snl_attr_parser ap_getrule[] = { { .type = PF_RT_NAF, .off = _OUT(r.naf), .cb = snl_attr_get_uint8 }, { .type = PF_RT_RPOOL_RT, .off = _OUT(r.route), .arg = &pool_parser, .cb = snl_attr_get_nested }, { .type = PF_RT_RCV_IFNOT, .off = _OUT(r.rcvifnot),.cb = snl_attr_get_bool }, + { .type = PF_RT_SRC_NODES_LIMIT, .off = _OUT(r.src_nodes_type[PF_SN_LIMIT]), .cb = snl_attr_get_uint64 }, + { .type = PF_RT_SRC_NODES_NAT, .off = _OUT(r.src_nodes_type[PF_SN_NAT]), .cb = snl_attr_get_uint64 }, + { .type = PF_RT_SRC_NODES_ROUTE, .off = _OUT(r.src_nodes_type[PF_SN_ROUTE]), .cb = snl_attr_get_uint64 }, }; #undef _OUT SNL_DECLARE_PARSER(getrule_parser, struct genlmsghdr, snl_f_p_empty, ap_getrule); @@ -1910,6 +1913,7 @@ static struct snl_attr_parser ap_state[] = { { .type = PF_ST_DNRPIPE, .off = _OUT(dnrpipe), .cb = snl_attr_get_uint16 }, { .type = PF_ST_RT, .off = _OUT(rt), .cb = snl_attr_get_uint8 }, { .type = PF_ST_RT_IFNAME, .off = _OUT(rt_ifname), .cb = snl_attr_store_ifname }, + { .type = PF_ST_SRC_NODE_FLAGS, .off = _OUT(src_node_flags), .cb = snl_attr_get_uint8 }, }; #undef _IN #undef _OUT @@ -3018,6 +3022,7 @@ static struct snl_attr_parser ap_srcnode[] = { { .type = PF_SN_EXPIRE, .off = _OUT(expire), .cb = snl_attr_get_uint64 }, { .type = PF_SN_CONNECTION_RATE, .off = _OUT(conn_rate), .arg = &pfctl_threshold_parser, .cb = snl_attr_get_nested }, { .type = PF_SN_NAF, .off = _OUT(naf), .cb = snl_attr_get_uint8 }, + { .type = PF_SN_NODE_TYPE, .off = _OUT(type), .cb = snl_attr_get_uint8 }, }; #undef _OUT SNL_DECLARE_PARSER(srcnode_parser, struct genlmsghdr, snl_f_p_empty, ap_srcnode); diff --git a/lib/libpfctl/libpfctl.h b/lib/libpfctl/libpfctl.h index e1af4b5e97ff..1108b0ffc693 100644 --- a/lib/libpfctl/libpfctl.h +++ b/lib/libpfctl/libpfctl.h @@ -216,6 +216,7 @@ struct pfctl_rule { uint64_t states_cur; uint64_t states_tot; uint64_t src_nodes; + uint64_t src_nodes_type[PF_SN_MAX]; uint16_t return_icmp; uint16_t return_icmp6; @@ -373,6 +374,7 @@ struct pfctl_state { uint8_t set_prio[2]; uint8_t rt; char rt_ifname[IFNAMSIZ]; + uint8_t src_node_flags; }; TAILQ_HEAD(pfctl_statelist, pfctl_state); @@ -415,6 +417,7 @@ struct pfctl_src_node { uint64_t creation; uint64_t expire; struct pfctl_threshold conn_rate; + pf_sn_types_t type; }; #define PF_DEVICE "/dev/pf" diff --git a/sbin/pfctl/pf_print_state.c b/sbin/pfctl/pf_print_state.c index e6495dfa4ca6..1d2fa45cd9d7 100644 --- a/sbin/pfctl/pf_print_state.c +++ b/sbin/pfctl/pf_print_state.c @@ -245,6 +245,7 @@ print_state(struct pfctl_state *s, int opts) uint8_t proto; int afto = (s->key[PF_SK_STACK].af != s->key[PF_SK_WIRE].af); int idx; + const char *sn_type_names[] = PF_SN_TYPE_NAMES; #ifndef __NO_STRICT_ALIGNMENT struct pfctl_state_key aligned_key[2]; @@ -405,10 +406,14 @@ print_state(struct pfctl_state *s, int opts) printf(", dummynet queue (%d %d)", s->dnpipe, s->dnrpipe); } - if (s->sync_flags & PFSYNC_FLAG_SRCNODE) - printf(", source-track"); - if (s->sync_flags & PFSYNC_FLAG_NATSRCNODE) - printf(", sticky-address"); + if (s->src_node_flags & PFSTATE_SRC_NODE_LIMIT) + printf(", %s", sn_type_names[PF_SN_LIMIT]); + if (s->src_node_flags & PFSTATE_SRC_NODE_LIMIT_GLOBAL) + printf(" global"); + if (s->src_node_flags & PFSTATE_SRC_NODE_NAT) + printf(", %s", sn_type_names[PF_SN_NAT]); + if (s->src_node_flags & PFSTATE_SRC_NODE_ROUTE) + printf(", %s", sn_type_names[PF_SN_ROUTE]); if (s->log) printf(", log"); if (s->log & PF_LOG_ALL) diff --git a/sbin/pfctl/pfctl.c b/sbin/pfctl/pfctl.c index 48e1d0b833c5..e05c96a252fc 100644 --- a/sbin/pfctl/pfctl.c +++ b/sbin/pfctl/pfctl.c @@ -1064,6 +1064,15 @@ pfctl_print_rule_counters(struct pfctl_rule *rule, int opts) rule->packets[1]), (unsigned long long)(rule->bytes[0] + rule->bytes[1]), (uintmax_t)rule->states_cur); + printf(" [ Source Nodes: %-6ju " + "Limit: %-6ju " + "NAT/RDR: %-6ju " + "Route: %-6ju " + "]\n", + (uintmax_t)rule->src_nodes, + (uintmax_t)rule->src_nodes_type[PF_SN_LIMIT], + (uintmax_t)rule->src_nodes_type[PF_SN_NAT], + (uintmax_t)rule->src_nodes_type[PF_SN_ROUTE]); if (!(opts & PF_OPT_DEBUG)) printf(" [ Inserted: uid %u pid %u " "State Creations: %-6ju]\n", diff --git a/sbin/pfctl/pfctl_parser.c b/sbin/pfctl/pfctl_parser.c index 7a6d2fc8eed5..bb458bce24fb 100644 --- a/sbin/pfctl/pfctl_parser.c +++ b/sbin/pfctl/pfctl_parser.c @@ -651,6 +651,7 @@ print_src_node(struct pfctl_src_node *sn, int opts) { struct pf_addr_wrap aw; uint64_t min, sec; + const char *sn_type_names[] = PF_SN_TYPE_NAMES; memset(&aw, 0, sizeof(aw)); if (sn->af == AF_INET) @@ -699,6 +700,7 @@ print_src_node(struct pfctl_src_node *sn, int opts) printf(", filter rule %u", sn->rule); break; } + printf(", %s", sn_type_names[sn->type]); printf("\n"); } } diff --git a/sys/net/pfvar.h b/sys/net/pfvar.h index d973fe15a5c4..076027e436dc 100644 --- a/sys/net/pfvar.h +++ b/sys/net/pfvar.h @@ -624,6 +624,21 @@ extern struct sx pf_end_lock; #define PF_ALGNMNT(off) (((off) % 2) == 0) +/* + * At the moment there are no rules which have both NAT and RDR actions, + * apart from af-to rules, but those don't to source tracking for address + * translation. And the r->rdr pool is used for both NAT and RDR. + * So there is no PF_SN_RDR. + */ +enum pf_sn_types { PF_SN_LIMIT, PF_SN_NAT, PF_SN_ROUTE, PF_SN_MAX }; +typedef enum pf_sn_types pf_sn_types_t; +#define PF_SN_TYPE_NAMES { \ + "limit source-track", \ + "NAT/RDR sticky-address", \ + "route sticky-address", \ + NULL \ +} + #ifdef _KERNEL struct pf_kpooladdr { @@ -822,7 +837,7 @@ struct pf_krule { counter_u64_t states_cur; counter_u64_t states_tot; - counter_u64_t src_nodes; + counter_u64_t src_nodes[PF_SN_MAX]; u_int16_t return_icmp; u_int16_t return_icmp6; @@ -904,6 +919,7 @@ struct pf_ksrc_node { sa_family_t af; sa_family_t naf; u_int8_t ruletype; + pf_sn_types_t type; struct mtx *lock; }; #endif @@ -1104,8 +1120,7 @@ struct pf_kstate { struct pf_udp_mapping *udp_mapping; struct pfi_kkif *kif; struct pfi_kkif *orig_kif; /* The real kif, even if we're a floating state (i.e. if == V_pfi_all). */ - struct pf_ksrc_node *src_node; - struct pf_ksrc_node *nat_src_node; + struct pf_ksrc_node *sns[PF_SN_MAX];/* source nodes */ u_int64_t packets[2]; u_int64_t bytes[2]; u_int64_t creation; @@ -1118,9 +1133,10 @@ struct pf_kstate { }; /* - * Size <= fits 11 objects per page on LP64. Try to not grow the struct beyond that. + * 6 cache lines per struct, 10 structs per page. + * Try to not grow the struct beyond that. */ -_Static_assert(sizeof(struct pf_kstate) <= 372, "pf_kstate size crosses 372 bytes"); +_Static_assert(sizeof(struct pf_kstate) <= 384, "pf_kstate size crosses 384 bytes"); #endif /* @@ -2367,7 +2383,7 @@ extern bool pf_src_node_exists(struct pf_ksrc_node **, struct pf_srchash *); extern struct pf_ksrc_node *pf_find_src_node(struct pf_addr *, struct pf_krule *, sa_family_t, - struct pf_srchash **, bool); + struct pf_srchash **, pf_sn_types_t, bool); extern void pf_unlink_src_node(struct pf_ksrc_node *); extern u_int pf_free_src_nodes(struct pf_ksrc_node_list *); extern void pf_print_state(struct pf_kstate *); @@ -2670,7 +2686,7 @@ u_short pf_map_addr_sn(u_int8_t, struct pf_krule *, struct pf_addr *, struct pf_addr *, struct pfi_kkif **nkif, struct pf_addr *, struct pf_ksrc_node **, struct pf_srchash **, - struct pf_kpool *); + struct pf_kpool *, pf_sn_types_t); int pf_get_transaddr_af(struct pf_krule *, struct pf_pdesc *); u_short pf_get_translation(struct pf_pdesc *, diff --git a/sys/netpfil/pf/pf.c b/sys/netpfil/pf/pf.c index 378be1e72d9a..c5042a7685c2 100644 --- a/sys/netpfil/pf/pf.c +++ b/sys/netpfil/pf/pf.c @@ -389,7 +389,7 @@ static void pf_overload_task(void *v, int pending); static u_short pf_insert_src_node(struct pf_ksrc_node **, struct pf_srchash **, struct pf_krule *, struct pf_addr *, sa_family_t, struct pf_addr *, - struct pfi_kkif *); + struct pfi_kkif *, pf_sn_types_t); static u_int pf_purge_expired_states(u_int, int); static void pf_purge_unlinked_rules(void); static int pf_mtag_uminit(void *, int, int); @@ -835,25 +835,26 @@ pf_check_threshold(struct pf_threshold *threshold) static bool pf_src_connlimit(struct pf_kstate *state) { - struct pf_overload_entry *pfoe; - bool limited = false; + struct pf_overload_entry *pfoe; + struct pf_ksrc_node *src_node = state->sns[PF_SN_LIMIT]; + bool limited = false; PF_STATE_LOCK_ASSERT(state); - PF_SRC_NODE_LOCK(state->src_node); + PF_SRC_NODE_LOCK(src_node); - state->src_node->conn++; + src_node->conn++; state->src.tcp_est = 1; - pf_add_threshold(&state->src_node->conn_rate); + pf_add_threshold(&src_node->conn_rate); if (state->rule->max_src_conn && state->rule->max_src_conn < - state->src_node->conn) { + src_node->conn) { counter_u64_add(V_pf_status.lcounters[LCNT_SRCCONN], 1); limited = true; } if (state->rule->max_src_conn_rate.limit && - pf_check_threshold(&state->src_node->conn_rate)) { + pf_check_threshold(&src_node->conn_rate)) { counter_u64_add(V_pf_status.lcounters[LCNT_SRCCONNRATE], 1); limited = true; } @@ -873,7 +874,7 @@ pf_src_connlimit(struct pf_kstate *state) if (pfoe == NULL) goto done; /* too bad :( */ - bcopy(&state->src_node->addr, &pfoe->addr, sizeof(pfoe->addr)); + bcopy(&src_node->addr, &pfoe->addr, sizeof(pfoe->addr)); pfoe->af = state->key[PF_SK_WIRE]->af; pfoe->rule = state->rule; pfoe->dir = state->direction; @@ -883,7 +884,7 @@ pf_src_connlimit(struct pf_kstate *state) taskqueue_enqueue(taskqueue_swi, &V_pf_overloadtask); done: - PF_SRC_NODE_UNLOCK(state->src_node); + PF_SRC_NODE_UNLOCK(src_node); return (limited); } @@ -985,7 +986,7 @@ pf_overload_task(void *v, int pending) */ struct pf_ksrc_node * pf_find_src_node(struct pf_addr *src, struct pf_krule *rule, sa_family_t af, - struct pf_srchash **sh, bool returnlocked) + struct pf_srchash **sh, pf_sn_types_t sn_type, bool returnlocked) { struct pf_ksrc_node *n; @@ -994,7 +995,7 @@ pf_find_src_node(struct pf_addr *src, struct pf_krule *rule, sa_family_t af, *sh = &V_pf_srchash[pf_hashsrc(src, af)]; PF_HASHROW_LOCK(*sh); LIST_FOREACH(n, &(*sh)->nodes, entry) - if (n->rule == rule && n->af == af && + if (n->rule == rule && n->af == af && n->type == sn_type && ((af == AF_INET && n->addr.v4.s_addr == src->v4.s_addr) || (af == AF_INET6 && bcmp(&n->addr, src, sizeof(*src)) == 0))) break; @@ -1039,27 +1040,43 @@ pf_free_src_node(struct pf_ksrc_node *sn) } static u_short -pf_insert_src_node(struct pf_ksrc_node **sn, struct pf_srchash **sh, - struct pf_krule *rule, struct pf_addr *src, sa_family_t af, - struct pf_addr *raddr, struct pfi_kkif *rkif) +pf_insert_src_node(struct pf_ksrc_node *sns[PF_SN_MAX], + struct pf_srchash *snhs[PF_SN_MAX], struct pf_krule *rule, + struct pf_addr *src, sa_family_t af, struct pf_addr *raddr, + struct pfi_kkif *rkif, pf_sn_types_t sn_type) { u_short reason = 0; + struct pf_krule *r_track = rule; + struct pf_ksrc_node **sn = &(sns[sn_type]); + struct pf_srchash **sh = &(snhs[sn_type]); - KASSERT((rule->rule_flag & PFRULE_SRCTRACK || - rule->rdr.opts & PF_POOL_STICKYADDR), - ("%s for non-tracking rule %p", __func__, rule)); + KASSERT(sn_type != PF_SN_LIMIT || (raddr == NULL && rkif == NULL), + ("%s: raddr and rkif must be NULL for PF_SN_LIMIT", __func__)); + + KASSERT(sn_type != PF_SN_LIMIT || (rule->rule_flag & PFRULE_SRCTRACK), + ("%s: PF_SN_LIMIT only valid for rules with PFRULE_SRCTRACK", __func__)); + + /* + * XXX: There could be a KASSERT for + * sn_type == PF_SN_LIMIT || (pool->opts & PF_POOL_STICKYADDR) + * but we'd need to pass pool *only* for this KASSERT. + */ + + if ( (rule->rule_flag & PFRULE_SRCTRACK) && + !(rule->rule_flag & PFRULE_RULESRCTRACK)) + r_track = &V_pf_default_rule; /* * Request the sh to always be locked, as we might insert a new sn. */ if (*sn == NULL) - *sn = pf_find_src_node(src, rule, af, sh, true); + *sn = pf_find_src_node(src, r_track, af, sh, sn_type, true); if (*sn == NULL) { PF_HASHROW_ASSERT(*sh); - if (rule->max_src_nodes && - counter_u64_fetch(rule->src_nodes) >= rule->max_src_nodes) { + if (sn_type == PF_SN_LIMIT && rule->max_src_nodes && + counter_u64_fetch(r_track->src_nodes[sn_type]) >= rule->max_src_nodes) { counter_u64_add(V_pf_status.lcounters[LCNT_SRCNODES], 1); reason = PFRES_SRCLIMIT; goto done; @@ -1082,26 +1099,28 @@ pf_insert_src_node(struct pf_ksrc_node **sn, struct pf_srchash **sh, } } - pf_init_threshold(&(*sn)->conn_rate, - rule->max_src_conn_rate.limit, - rule->max_src_conn_rate.seconds); + if (sn_type == PF_SN_LIMIT) + pf_init_threshold(&(*sn)->conn_rate, + rule->max_src_conn_rate.limit, + rule->max_src_conn_rate.seconds); MPASS((*sn)->lock == NULL); (*sn)->lock = &(*sh)->lock; (*sn)->af = af; - (*sn)->rule = rule; + (*sn)->rule = r_track; PF_ACPY(&(*sn)->addr, src, af); - PF_ACPY(&(*sn)->raddr, raddr, af); + if (raddr != NULL) + PF_ACPY(&(*sn)->raddr, raddr, af); (*sn)->rkif = rkif; LIST_INSERT_HEAD(&(*sh)->nodes, *sn, entry); (*sn)->creation = time_uptime; (*sn)->ruletype = rule->action; - if ((*sn)->rule != NULL) - counter_u64_add((*sn)->rule->src_nodes, 1); + (*sn)->type = sn_type; + counter_u64_add(r_track->src_nodes[sn_type], 1); counter_u64_add(V_pf_status.scounters[SCNT_SRC_NODE_INSERT], 1); } else { - if (rule->max_src_states && + if (sn_type == PF_SN_LIMIT && rule->max_src_states && (*sn)->states >= rule->max_src_states) { counter_u64_add(V_pf_status.lcounters[LCNT_SRCSTATES], 1); @@ -1126,7 +1145,7 @@ pf_unlink_src_node(struct pf_ksrc_node *src) LIST_REMOVE(src, entry); if (src->rule) - counter_u64_add(src->rule->src_nodes, -1); + counter_u64_add(src->rule->src_nodes[src->type], -1); } u_int @@ -2647,30 +2666,24 @@ pf_purge_expired_src_nodes(void) static void pf_src_tree_remove_state(struct pf_kstate *s) { - struct pf_ksrc_node *sn; uint32_t timeout; timeout = s->rule->timeout[PFTM_SRC_NODE] ? s->rule->timeout[PFTM_SRC_NODE] : V_pf_default_rule.timeout[PFTM_SRC_NODE]; - if (s->src_node != NULL) { - sn = s->src_node; - PF_SRC_NODE_LOCK(sn); - if (s->src.tcp_est) - --sn->conn; - if (--sn->states == 0) - sn->expire = time_uptime + timeout; - PF_SRC_NODE_UNLOCK(sn); - } - if (s->nat_src_node != s->src_node && s->nat_src_node != NULL) { - sn = s->nat_src_node; - PF_SRC_NODE_LOCK(sn); - if (--sn->states == 0) - sn->expire = time_uptime + timeout; - PF_SRC_NODE_UNLOCK(sn); + for (pf_sn_types_t sn_type=0; sn_type<PF_SN_MAX; sn_type++) { + if (s->sns[sn_type] == NULL) + continue; + PF_SRC_NODE_LOCK(s->sns[sn_type]); + if (sn_type == PF_SN_LIMIT && s->src.tcp_est) + --(s->sns[sn_type]->conn); + if (--(s->sns[sn_type]->states) == 0) + s->sns[sn_type]->expire = time_uptime + timeout; + PF_SRC_NODE_UNLOCK(s->sns[sn_type]); + s->sns[sn_type] = NULL; } - s->src_node = s->nat_src_node = NULL; + } /* @@ -5895,7 +5908,7 @@ nextrule: pd->act.rt = r->rt; /* Don't use REASON_SET, pf_map_addr increases the reason counters */ reason = pf_map_addr_sn(pd->af, r, pd->src, &pd->act.rt_addr, - &pd->act.rt_kif, NULL, &sn, &snh, pool); + &pd->act.rt_kif, NULL, &sn, &snh, pool, PF_SN_ROUTE); if (reason != 0) goto cleanup; } @@ -5997,14 +6010,18 @@ pf_create_state(struct pf_krule *r, struct pf_krule *nr, struct pf_krule *a, struct pf_udp_mapping *udp_mapping) { struct pf_kstate *s = NULL; - struct pf_ksrc_node *sn = NULL; - struct pf_srchash *snh = NULL; - struct pf_ksrc_node *nsn = NULL; - struct pf_srchash *nsnh = NULL; + struct pf_ksrc_node *sns[PF_SN_MAX] = { NULL }; + /* + * XXXKS: The hash for PF_SN_LIMIT and PF_SN_ROUTE should be the same + * but for PF_SN_NAT it is different. Don't try optimizing it, + * just store all 3 hashes. + */ + struct pf_srchash *snhs[PF_SN_MAX] = { NULL }; struct tcphdr *th = &pd->hdr.tcp; u_int16_t mss = V_tcp_mssdflt; u_short reason, sn_reason; struct pf_krule_item *ri; + struct pf_kpool *pool_route = &r->route; /* check maximums */ if (r->max_states && @@ -6013,18 +6030,26 @@ pf_create_state(struct pf_krule *r, struct pf_krule *nr, struct pf_krule *a, REASON_SET(&reason, PFRES_MAXSTATES); goto csfailed; } - /* src node for filter rule */ - if ((r->rule_flag & PFRULE_SRCTRACK || - r->rdr.opts & PF_POOL_STICKYADDR) && - (sn_reason = pf_insert_src_node(&sn, &snh, r, pd->src, pd->af, - &pd->act.rt_addr, pd->act.rt_kif)) != 0) { + /* src node for limits */ + if ((r->rule_flag & PFRULE_SRCTRACK) && + (sn_reason = pf_insert_src_node(sns, snhs, r, pd->src, pd->af, + NULL, NULL, PF_SN_LIMIT)) != 0) { + REASON_SET(&reason, sn_reason); + goto csfailed; + } + /* src node for route-to rule */ + if (TAILQ_EMPTY(&pool_route->list)) /* Backwards compatibility. */ + pool_route = &r->rdr; + if ((pool_route->opts & PF_POOL_STICKYADDR) && + (sn_reason = pf_insert_src_node(sns, snhs, r, pd->src, pd->af, + &pd->act.rt_addr, pd->act.rt_kif, PF_SN_ROUTE)) != 0) { REASON_SET(&reason, sn_reason); goto csfailed; } /* src node for translation rule */ if (nr != NULL && (nr->rdr.opts & PF_POOL_STICKYADDR) && - (sn_reason = pf_insert_src_node(&nsn, &nsnh, nr, &sk->addr[pd->sidx], - pd->af, &nk->addr[1], NULL)) != 0 ) { + (sn_reason = pf_insert_src_node(sns, snhs, nr, &sk->addr[pd->sidx], + pd->af, &nk->addr[1], NULL, PF_SN_NAT)) != 0 ) { REASON_SET(&reason, sn_reason); goto csfailed; } @@ -6166,13 +6191,11 @@ pf_create_state(struct pf_krule *r, struct pf_krule *nr, struct pf_krule *a, /* * Lock order is important: first state, then source node. */ - if (pf_src_node_exists(&sn, snh)) { - s->src_node = sn; - PF_HASHROW_UNLOCK(snh); - } - if (pf_src_node_exists(&nsn, nsnh)) { - s->nat_src_node = nsn; - PF_HASHROW_UNLOCK(nsnh); + for (pf_sn_types_t sn_type=0; sn_type<PF_SN_MAX; sn_type++) { + if (pf_src_node_exists(&sns[sn_type], snhs[sn_type])) { + s->sns[sn_type] = sns[sn_type]; + PF_HASHROW_UNLOCK(snhs[sn_type]); + } } if (tag > 0) @@ -6223,24 +6246,17 @@ csfailed: uma_zfree(V_pf_state_key_z, sk); uma_zfree(V_pf_state_key_z, nk); - if (pf_src_node_exists(&sn, snh)) { - if (--sn->states == 0 && sn->expire == 0) { - pf_unlink_src_node(sn); - pf_free_src_node(sn); - counter_u64_add( - V_pf_status.scounters[SCNT_SRC_NODE_REMOVALS], 1); - } - PF_HASHROW_UNLOCK(snh); - } - - if (sn != nsn && pf_src_node_exists(&nsn, nsnh)) { - if (--nsn->states == 0 && nsn->expire == 0) { - pf_unlink_src_node(nsn); - pf_free_src_node(nsn); - counter_u64_add( - V_pf_status.scounters[SCNT_SRC_NODE_REMOVALS], 1); + for (pf_sn_types_t sn_type=0; sn_type<PF_SN_MAX; sn_type++) { + if (pf_src_node_exists(&sns[sn_type], snhs[sn_type])) { + if (--sns[sn_type]->states == 0 && + sns[sn_type]->expire == 0) { + pf_unlink_src_node(sns[sn_type]); + pf_free_src_node(sns[sn_type]); + counter_u64_add( + V_pf_status.scounters[SCNT_SRC_NODE_REMOVALS], 1); + } + PF_HASHROW_UNLOCK(snhs[sn_type]); } - PF_HASHROW_UNLOCK(nsnh); } drop: @@ -6575,7 +6591,7 @@ pf_tcp_track_full(struct pf_kstate **state, struct pf_pdesc *pd, pf_set_protostate(*state, pdst, TCPS_ESTABLISHED); if (src->state == TCPS_ESTABLISHED && - (*state)->src_node != NULL && + (*state)->sns[PF_SN_LIMIT] != NULL && pf_src_connlimit(*state)) { REASON_SET(reason, PFRES_SRCLIMIT); return (PF_DROP); @@ -6746,7 +6762,7 @@ pf_tcp_track_sloppy(struct pf_kstate **state, struct pf_pdesc *pd, u_short *reas if (dst->state == TCPS_SYN_SENT) { pf_set_protostate(*state, pdst, TCPS_ESTABLISHED); if (src->state == TCPS_ESTABLISHED && - (*state)->src_node != NULL && + (*state)->sns[PF_SN_LIMIT] != NULL && pf_src_connlimit(*state)) { REASON_SET(reason, PFRES_SRCLIMIT); return (PF_DROP); @@ -6764,7 +6780,7 @@ pf_tcp_track_sloppy(struct pf_kstate **state, struct pf_pdesc *pd, u_short *reas pf_set_protostate(*state, PF_PEER_BOTH, TCPS_ESTABLISHED); dst->state = src->state = TCPS_ESTABLISHED; - if ((*state)->src_node != NULL && + if ((*state)->sns[PF_SN_LIMIT] != NULL && pf_src_connlimit(*state)) { REASON_SET(reason, PFRES_SRCLIMIT); return (PF_DROP); @@ -6831,7 +6847,7 @@ pf_synproxy(struct pf_pdesc *pd, struct pf_kstate **state, u_short *reason) (ntohl(th->th_seq) != (*state)->src.seqlo + 1)) { REASON_SET(reason, PFRES_SYNPROXY); return (PF_DROP); - } else if ((*state)->src_node != NULL && + } else if ((*state)->sns[PF_SN_LIMIT] != NULL && pf_src_connlimit(*state)) { REASON_SET(reason, PFRES_SRCLIMIT); return (PF_DROP); @@ -10023,17 +10039,21 @@ pf_counters_inc(int action, struct pf_pdesc *pd, pf_counter_u64_add_protected(&s->nat_rule->bytes[dirndx], pd->tot_len); } - if (s->src_node != NULL) { - counter_u64_add(s->src_node->packets[dirndx], - 1); - counter_u64_add(s->src_node->bytes[dirndx], - pd->tot_len); - } - if (s->nat_src_node != NULL) { - counter_u64_add(s->nat_src_node->packets[dirndx], - 1); - counter_u64_add(s->nat_src_node->bytes[dirndx], - pd->tot_len); + /* + * Source nodes are accessed unlocked here. + * But since we are operating with stateful tracking + * and the state is locked, those SNs could not have + * been freed. + */ + for (pf_sn_types_t sn_type=0; sn_type<PF_SN_MAX; sn_type++) { + if (s->sns[sn_type] != NULL) { + counter_u64_add( + s->sns[sn_type]->packets[dirndx], + 1); + counter_u64_add( + s->sns[sn_type]->bytes[dirndx], + pd->tot_len); + } } dirndx = (dir == s->direction) ? 0 : 1; s->packets[dirndx]++; diff --git a/sys/netpfil/pf/pf.h b/sys/netpfil/pf/pf.h index 45652f174884..dfa86e7f1d6d 100644 --- a/sys/netpfil/pf/pf.h +++ b/sys/netpfil/pf/pf.h @@ -649,6 +649,12 @@ struct pf_rule { #define PFSTATE_SCRUBMASK (PFSTATE_NODF|PFSTATE_RANDOMID|PFSTATE_SCRUB_TCP) #define PFSTATE_SETMASK (PFSTATE_SETTOS|PFSTATE_SETPRIO) +/* pfctl_state->src_node_flags */ +#define PFSTATE_SRC_NODE_LIMIT 0x01 +#define PFSTATE_SRC_NODE_NAT 0x02 +#define PFSTATE_SRC_NODE_ROUTE 0x04 +#define PFSTATE_SRC_NODE_LIMIT_GLOBAL 0x10 + #define PFSTATE_HIWAT 100000 /* default state table size */ #define PFSTATE_ADAPT_START 60000 /* default adaptive timeout start */ #define PFSTATE_ADAPT_END 120000 /* default adaptive timeout end */ diff --git a/sys/netpfil/pf/pf_ioctl.c b/sys/netpfil/pf/pf_ioctl.c index bea2cf1a5331..6553981a1059 100644 --- a/sys/netpfil/pf/pf_ioctl.c +++ b/sys/netpfil/pf/pf_ioctl.c @@ -352,7 +352,8 @@ pfattach_vnet(void) } V_pf_default_rule.states_cur = counter_u64_alloc(M_WAITOK); V_pf_default_rule.states_tot = counter_u64_alloc(M_WAITOK); - V_pf_default_rule.src_nodes = counter_u64_alloc(M_WAITOK); + for (pf_sn_types_t sn_type = 0; sn_type<PF_SN_MAX; sn_type++) + V_pf_default_rule.src_nodes[sn_type] = counter_u64_alloc(M_WAITOK); V_pf_default_rule.timestamp = uma_zalloc_pcpu(pf_timestamp_pcpu_zone, M_WAITOK | M_ZERO); @@ -1854,7 +1855,8 @@ pf_krule_free(struct pf_krule *rule) } counter_u64_free(rule->states_cur); counter_u64_free(rule->states_tot); - counter_u64_free(rule->src_nodes); + for (pf_sn_types_t sn_type=0; sn_type<PF_SN_MAX; sn_type++) + counter_u64_free(rule->src_nodes[sn_type]); uma_zfree_pcpu(pf_timestamp_pcpu_zone, rule->timestamp); mtx_destroy(&rule->nat.mtx); @@ -2090,7 +2092,8 @@ pf_ioctl_addrule(struct pf_krule *rule, uint32_t ticket, } rule->states_cur = counter_u64_alloc(M_WAITOK); rule->states_tot = counter_u64_alloc(M_WAITOK); - rule->src_nodes = counter_u64_alloc(M_WAITOK); + for (pf_sn_types_t sn_type=0; sn_type<PF_SN_MAX; sn_type++) + rule->src_nodes[sn_type] = counter_u64_alloc(M_WAITOK); rule->cuid = uid; rule->cpid = pid; TAILQ_INIT(&rule->rdr.list); @@ -3651,7 +3654,8 @@ DIOCGETRULENV_error: } newrule->states_cur = counter_u64_alloc(M_WAITOK); newrule->states_tot = counter_u64_alloc(M_WAITOK); - newrule->src_nodes = counter_u64_alloc(M_WAITOK); + for (pf_sn_types_t sn_type=0; sn_type<PF_SN_MAX; sn_type++) + newrule->src_nodes[sn_type] = counter_u64_alloc(M_WAITOK); newrule->cuid = td->td_ucred->cr_ruid; newrule->cpid = td->td_proc ? td->td_proc->p_pid : 0; TAILQ_INIT(&newrule->nat.list); @@ -5672,9 +5676,14 @@ pfsync_state_export(union pfsync_state_union *sp, struct pf_kstate *st, int msg_ __func__, msg_version); } - if (st->src_node) + /* + * XXX Why do we bother pfsyncing source node information if source + * nodes are not synced? Showing users that there is source tracking + * when there is none seems useless. + */ + if (st->sns[PF_SN_LIMIT] != NULL) sp->pfs_1301.sync_flags |= PFSYNC_FLAG_SRCNODE; - if (st->nat_src_node) + if (st->sns[PF_SN_NAT] != NULL || st->sns[PF_SN_ROUTE]) sp->pfs_1301.sync_flags |= PFSYNC_FLAG_NATSRCNODE; sp->pfs_1301.id = st->id; @@ -5738,11 +5747,10 @@ pf_state_export(struct pf_state_export *sp, struct pf_kstate *st) /* 8 bits for the old libpfctl, 16 bits for the new libpfctl */ sp->state_flags_compat = st->state_flags; sp->state_flags = htons(st->state_flags); - if (st->src_node) + if (st->sns[PF_SN_LIMIT] != NULL) sp->sync_flags |= PFSYNC_FLAG_SRCNODE; - if (st->nat_src_node) + if (st->sns[PF_SN_NAT] != NULL || st->sns[PF_SN_ROUTE] != NULL) sp->sync_flags |= PFSYNC_FLAG_NATSRCNODE; - sp->id = st->id; sp->creatorid = st->creatorid; pf_state_peer_hton(&st->src, &sp->src); @@ -6007,10 +6015,13 @@ pf_kill_srcnodes(struct pfioc_src_node_kill *psnk) PF_HASHROW_LOCK(ih); LIST_FOREACH(s, &ih->states, entry) { - if (s->src_node && s->src_node->expire == 1) - s->src_node = NULL; - if (s->nat_src_node && s->nat_src_node->expire == 1) - s->nat_src_node = NULL; + for(pf_sn_types_t sn_type=0; sn_type<PF_SN_MAX; + sn_type++) { + if (s->sns[sn_type] && + s->sns[sn_type]->expire == 1) { + s->sns[sn_type] = NULL; + } + } } PF_HASHROW_UNLOCK(ih); } @@ -6834,7 +6845,8 @@ pf_unload_vnet(void) } counter_u64_free(V_pf_default_rule.states_cur); counter_u64_free(V_pf_default_rule.states_tot); - counter_u64_free(V_pf_default_rule.src_nodes); + for (pf_sn_types_t sn_type=0; sn_type<PF_SN_MAX; sn_type++) + counter_u64_free(V_pf_default_rule.src_nodes[sn_type]); uma_zfree_pcpu(pf_timestamp_pcpu_zone, V_pf_default_rule.timestamp); for (int i = 0; i < PFRES_MAX; i++) diff --git a/sys/netpfil/pf/pf_lb.c b/sys/netpfil/pf/pf_lb.c index 23c7ad1c0a66..9c2d7b4c71b6 100644 --- a/sys/netpfil/pf/pf_lb.c +++ b/sys/netpfil/pf/pf_lb.c @@ -75,9 +75,11 @@ static void pf_hash(struct pf_addr *, struct pf_addr *, struct pf_poolhashkey *, sa_family_t); static struct pf_krule *pf_match_translation(struct pf_pdesc *, int, struct pf_kanchor_stackframe *); -static int pf_get_sport(struct pf_pdesc *, struct pf_krule *, - struct pf_addr *, uint16_t *, uint16_t, uint16_t, struct pf_ksrc_node **, - struct pf_srchash **, struct pf_kpool *, struct pf_udp_mapping **); +static int pf_get_sport(struct pf_pdesc *, struct pf_krule *, + struct pf_addr *, uint16_t *, uint16_t, uint16_t, + struct pf_ksrc_node **, struct pf_srchash **, + struct pf_kpool *, struct pf_udp_mapping **, + pf_sn_types_t); static bool pf_islinklocal(const sa_family_t, const struct pf_addr *); #define mix(a,b,c) \ @@ -231,7 +233,7 @@ pf_get_sport(struct pf_pdesc *pd, struct pf_krule *r, struct pf_addr *naddr, uint16_t *nport, uint16_t low, uint16_t high, struct pf_ksrc_node **sn, struct pf_srchash **sh, struct pf_kpool *rpool, - struct pf_udp_mapping **udp_mapping) + struct pf_udp_mapping **udp_mapping, pf_sn_types_t sn_type) { struct pf_state_key_cmp key; struct pf_addr init_addr; @@ -262,7 +264,8 @@ pf_get_sport(struct pf_pdesc *pd, struct pf_krule *r, /* Try to find a src_node as per pf_map_addr(). */ if (*sn == NULL && rpool->opts & PF_POOL_STICKYADDR && (rpool->opts & PF_POOL_TYPEMASK) != PF_POOL_NONE) - *sn = pf_find_src_node(&pd->nsaddr, r, pd->af, sh, false); + *sn = pf_find_src_node(&pd->nsaddr, r, + pd->af, sh, sn_type, false); if (*sn != NULL) PF_SRC_NODE_UNLOCK(*sn); return (0); @@ -276,7 +279,7 @@ pf_get_sport(struct pf_pdesc *pd, struct pf_krule *r, } if (pf_map_addr_sn(pd->naf, r, &pd->nsaddr, naddr, NULL, &init_addr, - sn, sh, rpool)) + sn, sh, rpool, sn_type)) goto failed; if (pd->proto == IPPROTO_ICMP) { @@ -400,7 +403,7 @@ pf_get_sport(struct pf_pdesc *pd, struct pf_krule *r, */ (*sn) = NULL; if (pf_map_addr_sn(pd->naf, r, &pd->nsaddr, naddr, NULL, - &init_addr, sn, sh, rpool)) + &init_addr, sn, sh, rpool, sn_type)) return (1); break; case PF_POOL_NONE: @@ -453,14 +456,14 @@ pf_get_mape_sport(struct pf_pdesc *pd, struct pf_krule *r, low = (i << ashift) | psmask; if (!pf_get_sport(pd, r, naddr, nport, low, low | highmask, sn, sh, &r->rdr, - udp_mapping)) + udp_mapping, PF_SN_NAT)) return (0); } for (i = cut - 1; i > 0; i--) { low = (i << ashift) | psmask; if (!pf_get_sport(pd, r, naddr, nport, low, low | highmask, sn, sh, &r->rdr, - udp_mapping)) + udp_mapping, PF_SN_NAT)) return (0); } return (1); @@ -642,7 +645,8 @@ done_pool_mtx: u_short pf_map_addr_sn(sa_family_t af, struct pf_krule *r, struct pf_addr *saddr, struct pf_addr *naddr, struct pfi_kkif **nkif, struct pf_addr *init_addr, - struct pf_ksrc_node **sn, struct pf_srchash **sh, struct pf_kpool *rpool) + struct pf_ksrc_node **sn, struct pf_srchash **sh, struct pf_kpool *rpool, + pf_sn_types_t sn_type) { u_short reason = 0; @@ -655,7 +659,7 @@ pf_map_addr_sn(sa_family_t af, struct pf_krule *r, struct pf_addr *saddr, */ if (rpool->opts & PF_POOL_STICKYADDR && (rpool->opts & PF_POOL_TYPEMASK) != PF_POOL_NONE) - *sn = pf_find_src_node(saddr, r, af, sh, false); + *sn = pf_find_src_node(saddr, r, af, sh, sn_type, false); if (*sn != NULL) { PF_SRC_NODE_LOCK_ASSERT(*sn); @@ -780,7 +784,7 @@ pf_get_translation(struct pf_pdesc *pd, int off, goto notrans; } } else if (pf_get_sport(pd, r, naddr, nportp, low, high, &sn, - &sh, &r->rdr, udp_mapping)) { + &sh, &r->rdr, udp_mapping, PF_SN_NAT)) { DPFPRINTF(PF_DEBUG_MISC, ("pf: NAT proxy port allocation (%u-%u) failed\n", r->rdr.proxy_port[0], r->rdr.proxy_port[1])); @@ -868,7 +872,7 @@ pf_get_translation(struct pf_pdesc *pd, int off, uint16_t cut, low, high, nport; reason = pf_map_addr_sn(pd->af, r, &pd->nsaddr, naddr, NULL, - NULL, &sn, &sh, &r->rdr); + NULL, &sn, &sh, &r->rdr, PF_SN_NAT); if (reason != 0) goto notrans; if ((r->rdr.opts & PF_POOL_TYPEMASK) == PF_POOL_BITMASK) @@ -1007,7 +1011,8 @@ pf_get_transaddr_af(struct pf_krule *r, struct pf_pdesc *pd) /* get source address and port */ if (pf_get_sport(pd, r, &nsaddr, &nport, - r->nat.proxy_port[0], r->nat.proxy_port[1], &sns, &sh, &r->nat, NULL)) { + r->nat.proxy_port[0], r->nat.proxy_port[1], &sns, &sh, &r->nat, + NULL, PF_SN_NAT)) { DPFPRINTF(PF_DEBUG_MISC, ("pf: af-to NAT proxy port allocation (%u-%u) failed", r->nat.proxy_port[0], r->nat.proxy_port[1])); @@ -1051,7 +1056,7 @@ pf_get_transaddr_af(struct pf_krule *r, struct pf_pdesc *pd) /* get the destination address and port */ if (! TAILQ_EMPTY(&r->rdr.list)) { if (pf_map_addr_sn(pd->naf, r, &nsaddr, &naddr, NULL, NULL, - &sns, NULL, &r->rdr)) + &sns, NULL, &r->rdr, PF_SN_NAT)) return (-1); if (r->rdr.proxy_port[0]) pd->ndport = htons(r->rdr.proxy_port[0]); diff --git a/sys/netpfil/pf/pf_nl.c b/sys/netpfil/pf/pf_nl.c index 4cdb16d1fbba..73c39e1f7471 100644 --- a/sys/netpfil/pf/pf_nl.c +++ b/sys/netpfil/pf/pf_nl.c @@ -186,9 +186,9 @@ dump_state(struct nlpcb *nlp, const struct nlmsghdr *hdr, struct pf_kstate *s, nlattr_add_u8(nw, PF_ST_TIMEOUT, s->timeout); nlattr_add_u16(nw, PF_ST_STATE_FLAGS, s->state_flags); uint8_t sync_flags = 0; - if (s->src_node) + if (s->sns[PF_SN_LIMIT] != NULL) sync_flags |= PFSYNC_FLAG_SRCNODE; - if (s->nat_src_node) + if (s->sns[PF_SN_NAT] != NULL || s->sns[PF_SN_ROUTE]) sync_flags |= PFSYNC_FLAG_NATSRCNODE; nlattr_add_u8(nw, PF_ST_SYNC_FLAGS, sync_flags); nlattr_add_u64(nw, PF_ST_ID, s->id); @@ -210,6 +210,17 @@ dump_state(struct nlpcb *nlp, const struct nlmsghdr *hdr, struct pf_kstate *s, nlattr_add_u8(nw, PF_ST_RT, s->act.rt); if (s->act.rt_kif != NULL) nlattr_add_string(nw, PF_ST_RT_IFNAME, s->act.rt_kif->pfik_name); + uint8_t src_node_flags = 0; + if (s->sns[PF_SN_LIMIT] != NULL) { + src_node_flags |= PFSTATE_SRC_NODE_LIMIT; + if (s->sns[PF_SN_LIMIT]->rule == &V_pf_default_rule) + src_node_flags |= PFSTATE_SRC_NODE_LIMIT_GLOBAL; + } + if (s->sns[PF_SN_NAT] != NULL) + src_node_flags |= PFSTATE_SRC_NODE_NAT; + if (s->sns[PF_SN_ROUTE] != NULL) + src_node_flags |= PFSTATE_SRC_NODE_ROUTE; + nlattr_add_u8(nw, PF_ST_SRC_NODE_FLAGS, src_node_flags); if (!dump_state_peer(nw, PF_ST_PEER_SRC, &s->src)) goto enomem; @@ -854,6 +865,7 @@ pf_handle_getrule(struct nlmsghdr *hdr, struct nl_pstate *npt) struct genlmsghdr *ghdr_new; struct pf_kruleset *ruleset; struct pf_krule *rule; + u_int64_t src_nodes_total = 0; int rs_num; int error; @@ -985,7 +997,12 @@ pf_handle_getrule(struct nlmsghdr *hdr, struct nl_pstate *npt) nlattr_add_u64(nw, PF_RT_TIMESTAMP, pf_get_timestamp(rule)); nlattr_add_u64(nw, PF_RT_STATES_CUR, counter_u64_fetch(rule->states_cur)); nlattr_add_u64(nw, PF_RT_STATES_TOTAL, counter_u64_fetch(rule->states_tot)); - nlattr_add_u64(nw, PF_RT_SRC_NODES, counter_u64_fetch(rule->src_nodes)); + for (pf_sn_types_t sn_type=0; sn_type<PF_SN_MAX; sn_type++) + src_nodes_total += counter_u64_fetch(rule->src_nodes[sn_type]); + nlattr_add_u64(nw, PF_RT_SRC_NODES, src_nodes_total); + nlattr_add_u64(nw, PF_RT_SRC_NODES_LIMIT, counter_u64_fetch(rule->src_nodes[PF_SN_LIMIT])); + nlattr_add_u64(nw, PF_RT_SRC_NODES_NAT, counter_u64_fetch(rule->src_nodes[PF_SN_NAT])); + nlattr_add_u64(nw, PF_RT_SRC_NODES_ROUTE, counter_u64_fetch(rule->src_nodes[PF_SN_ROUTE])); error = pf_kanchor_copyout(ruleset, rule, anchor_call, sizeof(anchor_call)); MPASS(error == 0); @@ -1785,6 +1802,8 @@ pf_handle_get_srcnodes(struct nlmsghdr *hdr, struct nl_pstate *npt) *** 252 LINES SKIPPED ***
Want to link to this message? Use this URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?202502131500.51DF0nbT046085>