git: a7aad5bbcdf7 - main - pf: convert DIOCXBEGIN, DIOCXROLLBACK, DIOCXCOMMIT to netlink

From: Kristof Provost <kp_at_FreeBSD.org>
Date: Fri, 30 Oct 2026 12:21:37 UTC
The branch main has been updated by kp:

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

commit a7aad5bbcdf7f82b4db8557adfa1a0267d43d06a
Author:     Kristof Provost <kp@FreeBSD.org>
AuthorDate: 2026-10-20 07:34:39 +0000
Commit:     Kristof Provost <kp@FreeBSD.org>
CommitDate: 2026-10-30 12:21:10 +0000

    pf: convert DIOCXBEGIN, DIOCXROLLBACK, DIOCXCOMMIT to netlink
    
    Sponsored by:   Rubicon Communications, LLC ("Netgate")
---
 lib/libpfctl/libpfctl.c    | 200 ++++++++++++++++++++++++++++++++++++++++
 lib/libpfctl/libpfctl.h    |  12 +++
 lib/libsysdecode/netlink.c |  16 ++++
 sbin/pfctl/pfctl.c         |  74 ++++++++-------
 sbin/pfctl/pfctl.h         |   4 -
 sbin/pfctl/pfctl_parser.c  |  37 --------
 sbin/pfctl/pfctl_parser.h  |   4 +-
 sys/net/pfvar.h            |  18 ++++
 sys/netpfil/pf/pf_ioctl.c  | 224 +++++++++++++++++++++++++++++++++++++++++++++
 sys/netpfil/pf/pf_nl.c     | 145 +++++++++++++++++++++++++++++
 sys/netpfil/pf/pf_nl.h     |  15 +++
 11 files changed, 673 insertions(+), 76 deletions(-)

diff --git a/lib/libpfctl/libpfctl.c b/lib/libpfctl/libpfctl.c
index f8ce51e2df18..1522fd1554ab 100644
--- a/lib/libpfctl/libpfctl.c
+++ b/lib/libpfctl/libpfctl.c
@@ -4594,6 +4594,206 @@ out:
 	return (e.error);
 }
 
