git: 726ff260ecfa - main - pfctl: optionally print the rule in the state overview

From: Kristof Provost <kp_at_FreeBSD.org>
Date: Thu, 07 May 2026 16:24:01 UTC
The branch main has been updated by kp:

URL: https://cgit.FreeBSD.org/src/commit/?id=726ff260ecfa38878aec982456c44ddb0f9c791b

commit 726ff260ecfa38878aec982456c44ddb0f9c791b
Author:     Kristof Provost <kp@FreeBSD.org>
AuthorDate: 2026-05-05 12:42:16 +0000
Commit:     Kristof Provost <kp@FreeBSD.org>
CommitDate: 2026-05-07 15:06:56 +0000

    pfctl: optionally print the rule in the state overview
    
    When dumping states optionally (at '-vv') also show the rule which
    created the state. This can be helpful if the ruleset changed and we
    want to know what rule created the state.
    
    Sponsored by:   Rubicon Communications, LLC ("Netgate")
---
 lib/libpfctl/libpfctl.c           |   4 +
 lib/libpfctl/libpfctl.h           |   2 +
 sbin/pfctl/pf_print_state.c       |   4 +
 sbin/pfctl/pfctl.c                |   3 +
 sys/netpfil/pf/pf_nl.c            | 189 +++++++++++++++++++++++---------------
 sys/netpfil/pf/pf_nl.h            |   2 +
 tests/sys/netpfil/pf/get_state.sh |  44 +++++++++
 tests/sys/netpfil/pf/killstate.sh |   2 +-
 8 files changed, 173 insertions(+), 77 deletions(-)