+struct pfctl_trans_e {
+	int rs_num;
+	char anchor[MAXPATHLEN];
+	uint32_t ticket;
+	TAILQ_ENTRY(pfctl_trans_e) entry;
+};
+
+struct pfctl_trans {
+	struct pfctl_trans_e tmp;
+	TAILQ_HEAD(, pfctl_trans_e) elms;
+};
+
+struct pfctl_trans *
+pfctl_trans_alloc(void)
+{
+	struct pfctl_trans *t = malloc(sizeof(struct pfctl_trans));
+
+	bzero(t, sizeof(*t));
+
+	TAILQ_INIT(&t->elms);
+
+	return (t);
+}
+
+void
+pfctl_trans_free(struct pfctl_trans *t)
+{
+	struct pfctl_trans_e *e, *et;
+
+	TAILQ_FOREACH_SAFE(e, &t->elms, entry, et)
+		free(e);
+
+	free(t);
+}
+
+size_t
+pfctl_trans_size(struct pfctl_trans *t)
+{
+	struct pfctl_trans_e *e;
+	size_t s = 0;
+
+	TAILQ_FOREACH(e, &t->elms, entry)
+		s++;
+
+	return (s);
+}
+
+int
+pfctl_trans_add(struct pfctl_trans *t, int rs_num, const char *anchor)
+{
+	struct pfctl_trans_e *e;
+
+	e = malloc(sizeof(*e));
+	if (e == NULL)
+		return (ENOMEM);
+
+	e->rs_num = rs_num;
+	e->ticket = 0;
+	strlcpy(e->anchor, anchor, sizeof(e->anchor));
+
+	TAILQ_INSERT_TAIL(&t->elms, e, entry);
+
+	return (0);
+}
+
+uint32_t
+pfctl_get_ticket(struct pfctl_trans *t, int rs_num, const char *anchor)
+{
+	struct pfctl_trans_e *e;
+
+	TAILQ_FOREACH(e, &t->elms, entry) {
+		if (e->rs_num != rs_num)
+			continue;
+		if (strcmp(e->anchor, anchor) != 0)
+			continue;
+
+		return (e->ticket);
+	}
+
+	return (-1);
+}
+
+static void
+snl_add_msg_attr_trans_e(struct snl_writer *nw, uint32_t type,
+    const struct pfctl_trans_e *e)
+{
+	int off = snl_add_msg_attr_nested(nw, type);
+
+	snl_add_msg_attr_u32(nw, PF_TE_RULESET_NUM, e->rs_num);
+	snl_add_msg_attr_string(nw, PF_TE_ANCHOR, e->anchor);
+	snl_add_msg_attr_u32(nw, PF_TE_TICKET, e->ticket);
+
+	snl_end_attr_nested(nw, off);
+}
+
+#define _OUT(_field)	offsetof(struct pfctl_trans_e, _field)
+static const struct snl_attr_parser ap_trans_e[] = {
+	{ .type = PF_TE_RULESET_NUM, .off = _OUT(rs_num), .cb = snl_attr_get_uint32 },
+	{ .type = PF_TE_ANCHOR, .off = _OUT(anchor), .arg_u32 = MAXPATHLEN, .cb = snl_attr_copy_string },
+	{ .type = PF_TE_TICKET, .off = _OUT(ticket), .cb = snl_attr_get_uint32 },
+};
+#undef _OUT
+
+static bool
+_pfctl_trans_e_post(struct snl_state *ss, void *target)
+{
+	struct pfctl_trans *t = (struct pfctl_trans *)target;
+	struct pfctl_trans_e *e;
+
+	(void)ss;
+
+	TAILQ_FOREACH(e, &t->elms, entry) {
+		if (e->rs_num != t->tmp.rs_num)
+			continue;
+		if (strcmp(e->anchor, t->tmp.anchor) != 0)
+			continue;
+		e->ticket = t->tmp.ticket;
+		return (true);
+	}
+
+	e = malloc(sizeof(*e));
+	if (! e)
+		return (false);
+
+	memcpy(e, &t->tmp, sizeof(*e));
+
+	TAILQ_INSERT_TAIL(&t->elms, e, entry);
+
+	return (true);
+}
+
+SNL_DECLARE_ATTR_PARSER_EXT(trans_e_parser, 0, ap_trans_e, _pfctl_trans_e_post);
+
+static struct snl_attr_parser ap_trans[] = {
+	{ .type = PF_TR_TRANS, .arg = &trans_e_parser, .cb = snl_attr_get_nested },
+};
+SNL_DECLARE_PARSER(trans_parser, struct genlmsghdr, snl_f_p_empty, ap_trans);
+
+static int
+_pfctl_trans(struct pfctl_handle *h, struct pfctl_trans *t, size_t off, int cmd)
+{
+	struct snl_writer nw;
+	struct snl_errmsg_data e = {};
+	struct nlmsghdr *hdr;
+	struct pfctl_trans_e *te;
+	uint32_t seq_id;
+
+	snl_init_writer(&h->ss, &nw);
+	hdr = snl_create_genl_msg_request(&nw, h->family_id,
+	    cmd);
+
+	snl_add_msg_attr_u32(&nw, PF_TR_UNSPEC, 0);
+	TAILQ_FOREACH(te, &t->elms, entry) {
+		if (off) {
+			off--;
+			continue;
+		}
+
+		snl_add_msg_attr_trans_e(&nw, PF_TR_TRANS, te);
+	}
+
+	if ((hdr = snl_finalize_msg(&nw)) == NULL) {
+		e.error = ENXIO;
+		goto out;
+	}
+	seq_id = hdr->nlmsg_seq;
+
+	if (! snl_send_message(&h->ss, hdr)) {
+		e.error = ENXIO;
+		goto out;
+	}
+
+	while ((hdr = snl_read_reply_multi(&h->ss, seq_id, &e)) != NULL) {
+		if (! snl_parse_nlmsg(&h->ss, hdr, &trans_parser, t))
+			continue;
+	}
+
+out:
+	snl_clear_lb(&h->ss);
+	return (e.error);
+}
+
+int
+pfctl_trans_begin(struct pfctl_handle *h, struct pfctl_trans *t, size_t off)
+{
+	return (_pfctl_trans(h, t, off, PFNL_CMD_BEGIN));
+}
+
+int
+pfctl_trans_commit(struct pfctl_handle *h, struct pfctl_trans *t, size_t off)
+{
+	return (_pfctl_trans(h, t, off, PFNL_CMD_COMMIT));
+}
+
+int
+pfctl_trans_rollback(struct pfctl_handle *h, struct pfctl_trans *t, size_t off)
+{
+	return (_pfctl_trans(h, t, off, PFNL_CMD_ROLLBACK));
+}
+
 static const struct snl_hdr_parser *all_parsers[] = {
 	&begin_addrs_parser,
 	&clear_states_parser,
diff --git a/lib/libpfctl/libpfctl.h b/lib/libpfctl/libpfctl.h
index 0484024bec17..0e56d081e9d7 100644
--- a/lib/libpfctl/libpfctl.h
+++ b/lib/libpfctl/libpfctl.h
@@ -699,4 +699,16 @@ int	pfctl_flush_fingerprints(struct pfctl_handle *h);
 int	pfctl_get_fingerprint(struct pfctl_handle *h, int idx, struct pf_osfp_ioctl *fp);
 int	pfctl_add_fingerprint(struct pfctl_handle *h, struct pf_osfp_ioctl *fp);
 
+struct pfctl_trans;
+
+struct pfctl_trans*	pfctl_trans_alloc(void);
+void			pfctl_trans_free(struct pfctl_trans *t);
+size_t			pfctl_trans_size(struct pfctl_trans *t);
+int			pfctl_trans_add(struct pfctl_trans *t, int rs_num, const char *anchor);
+uint32_t		pfctl_get_ticket(struct pfctl_trans *t, int rs_num, const char *anchor);
+
+int			pfctl_trans_begin(struct pfctl_handle *h, struct pfctl_trans *t, size_t off);
+int			pfctl_trans_commit(struct pfctl_handle *h, struct pfctl_trans *t, size_t off);
+int			pfctl_trans_rollback(struct pfctl_handle *h, struct pfctl_trans *t, size_t off);
+
 #endif
diff --git a/lib/libsysdecode/netlink.c b/lib/libsysdecode/netlink.c
index 5a7021b7b22b..ef13e213dcca 100644
--- a/lib/libsysdecode/netlink.c
+++ b/lib/libsysdecode/netlink.c
@@ -347,6 +347,18 @@ static const struct nlattr_decoder nla_d_ruleset[] = {
 };
 NL_DECLARE_ATTR_DECODER(ruleset_decoder, nla_d_ruleset);
 
+static const struct nlattr_decoder nla_d_trans_e[] = {
+	{ .type = PF_TE_RULESET_NUM, .attr_name = "rs_num", .cb = nlattr_decode_uint32 },
+	{ .type = PF_TE_ANCHOR, .attr_name="anchor", .cb = nlattr_decode_string },
+	{ .type = PF_TE_TICKET, .attr_name = "ticket", .cb = nlattr_decode_uint32 },
+};
+NL_DECLARE_ATTR_DECODER(trans_e_decoder, nla_d_trans_e);
+
+static const struct nlattr_decoder nla_d_trans[] = {
+	{ .type = PF_TR_TRANS, .attr_name = "trans", .cb = nlattr_decode_nested, .args = &trans_e_decoder },
+};
+NL_DECLARE_ATTR_DECODER(trans_decoder, nla_d_trans);
+
 static inline void
 nl_verify_decoders(const struct nlattr_decoder_set **decoder, size_t count)
 {
@@ -368,6 +380,7 @@ static const struct nlattr_decoder_set *all_decoders[] = {
 	&rule_addr_decoder,
 	&killclear_states_decoder,
 	&ruleset_decoder,
+	&trans_decoder,
 };
 
 static const struct pfnl_cmd_decoder cmd_decoder[] = {
@@ -378,6 +391,9 @@ static const struct pfnl_cmd_decoder cmd_decoder[] = {
 	{ .cmd_num = PFNL_CMD_GET_ADDR, .ds = &addr_decoder },
 	{ .cmd_num = PFNL_CMD_GET_RULESETS, .ds = &ruleset_decoder },
 	{ .cmd_num = PFNL_CMD_GET_RULESET, .ds = &ruleset_decoder },
+	{ .cmd_num = PFNL_CMD_BEGIN, .ds = &trans_decoder },
+	{ .cmd_num = PFNL_CMD_COMMIT, .ds = &trans_decoder },
+	{ .cmd_num = PFNL_CMD_ROLLBACK, .ds = &trans_decoder },
 };
 
 static inline void
diff --git a/sbin/pfctl/pfctl.c b/sbin/pfctl/pfctl.c
index 9ffdea03fea3..a8a870a740a2 100644
--- a/sbin/pfctl/pfctl.c
+++ b/sbin/pfctl/pfctl.c
@@ -620,18 +620,24 @@ pfctl_flush_nat(int dev, int opts, char *anchorname)
 int
 pfctl_clear_altq(int dev, int opts)
 {
-	struct pfr_buffer t;
+	struct pfctl_trans *t;
 
 	if (!altqsupport)
 		return (-1);
-	memset(&t, 0, sizeof(t));
-	t.pfrb_type = PFRB_TRANS;
-	if (pfctl_add_trans(&t, PF_RULESET_ALTQ, "") ||
-	    pfctl_trans(dev, &t, DIOCXBEGIN, 0) ||
-	    pfctl_trans(dev, &t, DIOCXCOMMIT, 0))
+
+	t = pfctl_trans_alloc();
+	if (t == NULL)
+		return (ENOMEM);
+
+	if (pfctl_trans_add(t, PF_RULESET_ALTQ, "") ||
+	    pfctl_trans_begin(pfh, t, 0) ||
+	    pfctl_trans_commit(pfh, t, 0))
 		err(1, "pfctl_clear_altq");
 	if ((opts & PF_OPT_QUIET) == 0)
 		fprintf(stderr, "altq cleared\n");
+
+	pfctl_trans_free(t);
+
 	return (0);
 }
 
@@ -2234,13 +2240,13 @@ int
 pfctl_eth_ruleset_trans(struct pfctl *pf, char *path,
     struct pfctl_eth_anchor *a)
 {
-	int osize = pf->trans->pfrb_size;
+	int osize = pfctl_trans_size(pf->trans);
 
 	if ((pf->loadopt & PFCTL_FLAG_ETH) != 0) {
-		if (pfctl_add_trans(pf->trans, PF_RULESET_ETH, path))
+		if (pfctl_trans_add(pf->trans, PF_RULESET_ETH, path))
 			return (1);
 	}
-	if (pfctl_trans(pf->dev, pf->trans, DIOCXBEGIN, osize))
+	if (pfctl_trans_begin(pf->h, pf->trans, osize))
 		return (5);
 
 	return (0);
@@ -2249,32 +2255,32 @@ pfctl_eth_ruleset_trans(struct pfctl *pf, char *path,
 int
 pfctl_ruleset_trans(struct pfctl *pf, char *path, struct pfctl_anchor *a, bool do_eth)
 {
-	int osize = pf->trans->pfrb_size;
+	int osize = pfctl_trans_size(pf->trans);
 
 	if ((pf->loadopt & PFCTL_FLAG_ETH) != 0 && do_eth) {
-		if (pfctl_add_trans(pf->trans, PF_RULESET_ETH, path))
+		if (pfctl_trans_add(pf->trans, PF_RULESET_ETH, path))
 			return (1);
 	}
 	if ((pf->loadopt & PFCTL_FLAG_NAT) != 0) {
-		if (pfctl_add_trans(pf->trans, PF_RULESET_NAT, path) ||
-		    pfctl_add_trans(pf->trans, PF_RULESET_BINAT, path) ||
-		    pfctl_add_trans(pf->trans, PF_RULESET_RDR, path))
+		if (pfctl_trans_add(pf->trans, PF_RULESET_NAT, path) ||
+		    pfctl_trans_add(pf->trans, PF_RULESET_BINAT, path) ||
+		    pfctl_trans_add(pf->trans, PF_RULESET_RDR, path))
 			return (1);
 	}
 	if (a == pf->astack[0] && ((altqsupport &&
 	    (pf->loadopt & PFCTL_FLAG_ALTQ) != 0))) {
-		if (pfctl_add_trans(pf->trans, PF_RULESET_ALTQ, path))
+		if (pfctl_trans_add(pf->trans, PF_RULESET_ALTQ, path))
 			return (2);
 	}
 	if ((pf->loadopt & PFCTL_FLAG_FILTER) != 0) {
-		if (pfctl_add_trans(pf->trans, PF_RULESET_SCRUB, path) ||
-		    pfctl_add_trans(pf->trans, PF_RULESET_FILTER, path))
+		if (pfctl_trans_add(pf->trans, PF_RULESET_SCRUB, path) ||
+		    pfctl_trans_add(pf->trans, PF_RULESET_FILTER, path))
 			return (3);
 	}
 	if (pf->loadopt & PFCTL_FLAG_TABLE)
-		if (pfctl_add_trans(pf->trans, PF_RULESET_TABLE, path))
+		if (pfctl_trans_add(pf->trans, PF_RULESET_TABLE, path))
 			return (4);
-	if (pfctl_trans(pf->dev, pf->trans, DIOCXBEGIN, osize))
+	if (pfctl_trans_begin(pf->h, pf->trans, osize))
 		return (5);
 
 	return (0);
@@ -2657,12 +2663,12 @@ pfctl_add_altq(struct pfctl *pf, struct pf_altq *a)
 
 int
 pfctl_rules(int dev, char *filename, int opts, int optimize,
-    char *anchorname, struct pfr_buffer *trans)
+    char *anchorname, struct pfctl_trans *trans)
 {
 #define ERR(...) do { warn(__VA_ARGS__); goto _error; } while(0)
 #define ERRX(...) do { warnx(__VA_ARGS__); goto _error; } while(0)
 
-	struct pfr_buffer	*t, buf;
+	struct pfctl_trans	*t;
 	struct pfioc_altq	 pa;
 	struct pfctl		 pf;
 	struct pfctl_ruleset	*rs;
@@ -2683,14 +2689,11 @@ pfctl_rules(int dev, char *filename, int opts, int optimize,
 	pf_eth_main_anchor.ruleset.anchor = &pf_eth_main_anchor;
 
 	if (trans == NULL) {
-		bzero(&buf, sizeof(buf));
-		buf.pfrb_type = PFRB_TRANS;
-		pf.trans = &buf;
-		t = &buf;
+		t = pfctl_trans_alloc();
 		osize = 0;
 	} else {
 		t = trans;
-		osize = t->pfrb_size;
+		osize = pfctl_trans_size(t);
 	}
 
 	memset(&pa, 0, sizeof(pa));
@@ -2812,9 +2815,10 @@ pfctl_rules(int dev, char *filename, int opts, int optimize,
 		if ((opts & PF_OPT_NOACTION) == 0) {
 			if (!anchorname[0] && pfctl_load_options(&pf))
 				goto _error;
-			if (pfctl_trans(dev, t, DIOCXCOMMIT, osize))
+			if (pfctl_trans_commit(pfh, t, osize))
 				ERR("DIOCXCOMMIT");
 		}
+		pfctl_trans_free(t);
 	}
 	free(path);
 	return (0);
@@ -2822,7 +2826,7 @@ pfctl_rules(int dev, char *filename, int opts, int optimize,
 _error:
 	if (trans == NULL) {	/* main ruleset */
 		if ((opts & PF_OPT_NOACTION) == 0)
-			if (pfctl_trans(dev, t, DIOCXROLLBACK, osize))
+			if (pfctl_trans_rollback(pfh, t, osize))
 				err(1, "DIOCXROLLBACK");
 		exit(1);
 	} else {		/* sub ruleset */
@@ -3618,7 +3622,7 @@ void
 pfctl_reset(int dev, int opts)
 {
 	struct pfctl pf;
-	struct pfr_buffer t;
+	struct pfctl_trans *t;
 	int i;
 
 	memset(&pf, 0, sizeof(pf));
@@ -3635,9 +3639,11 @@ pfctl_reset(int dev, int opts)
 		err(1, "%s: strdup", __func__);
 	pf.ifname_set = 1;
 
-	memset(&t, 0, sizeof(t));
-	t.pfrb_type = PFRB_TRANS;
-	if (pfctl_trans(dev, &t, DIOCXBEGIN, 0))
+	t = pfctl_trans_alloc();
+	if (t == NULL)
+		err(1, "%s: pfctl_trans_alloc()", __func__);
+
+	if (pfctl_trans_begin(pfh, t, 0))
 		err(1, "%s: DIOCXBEGIN", __func__);
 
 	for (i = 0; pf_limits[i].name; i++)
@@ -3648,9 +3654,11 @@ pfctl_reset(int dev, int opts)
 
 	pfctl_load_options(&pf);
 
-	if (pfctl_trans(dev, &t, DIOCXCOMMIT, 0))
+	if (pfctl_trans_commit(pfh, t, 0))
 		err(1, "%s: DIOCXCOMMIT", __func__);
 
+	pfctl_trans_free(t);
+
 	pfctl_clear_interface_flags(dev, opts);
 }
 
diff --git a/sbin/pfctl/pfctl.h b/sbin/pfctl/pfctl.h
index bcec429cdf78..48d8f096a0aa 100644
--- a/sbin/pfctl/pfctl.h
+++ b/sbin/pfctl/pfctl.h
@@ -170,10 +170,6 @@ void	 print_seq(struct pfctl_state_peer *);
 void	 print_state(struct pfctl_state *, int);
 
 int	 pfctl_cmdline_symset(char *);
-int	 pfctl_add_trans(struct pfr_buffer *, int, const char *);
-u_int32_t
-	 pfctl_get_ticket(struct pfr_buffer *, int, const char *);
-int	 pfctl_trans(int, struct pfr_buffer *, u_long, int);
 
 int	 pf_get_ruleset_number(u_int8_t);
 void	 pf_init_ruleset(struct pfctl_ruleset *);
diff --git a/sbin/pfctl/pfctl_parser.c b/sbin/pfctl/pfctl_parser.c
index 46e8f81d0693..3748b25669aa 100644
--- a/sbin/pfctl/pfctl_parser.c
+++ b/sbin/pfctl/pfctl_parser.c
@@ -2131,40 +2131,3 @@ append_addr_host(struct pfr_buffer *b, struct node_host *n, int test, int not)
 
 	return (0);
 }
-
-int
-pfctl_add_trans(struct pfr_buffer *buf, int rs_num, const char *anchor)
-{
-	struct pfioc_trans_e trans;
-
-	bzero(&trans, sizeof(trans));
-	trans.rs_num = rs_num;
-	if (strlcpy(trans.anchor, anchor,
-	    sizeof(trans.anchor)) >= sizeof(trans.anchor))
-		errx(1, "pfctl_add_trans: strlcpy");
-
-	return pfr_buf_add(buf, &trans);
-}
-
-u_int32_t
-pfctl_get_ticket(struct pfr_buffer *buf, int rs_num, const char *anchor)
-{
-	struct pfioc_trans_e *p;
-
-	PFRB_FOREACH(p, buf)
-		if (rs_num == p->rs_num && !strcmp(anchor, p->anchor))
-			return (p->ticket);
-	errx(1, "pfctl_get_ticket: assertion failed");
-}
-
-int
-pfctl_trans(int dev, struct pfr_buffer *buf, u_long cmd, int from)
-{
-	struct pfioc_trans trans;
-
-	bzero(&trans, sizeof(trans));
-	trans.size = buf->pfrb_size - from;
-	trans.esize = sizeof(struct pfioc_trans_e);
-	trans.array = ((struct pfioc_trans_e *)buf->pfrb_caddr) + from;
-	return ioctl(dev, cmd, &trans);
-}
diff --git a/sbin/pfctl/pfctl_parser.h b/sbin/pfctl/pfctl_parser.h
index 5b2ff95f88c9..6c01d72d78c9 100644
--- a/sbin/pfctl/pfctl_parser.h
+++ b/sbin/pfctl/pfctl_parser.h
@@ -107,7 +107,7 @@ struct pfctl {
 	struct pfioc_pooladdr paddr;
 	struct pfioc_altq *paltq;
 	struct pfioc_queue *pqueue;
-	struct pfr_buffer *trans;
+	struct pfctl_trans *trans;
 	struct pfctl_anchor *anchor, *alast;
 	struct pfr_ktablehead pfr_ktlast;
 	int eth_nr;
@@ -306,7 +306,7 @@ struct pfr_uktable;
 
 void	copy_satopfaddr(struct pf_addr *, struct sockaddr *);
 
-int	pfctl_rules(int, char *, int, int, char *, struct pfr_buffer *);
+int	pfctl_rules(int, char *, int, int, char *, struct pfctl_trans *);
 int	pfctl_optimize_ruleset(struct pfctl *, struct pfctl_ruleset *);
 
 void	pfctl_init_rule(struct pfctl_rule *r);
diff --git a/sys/net/pfvar.h b/sys/net/pfvar.h
index 5b4d87a22301..ba2bb8bf5bce 100644
--- a/sys/net/pfvar.h
+++ b/sys/net/pfvar.h
@@ -3186,6 +3186,24 @@ int	  inet_nat46(int, const void *, void *, const void *, u_int8_t);
 int	  inet_nat46_inet(const void *, void *, const void *, u_int8_t);
 int	  inet_nat46_inet6(const void *, void *, const void *, u_int8_t);
 
+struct pf_trans_e {
+	int rs_num;
+	char anchor[MAXPATHLEN];
+	uint32_t ticket;
+	TAILQ_ENTRY(pf_trans_e) entry;
+};
+
+struct pf_trans {
+	struct pf_trans_e tmp;
+	TAILQ_HEAD(, pf_trans_e) elms;
+	size_t count;
+};
+
+int	 pf_begin(struct pf_trans *);
+int	 pf_commit(struct pf_trans *);
+int	 pf_rollback(struct pf_trans *);
+void	 pf_trans_free(struct pf_trans *);
+
 #endif /* _KERNEL */
 
 #endif /* _NET_PFVAR_H_ */
diff --git a/sys/netpfil/pf/pf_ioctl.c b/sys/netpfil/pf/pf_ioctl.c
index 55a5c38e1301..464a89a40b9a 100644
--- a/sys/netpfil/pf/pf_ioctl.c
+++ b/sys/netpfil/pf/pf_ioctl.c
@@ -3840,6 +3840,230 @@ pf_ioctl_natlook(struct pfioc_natlook *pnl)
 	return (0);
 }
 
+int
+pf_begin(struct pf_trans *t)
+{
+	struct pf_trans_e *e;
+	int error = 0;
+
+	PF_RULES_WLOCK();
+
+	TAILQ_FOREACH(e, &t->elms, entry) {
+		switch (e->rs_num) {
+		case PF_RULESET_ETH:
+			error = pf_begin_eth(&e->ticket, e->anchor);
+			if (error)
+				goto out;
+			break;
+#ifdef ALTQ
+		case PF_RULESET_ALTQ:
+			if (e->anchor[0]) {
+				error = EINVAL;
+				goto out;
+			}
+			if ((error = pf_begin_altq(&e->ticket)))
+				goto out;
+			break;
+#endif
+		case PF_RULESET_TABLE: {
+			struct pfr_table table;
+
+			bzero(&table, sizeof(table));
+			strlcpy(table.pfrt_anchor, e->anchor,
+			    sizeof(table.pfrt_anchor));
+			if ((error = pfr_ina_begin(&table,
+			    &e->ticket, NULL, 0)))
+				goto out;
+			break;
+		}
+		default:
+			if ((error = pf_begin_rules(&e->ticket, e->rs_num,
+			    e->anchor)))
+				goto out;
+			break;
+		}
+	}
+
+out:
+	PF_RULES_WUNLOCK();
+
+	return (error);
+}
+
+int
+pf_commit(struct pf_trans *t)
+{
+	struct pf_trans_e	*e;
+	struct pf_kruleset	*rs;
+	struct pf_keth_ruleset	*ers;
+	int			 error = 0;
+
+	PF_RULES_WLOCK();
+
+	/* First makes sure everything will succeed. */
+	TAILQ_FOREACH(e, &t->elms, entry) {
+		switch (e->rs_num) {
+		case PF_RULESET_ETH:
+			ers = pf_find_keth_ruleset(e->anchor);
+			if (ers == NULL || e->ticket == 0 ||
+			    e->ticket != ers->inactive.ticket) {
+				error = EINVAL;
+				goto out;
+			}
+			break;
+#ifdef ALTQ
+		case PF_RULESET_ALTQ:
+			if (e->anchor[0]) {
+				error = EINVAL;
+				goto out;
+			}
+			if (!V_altqs_inactive_open || e->ticket !=
+			    V_ticket_altqs_inactive) {
+				error = EBUSY;
+				goto out;
+			}
+			break;
+#endif /* ALTQ */
+		case PF_RULESET_TABLE:
+			rs = pf_find_kruleset(e->anchor);
+			if (rs == NULL || !rs->topen || e->ticket !=
+			    rs->tticket) {
+				error = EBUSY;
+				goto out;
+			}
+			break;
+		default:
+			if (e->rs_num < 0 || e->rs_num >=
+			    PF_RULESET_MAX) {
+				error = EINVAL;
+				goto out;
+			}
+			rs = pf_find_kruleset(e->anchor);
+			if (rs == NULL ||
+			    !rs->rules[e->rs_num].inactive.open ||
+			    rs->rules[e->rs_num].inactive.ticket !=
+			    e->ticket) {
+				error = EBUSY;
+				goto out;
+			}
+			break;
+		}
+	}
+
+	/* Now do the commit - no errors should happen here. */
+	TAILQ_FOREACH(e, &t->elms, entry) {
+		switch (e->rs_num) {
+		case PF_RULESET_ETH:
+			if ((error = pf_commit_eth(e->ticket, e->anchor))) {
+				goto out; /* really bad */
+			}
+			break;
+#ifdef ALTQ
+		case PF_RULESET_ALTQ:
+			if ((error = pf_commit_altq(e->ticket))) {
+				goto out; /* really bad */
+			}
+			break;
+#endif /* ALTQ */
+		case PF_RULESET_TABLE:
+		    {
+			struct pfr_table table;
+
+			bzero(&table, sizeof(table));
+			(void)strlcpy(table.pfrt_anchor, e->anchor,
+			    sizeof(table.pfrt_anchor));
+			if ((error = pfr_ina_commit(&table,
+			    e->ticket, NULL, NULL, 0))) {
+				goto out; /* really bad */
+			}
+			break;
+		    }
+		default:
+			if ((error = pf_commit_rules(e->ticket,
+			    e->rs_num, e->anchor))) {
+				goto out; /* really bad */
+			}
+			break;
+		}
+		}
+
+out:
+	PF_RULES_WUNLOCK();
+
+	/* Only hook into EtherNet taffic if we've got rules for it. */
+	if (! TAILQ_EMPTY(V_pf_keth->active.rules))
+		hook_pf_eth();
+	else
+		dehook_pf_eth();
+
+	return (error);
+}
+
+int
+pf_rollback(struct pf_trans *t)
+{
+	struct pf_trans_e	*e;
+	int			 error = 0;
+
+	PF_RULES_WLOCK();
+	TAILQ_FOREACH(e, &t->elms, entry) {
+		e->anchor[sizeof(e->anchor) - 1] = '\0';
+		switch (e->rs_num) {
+		case PF_RULESET_ETH:
+			if ((error = pf_rollback_eth(e->ticket,
+			    e->anchor))) {
+				goto out; /* really bad */
+			}
+			break;
+#ifdef ALTQ
+		case PF_RULESET_ALTQ:
+			if (e->anchor[0]) {
+				error = EINVAL;
+				goto out;
+			}
+			if ((error = pf_rollback_altq(e->ticket))) {
+				goto out; /* really bad */
+			}
+			break;
+#endif /* ALTQ */
+		case PF_RULESET_TABLE:
+		    {
+			struct pfr_table table;
+
+			bzero(&table, sizeof(table));
+			strlcpy(table.pfrt_anchor, e->anchor,
+			    sizeof(table.pfrt_anchor));
+			if ((error = pfr_ina_rollback(&table,
+			    e->ticket, NULL, 0))) {
+				goto out; /* really bad */
+			}
+			break;
+		    }
+		default:
+			if ((error = pf_rollback_rules(e->ticket,
+			    e->rs_num, e->anchor))) {
+				goto out; /* really bad */
+			}
+			break;
+		}
+	}
+
+out:
+	PF_RULES_WUNLOCK();
+
+	return (error);
+}
+
+void
+pf_trans_free(struct pf_trans *t)
+{
+	struct pf_trans_e *e, *tmp;
+
+	TAILQ_FOREACH_SAFE(e, &t->elms, entry, tmp) {
+		free(e, M_PF);
+	}
+}
+
 static int
 pfioctl(struct cdev *dev, u_long cmd, caddr_t addr, int flags, struct thread *td)
 {
diff --git a/sys/netpfil/pf/pf_nl.c b/sys/netpfil/pf/pf_nl.c
index de7994b314c4..040d85e3c1cb 100644
--- a/sys/netpfil/pf/pf_nl.c
+++ b/sys/netpfil/pf/pf_nl.c
@@ -3088,6 +3088,126 @@ pf_handle_osfp_add(struct nlmsghdr *hdr, struct nl_pstate *npt)
 	return (0);
 }
 
+#define _OUT(_field)	offsetof(struct pf_trans_e, _field)
+static const struct nlattr_parser nla_p_trans_e[] = {
+	{ .type = PF_TE_RULESET_NUM, .off = _OUT(rs_num), .cb = nlattr_get_uint32 },
+	{ .type = PF_TE_ANCHOR, .off = _OUT(anchor), .arg = (void *)MAXPATHLEN, .cb = nlattr_get_chara },
+	{ .type = PF_TE_TICKET, .off = _OUT(ticket), .cb = nlattr_get_uint32 },
+};
+#undef _OUT
+static bool
+nl_trans_e_post_parse(void *parsed_attrs, struct nl_pstate *npt)
+{
+	struct pf_trans *attrs = (struct pf_trans *)parsed_attrs;
+	struct pf_trans_e *e;
+
+	e = malloc(sizeof (*e), M_PF, M_WAITOK);
+
+	memcpy(e, &attrs->tmp, sizeof(*e));
+
+	TAILQ_INSERT_TAIL(&attrs->elms, e, entry);
+
+	/* Limit number of entries we accept. */
+	attrs->count++;
+	if (attrs->count > pf_ioctl_maxcount) {
+		return (false);
+	}
+
+	return (true);
+}
+
+NL_DECLARE_ATTR_PARSER_EXT(trans_e_parser, nla_p_trans_e, nl_trans_e_post_parse);
+
+static const struct nlattr_parser nla_p_trans[] = {
+	{ .type = PF_TR_TRANS, .arg = &trans_e_parser, .cb = nlattr_get_nested },
+};
+NL_DECLARE_PARSER(trans_parser, struct genlmsghdr, nlf_p_empty, nla_p_trans);
+
+static bool
+nlattr_add_trans(struct nl_writer *nw, int attr,
+    const struct pf_trans_e *e)
+{
+	int off = nlattr_add_nested(nw, attr);
+	if (off == 0)
+		return (false);
+
+	nlattr_add_u32(nw, PF_TE_RULESET_NUM, e->rs_num);
+	nlattr_add_string(nw, PF_TE_ANCHOR, e->anchor);
+	nlattr_add_u32(nw, PF_TE_TICKET, e->ticket);
+
+	nlattr_set_len(nw, off);
+
+	return (true);
+}
+
+static int
+pf_handle_trans(struct nlmsghdr *hdr, struct nl_pstate *npt, int cmd)
+{
+	struct pf_trans attrs = { 0 };
+	struct pf_trans_e *e;
+	struct nl_writer *nw = npt->nw;
+	struct genlmsghdr *ghdr_new;
+	int error = 0;
+
+	TAILQ_INIT(&attrs.elms);
+
+	error = nl_parse_nlmsg(hdr, &trans_parser, npt, &attrs);
+	if (error != 0)
+		goto out;
+
+	switch (cmd) {
+	case PFNL_CMD_BEGIN:
+		error = pf_begin(&attrs);
+		break;
+	case PFNL_CMD_COMMIT:
+		error = pf_commit(&attrs);
+		break;
+	case PFNL_CMD_ROLLBACK:
+		error = pf_rollback(&attrs);
+		break;
+	default:
+		panic("Unexpected command %d", cmd);
+	}
+
+	if (! nlmsg_reply(nw, hdr, sizeof(struct genlmsghdr)))
+		return (ENOMEM);
+
+	ghdr_new = nlmsg_reserve_object(nw, struct genlmsghdr);
+	ghdr_new->cmd = cmd;
+
+	// Send the transaction info back, plus ticket info
+	nlattr_add_u32(nw, PF_TR_UNSPEC, 0);
+	TAILQ_FOREACH(e, &attrs.elms, entry) {
+		nlattr_add_trans(nw, PF_TR_TRANS, e);
+	}
+
+	if (! nlmsg_end(nw))
+		return (ENOMEM);
+
+out:
+	pf_trans_free(&attrs);
+
+	return (error);
+}
+
+static int
+pf_handle_begin(struct nlmsghdr *hdr, struct nl_pstate *npt)
+{
+	return (pf_handle_trans(hdr, npt, PFNL_CMD_BEGIN));
+}
+
+static int
+pf_handle_rollback(struct nlmsghdr *hdr, struct nl_pstate *npt)
+{
+	return (pf_handle_trans(hdr, npt, PFNL_CMD_ROLLBACK));
+}
+
+static int
+pf_handle_commit(struct nlmsghdr *hdr, struct nl_pstate *npt)
+{
+	return (pf_handle_trans(hdr, npt, PFNL_CMD_COMMIT));
+}
+
 static const struct nlhdr_parser *all_parsers[] = {
 	&state_parser,
 	&addrule_parser,
@@ -3104,6 +3224,7 @@ static const struct nlhdr_parser *all_parsers[] = {
 	&table_parser,
 	&table_addr_parser,
 	&table_astats_parser,
+	&trans_parser,
 	&state_limiter_parser,
 	&source_limiter_parser,
 	&source_parser,
@@ -3523,6 +3644,30 @@ static const struct genl_cmd pf_cmds[] = {
 		.cmd_priv = PRIV_NETINET_PF,
 		.cmd_securelevel = 3,
 	},
+	{
+		.cmd_num = PFNL_CMD_BEGIN,
+		.cmd_name = "BEGIN",
+		.cmd_cb = pf_handle_begin,
+		.cmd_flags = GENL_CMD_CAP_DO | GENL_CMD_CAP_HASPOL,
+		.cmd_priv = PRIV_NETINET_PF,
+		.cmd_securelevel = 3,
+	},
+	{
+		.cmd_num = PFNL_CMD_ROLLBACK,
+		.cmd_name = "ROLLBACK",
+		.cmd_cb = pf_handle_rollback,
+		.cmd_flags = GENL_CMD_CAP_DO | GENL_CMD_CAP_HASPOL,
*** 47 LINES SKIPPED ***