diff --git a/lib/libpfctl/libpfctl.c b/lib/libpfctl/libpfctl.c
index ac60a924c228..4e51167b401a 100644
--- a/lib/libpfctl/libpfctl.c
+++ b/lib/libpfctl/libpfctl.c
@@ -1929,6 +1929,8 @@ static const struct snl_attr_parser nla_p_skey[] = {
 SNL_DECLARE_ATTR_PARSER(skey_parser, nla_p_skey);
 #undef _OUT
 
+SNL_DECLARE_ATTR_PARSER(rule_parser, ap_getrule);
+
 #define	_IN(_field)	offsetof(struct genlmsghdr, _field)
 #define	_OUT(_field)	offsetof(struct pfctl_state, _field)
 static struct snl_attr_parser ap_state[] = {
@@ -1963,6 +1965,7 @@ static struct snl_attr_parser ap_state[] = {
 	{ .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 },
 	{ .type = PF_ST_RT_AF, .off = _OUT(rt_af), .cb = snl_attr_get_uint8 },
+	{ .type = PF_ST_CREATED_BY_RULE, .off = _OUT(created_by_rule), .arg = &rule_parser, .cb = snl_attr_get_nested },
 };
 #undef _IN
 #undef _OUT
@@ -1988,6 +1991,7 @@ pfctl_get_states_h(struct pfctl_handle *h, struct pfctl_state_filter *filter, pf
 	snl_add_msg_attr_u8(&nw, PF_ST_AF, filter->af);
 	snl_add_msg_attr_ip6(&nw, PF_ST_FILTER_ADDR, &filter->addr.v6);
 	snl_add_msg_attr_ip6(&nw, PF_ST_FILTER_MASK, &filter->mask.v6);
+	snl_add_msg_attr_bool(&nw, PF_ST_INCLUDE_RULE, filter->include_rule);
 
 	hdr = snl_finalize_msg(&nw);
 	if (hdr == NULL)
diff --git a/lib/libpfctl/libpfctl.h b/lib/libpfctl/libpfctl.h
index 1012be53db65..3080209ec7a0 100644
--- a/lib/libpfctl/libpfctl.h
+++ b/lib/libpfctl/libpfctl.h
@@ -406,6 +406,7 @@ struct pfctl_state {
 	char			 rt_ifname[IFNAMSIZ];
 	sa_family_t		 rt_af;
 	uint8_t			 src_node_flags;
+	struct pfctl_rule	 created_by_rule;
 };
 
 TAILQ_HEAD(pfctl_statelist, pfctl_state);
@@ -504,6 +505,7 @@ struct pfctl_state_filter {
 	sa_family_t		af;
 	struct pf_addr		addr;
 	struct pf_addr		mask;
+	bool			include_rule;
 };
 typedef int (*pfctl_get_state_fn)(struct pfctl_state *, void *);
 int pfctl_get_states_iter(pfctl_get_state_fn f, void *arg);
diff --git a/sbin/pfctl/pf_print_state.c b/sbin/pfctl/pf_print_state.c
index 1c5a46f86b35..9b0c57fd73d3 100644
--- a/sbin/pfctl/pf_print_state.c
+++ b/sbin/pfctl/pf_print_state.c
@@ -434,6 +434,10 @@ print_state(struct pfctl_state *s, int opts)
 
 		if (strcmp(s->ifname, s->orig_ifname) != 0)
 			printf("   origif: %s\n", s->orig_ifname);
+
+		printf("   rule: ");
+		print_rule(&s->created_by_rule, "", 0, 0);
+		printf("\n");
 	}
 }
 
diff --git a/sbin/pfctl/pfctl.c b/sbin/pfctl/pfctl.c
index f35baf25ec35..c349487ed9d2 100644
--- a/sbin/pfctl/pfctl.c
+++ b/sbin/pfctl/pfctl.c
@@ -1996,6 +1996,9 @@ pfctl_show_states(int dev, const char *iface, int opts)
 	if (iface != NULL)
 		strlcpy(filter.ifname, iface, IFNAMSIZ);
 
+	if (opts & PF_OPT_VERBOSE2)
+		filter.include_rule = true;
+
 	arg.opts = opts;
 	arg.dotitle = opts & PF_OPT_SHOWALL;
 	arg.iface = iface;
diff --git a/sys/netpfil/pf/pf_nl.c b/sys/netpfil/pf/pf_nl.c
index e4ce9e64f637..d1beb7681c21 100644
--- a/sys/netpfil/pf/pf_nl.c
+++ b/sys/netpfil/pf/pf_nl.c
@@ -51,8 +51,11 @@
 #include <netlink/netlink_debug.h>
 _DECLARE_DEBUG(LOG_DEBUG);
 
+static bool nlattr_add_labels(struct nl_writer *nw, int attrtype,
+    const struct pf_krule *r);
+static bool nlattr_add_rule(struct nl_writer *nw, const struct pf_krule *rule);
 static bool nlattr_add_pf_threshold(struct nl_writer *, int,
-    struct pf_kthreshold *);
+    const struct pf_kthreshold *);
 
 struct nl_parsed_state {
 	uint8_t		version;
@@ -63,6 +66,7 @@ struct nl_parsed_state {
 	sa_family_t	af;
 	struct pf_addr	addr;
 	struct pf_addr	mask;
+	bool		include_rule;
 };
 
 #define	_IN(_field)	offsetof(struct genlmsghdr, _field)
@@ -75,6 +79,7 @@ static const struct nlattr_parser nla_p_state[] = {
 	{ .type = PF_ST_PROTO, .off = _OUT(proto), .cb = nlattr_get_uint16 },
 	{ .type = PF_ST_FILTER_ADDR, .off = _OUT(addr), .cb = nlattr_get_in6_addr },
 	{ .type = PF_ST_FILTER_MASK, .off = _OUT(mask), .cb = nlattr_get_in6_addr },
+	{ .type = PF_ST_INCLUDE_RULE, .off = _OUT(include_rule), .cb = nlattr_get_bool },
 };
 static const struct nlfield_parser nlf_p_generic[] = {
 	{ .off_in = _IN(version), .off_out = _OUT(version), .cb = nlf_get_u8 },
@@ -146,8 +151,26 @@ dump_state_key(struct nl_writer *nw, int attr, const struct pf_state_key *key)
 	return (true);
 }
 
+static bool
+nlattr_add_rule_nested(struct nl_writer *nw, int attr, const struct pf_krule *r)
+{
+	int off;
+	bool ret;
+
+	off = nlattr_add_nested(nw, attr);
+	if (off == 0)
+		return (false);
+
+	ret = nlattr_add_rule(nw, r);
+
+	nlattr_set_len(nw, off);
+
+	return (ret);
+}
+
 static int
-dump_state(struct nlpcb *nlp, const struct nlmsghdr *hdr, struct pf_kstate *s,
+dump_state(struct nlpcb *nlp, const struct nlmsghdr *hdr,
+    struct nl_parsed_state *attrs, struct pf_kstate *s,
     struct nl_pstate *npt)
 {
 	struct nl_writer *nw = npt->nw;
@@ -231,6 +254,9 @@ dump_state(struct nlpcb *nlp, const struct nlmsghdr *hdr, struct pf_kstate *s,
 	if (!dump_state_peer(nw, PF_ST_PEER_DST, &s->dst))
 		goto enomem;
 
+	if (attrs->include_rule && s->rule != NULL)
+		nlattr_add_rule_nested(nw, PF_ST_CREATED_BY_RULE, s->rule);
+
 	if (nlmsg_end(nw))
 		return (0);
 
@@ -282,7 +308,7 @@ handle_dumpstates(struct nlpcb *nlp, struct nl_parsed_state *attrs,
 			    &attrs->mask, &attrs->addr, af))
 				continue;
 
-			error = dump_state(nlp, hdr, s, npt);
+			error = dump_state(nlp, hdr, attrs, s, npt);
 			if (error != 0)
 				break;
 		}
@@ -307,7 +333,7 @@ handle_getstate(struct nlpcb *nlp, struct nl_parsed_state *attrs,
 	s = pf_find_state_byid(attrs->id, attrs->creatorid);
 	if (s == NULL)
 		return (ENOENT);
-	ret = dump_state(nlp, hdr, s, npt);
+	ret = dump_state(nlp, hdr, attrs, s, npt);
 	PF_STATE_UNLOCK(s);
 
 	return (ret);
@@ -465,7 +491,8 @@ NL_DECLARE_ATTR_PARSER(rule_addr_parser, nla_p_ruleaddr);
 #undef _OUT
 
 static bool
-nlattr_add_rule_addr(struct nl_writer *nw, int attrtype, struct pf_rule_addr *r)
+nlattr_add_rule_addr(struct nl_writer *nw, int attrtype,
+    const struct pf_rule_addr *r)
 {
 	struct pf_addr_wrap aw = {0};
 	int off = nlattr_add_nested(nw, attrtype);
@@ -687,7 +714,8 @@ nlattr_get_nested_timeouts(struct nlattr *nla, struct nl_pstate *npt, const void
 }
 
 static bool
-nlattr_add_timeout(struct nl_writer *nw, int attrtype, uint32_t *timeout)
+nlattr_add_timeout(struct nl_writer *nw, int attrtype,
+    const uint32_t *timeout)
 {
 	int off = nlattr_add_nested(nw, attrtype);
 
@@ -875,76 +903,10 @@ out:
 	return (error);
 }
 
-struct nl_parsed_get_rule {
-	char anchor[MAXPATHLEN];
-	uint8_t action;
-	uint32_t nr;
-	uint32_t ticket;
-	uint8_t clear;
-};
-#define	_OUT(_field)	offsetof(struct nl_parsed_get_rule, _field)
-static const struct nlattr_parser nla_p_getrule[] = {
-	{ .type = PF_GR_ANCHOR, .off = _OUT(anchor), .arg = (void *)MAXPATHLEN, .cb = nlattr_get_chara },
-	{ .type = PF_GR_ACTION, .off = _OUT(action), .cb = nlattr_get_uint8 },
-	{ .type = PF_GR_NR, .off = _OUT(nr), .cb = nlattr_get_uint32 },
-	{ .type = PF_GR_TICKET, .off = _OUT(ticket), .cb = nlattr_get_uint32 },
-	{ .type = PF_GR_CLEAR, .off = _OUT(clear), .cb = nlattr_get_uint8 },
-};
-#undef _OUT
-NL_DECLARE_PARSER(getrule_parser, struct genlmsghdr, nlf_p_empty, nla_p_getrule);
-
-static int
-pf_handle_getrule(struct nlmsghdr *hdr, struct nl_pstate *npt)
+static bool
+nlattr_add_rule(struct nl_writer *nw, const struct pf_krule *rule)
 {
-	char				 anchor_call[MAXPATHLEN];
-	struct nl_parsed_get_rule	 attrs = {};
-	struct nl_writer		*nw = npt->nw;
-	struct genlmsghdr		*ghdr_new;
-	struct pf_kruleset		*ruleset;
-	struct pf_krule			*rule;
-	u_int64_t			 src_nodes_total = 0;
-	int				 rs_num;
-	int				 error;
-
-	error = nl_parse_nlmsg(hdr, &getrule_parser, npt, &attrs);
-	if (error != 0)
-		return (error);
-
-	if (!nlmsg_reply(nw, hdr, sizeof(struct genlmsghdr)))
-		return (ENOMEM);
-
-	ghdr_new = nlmsg_reserve_object(nw, struct genlmsghdr);
-	ghdr_new->cmd = PFNL_CMD_GETRULE;
-
-	PF_RULES_WLOCK();
-	ruleset = pf_find_kruleset(attrs.anchor);
-	if (ruleset == NULL) {
-		PF_RULES_WUNLOCK();
-		error = ENOENT;
-		goto out;
-	}
-
-	rs_num = pf_get_ruleset_number(attrs.action);
-	if (rs_num >= PF_RULESET_MAX) {
-		PF_RULES_WUNLOCK();
-		error = EINVAL;
-		goto out;
-	}
-
-	if (attrs.ticket != ruleset->rules[rs_num].active.ticket) {
-		PF_RULES_WUNLOCK();
-		error = EBUSY;
-		goto out;
-	}
-
-	rule = TAILQ_FIRST(ruleset->rules[rs_num].active.ptr);
-	while ((rule != NULL) && (rule->nr != attrs.nr))
-		rule = TAILQ_NEXT(rule, entries);
-	if (rule == NULL) {
-		PF_RULES_WUNLOCK();
-		error = EBUSY;
-		goto out;
-	}
+	u_int64_t src_nodes_total = 0;
 
 	nlattr_add_rule_addr(nw, PF_RT_SRC, &rule->src);
 	nlattr_add_rule_addr(nw, PF_RT_DST, &rule->dst);
@@ -1050,6 +1012,81 @@ pf_handle_getrule(struct nlmsghdr *hdr, struct nl_pstate *npt)
 	nlattr_add_u8(nw, PF_RT_SOURCE_LIMIT, rule->sourcelim.id);
 	nlattr_add_u32(nw, PF_RT_SOURCE_LIMIT_ACTION, rule->sourcelim.limiter_action);
 
+	return (true);
+}
+
+struct nl_parsed_get_rule {
+	char anchor[MAXPATHLEN];
+	uint8_t action;
+	uint32_t nr;
+	uint32_t ticket;
+	uint8_t clear;
+};
+#define	_OUT(_field)	offsetof(struct nl_parsed_get_rule, _field)
+static const struct nlattr_parser nla_p_getrule[] = {
+	{ .type = PF_GR_ANCHOR, .off = _OUT(anchor), .arg = (void *)MAXPATHLEN, .cb = nlattr_get_chara },
+	{ .type = PF_GR_ACTION, .off = _OUT(action), .cb = nlattr_get_uint8 },
+	{ .type = PF_GR_NR, .off = _OUT(nr), .cb = nlattr_get_uint32 },
+	{ .type = PF_GR_TICKET, .off = _OUT(ticket), .cb = nlattr_get_uint32 },
+	{ .type = PF_GR_CLEAR, .off = _OUT(clear), .cb = nlattr_get_uint8 },
+};
+#undef _OUT
+NL_DECLARE_PARSER(getrule_parser, struct genlmsghdr, nlf_p_empty, nla_p_getrule);
+
+static int
+pf_handle_getrule(struct nlmsghdr *hdr, struct nl_pstate *npt)
+{
+	char				 anchor_call[MAXPATHLEN];
+	struct nl_parsed_get_rule	 attrs = {};
+	struct nl_writer		*nw = npt->nw;
+	struct genlmsghdr		*ghdr_new;
+	struct pf_kruleset		*ruleset;
+	struct pf_krule			*rule;
+	int				 rs_num;
+	int				 error;
+
+	error = nl_parse_nlmsg(hdr, &getrule_parser, npt, &attrs);
+	if (error != 0)
+		return (error);
+
+	if (!nlmsg_reply(nw, hdr, sizeof(struct genlmsghdr)))
+		return (ENOMEM);
+
+	ghdr_new = nlmsg_reserve_object(nw, struct genlmsghdr);
+	ghdr_new->cmd = PFNL_CMD_GETRULE;
+
+	PF_RULES_WLOCK();
+	ruleset = pf_find_kruleset(attrs.anchor);
+	if (ruleset == NULL) {
+		PF_RULES_WUNLOCK();
+		error = ENOENT;
+		goto out;
+	}
+
+	rs_num = pf_get_ruleset_number(attrs.action);
+	if (rs_num >= PF_RULESET_MAX) {
+		PF_RULES_WUNLOCK();
+		error = EINVAL;
+		goto out;
+	}
+
+	if (attrs.ticket != ruleset->rules[rs_num].active.ticket) {
+		PF_RULES_WUNLOCK();
+		error = EBUSY;
+		goto out;
+	}
+
+	rule = TAILQ_FIRST(ruleset->rules[rs_num].active.ptr);
+	while ((rule != NULL) && (rule->nr != attrs.nr))
+		rule = TAILQ_NEXT(rule, entries);
+	if (rule == NULL) {
+		PF_RULES_WUNLOCK();
+		error = EBUSY;
+		goto out;
+	}
+
+	nlattr_add_rule(nw, rule);
+
 	error = pf_kanchor_copyout(ruleset, rule, anchor_call, sizeof(anchor_call));
 	MPASS(error == 0);
 
@@ -1729,7 +1766,7 @@ pf_handle_get_ruleset(struct nlmsghdr *hdr, struct nl_pstate *npt)
 
 static bool
 nlattr_add_pf_threshold(struct nl_writer *nw, int attrtype,
-    struct pf_kthreshold *t)
+    const struct pf_kthreshold *t)
 {
 	int	 off = nlattr_add_nested(nw, attrtype);
 	int	 conn_rate_count = 0;
diff --git a/sys/netpfil/pf/pf_nl.h b/sys/netpfil/pf/pf_nl.h
index 6591c707d9a4..4d0186ea86a5 100644
--- a/sys/netpfil/pf/pf_nl.h
+++ b/sys/netpfil/pf/pf_nl.h
@@ -152,6 +152,8 @@ enum pfstate_type_t {
 	PF_ST_RT_IFNAME		= 37, /* string */
 	PF_ST_SRC_NODE_FLAGS	= 38, /* u8 */
 	PF_ST_RT_AF		= 39, /* u8 */
+	PF_ST_INCLUDE_RULE	= 40, /* bool */
+	PF_ST_CREATED_BY_RULE	= 41, /* nested, pf_rule_type_t */
 };
 
 enum pf_addr_type_t {
diff --git a/tests/sys/netpfil/pf/get_state.sh b/tests/sys/netpfil/pf/get_state.sh
index eb2bc854c800..eb2e2f37a15d 100644
--- a/tests/sys/netpfil/pf/get_state.sh
+++ b/tests/sys/netpfil/pf/get_state.sh
@@ -74,7 +74,51 @@ many_cleanup()
 	pft_cleanup
 }
 
+atf_test_case "rule" "cleanup"
+rule_head()
+{
+	atf_set descr 'Test retrieving original state establishing rule'
+	atf_set require.user root
+}
+
+rule_body()
+{
+	pft_init
+
+	epair=$(vnet_mkepair)
+	ifconfig ${epair}a 192.0.2.1/24 up
+
+	vnet_mkjail alcatraz ${epair}b
+	jexec alcatraz ifconfig ${epair}b 192.0.2.2/24 up
+	jexec alcatraz pfctl -e
+
+	pft_set_rules alcatraz \
+		"pass in proto icmp label \"icmplabel\""
+
+	# Establish state
+	atf_check -o ignore ping -c 1 -W 1 192.0.2.2
+
+	# We should see the rule now
+	atf_check -o match:"rule: pass in proto icmp all keep state label \"icmplabel\"" \
+	    -e ignore \
+	    jexec alcatraz pfctl -ss -vv
+
+	pft_set_rules noflush alcatraz \
+	    "pass"
+
+	# Even after the rules changes we should see the original rule
+	atf_check -o match:"rule: pass in proto icmp all keep state label \"icmplabel\"" \
+	    -e ignore \
+	    jexec alcatraz pfctl -ss -vv
+}
+
+rule_cleanup()
+{
+	pft_cleanup
+}
+
 atf_init_test_cases()
 {
 	atf_add_test_case "many"
+	atf_add_test_case "rule"
 }
diff --git a/tests/sys/netpfil/pf/killstate.sh b/tests/sys/netpfil/pf/killstate.sh
index f5925d715e7c..161a8b7668f2 100644
--- a/tests/sys/netpfil/pf/killstate.sh
+++ b/tests/sys/netpfil/pf/killstate.sh
@@ -666,7 +666,7 @@ key_body()
 		--replyif ${epair}a
 
 	# Get the state key
-	key=$(jexec alcatraz pfctl -ss -vvv | awk '/icmp/ { print($2 " " $3 " " $4 " " $5); }')
+	key=$(jexec alcatraz pfctl -ss -vvv | awk '/all icmp/ { print($2 " " $3 " " $4 " " $5); }')
 	bad_key=$(echo ${key} | sed 's/icmp/tcp/')
 
 	# Kill the wrong key