git: 2b29ceb86f50 - main - pfctl: Print Ethernet rules

From: Kristof Provost <kp_at_FreeBSD.org>
Date: Wed, 02 Mar 2022 16:00:42 UTC
The branch main has been updated by kp:

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

commit 2b29ceb86f509dfaf34c3b7f790776c345915dba
Author:     Kristof Provost <kp@FreeBSD.org>
AuthorDate: 2021-02-04 12:19:12 +0000
Commit:     Kristof Provost <kp@FreeBSD.org>
CommitDate: 2022-03-02 16:00:03 +0000

    pfctl: Print Ethernet rules
    
    Extent pfctl to be able to read configured Ethernet filtering rules from
    the kernel and print them.
    
    Sponsored by:   Rubicon Communications, LLC ("Netgate")
    Differential Revision:  https://reviews.freebsd.org/D31738
---
 lib/libpfctl/libpfctl.c   | 179 ++++++++++++++++++++++++++++++++++++++
 lib/libpfctl/libpfctl.h   |  42 +++++++++
 sbin/pfctl/parse.y        | 216 +++++++++++++++++++++++++++++++++++++++++++++-
 sbin/pfctl/pfctl.c        |  89 ++++++++++++++++++-
 sbin/pfctl/pfctl_parser.c |  44 ++++++++++
 sbin/pfctl/pfctl_parser.h |   5 ++
 6 files changed, 568 insertions(+), 7 deletions(-)

diff --git a/lib/libpfctl/libpfctl.c b/lib/libpfctl/libpfctl.c
index 66f7f61cc1fa..e158faf317c1 100644
--- a/lib/libpfctl/libpfctl.c
+++ b/lib/libpfctl/libpfctl.c
@@ -546,6 +546,185 @@ pf_nvrule_to_rule(const nvlist_t *nvl, struct pfctl_rule *rule)
 	rule->src_nodes = nvlist_get_number(nvl, "src_nodes");
 }
 
+static void
+pfctl_nveth_addr_to_eth_addr(const nvlist_t *nvl, struct pfctl_eth_addr *addr)
+{
+	size_t len;
+	const void *data;
+
+	data = nvlist_get_binary(nvl, "addr", &len);
+	assert(len == sizeof(addr->addr));
+	memcpy(addr->addr, data, sizeof(addr->addr));
+
+	addr->neg = nvlist_get_bool(nvl, "neg");
+}
+
+static nvlist_t *
+pfctl_eth_addr_to_nveth_addr(const struct pfctl_eth_addr *addr)
+{
+	nvlist_t *nvl;
+
+	nvl = nvlist_create(0);
+	if (nvl == NULL)
+		return (NULL);
+
+	nvlist_add_bool(nvl, "neg", addr->neg);
+	nvlist_add_binary(nvl, "addr", &addr->addr, ETHER_ADDR_LEN);
+
+	return (nvl);
+}
+
+static void
+pfctl_nveth_rule_to_eth_rule(const nvlist_t *nvl, struct pfctl_eth_rule *rule)
+{
+	rule->nr = nvlist_get_number(nvl, "nr");
+	rule->quick = nvlist_get_bool(nvl, "quick");
+	strlcpy(rule->ifname, nvlist_get_string(nvl, "ifname"), IFNAMSIZ);
+	rule->ifnot = nvlist_get_bool(nvl, "ifnot");
+	rule->direction = nvlist_get_number(nvl, "direction");
+	rule->proto = nvlist_get_number(nvl, "proto");
+
+	pfctl_nveth_addr_to_eth_addr(nvlist_get_nvlist(nvl, "src"),
+	    &rule->src);
+	pfctl_nveth_addr_to_eth_addr(nvlist_get_nvlist(nvl, "dst"),
+	    &rule->dst);
+
+	rule->evaluations = nvlist_get_number(nvl, "evaluations");
+	rule->packets[0] = nvlist_get_number(nvl, "packets-in");
+	rule->packets[1] = nvlist_get_number(nvl, "packets-out");
+	rule->bytes[0] = nvlist_get_number(nvl, "bytes-in");
+	rule->bytes[1] = nvlist_get_number(nvl, "bytes-out");
+
+	strlcpy(rule->qname, nvlist_get_string(nvl, "qname"), PF_QNAME_SIZE);
+	strlcpy(rule->tagname, nvlist_get_string(nvl, "tagname"),
+	    PF_TAG_NAME_SIZE);
+
+	rule->action = nvlist_get_number(nvl, "action");
+}
+
+int
+pfctl_get_eth_rules_info(int dev, struct pfctl_eth_rules_info *rules)
+{
+	uint8_t buf[1024];
+	struct pfioc_nv nv;
+	nvlist_t *nvl;
+
+	bzero(rules, sizeof(*rules));
+
+	nv.data = buf;
+	nv.len = nv.size = sizeof(buf);
+
+	if (ioctl(dev, DIOCGETETHRULES, &nv) != 0)
+		return (errno);
+
+	nvl = nvlist_unpack(buf, nv.len, 0);
+	if (nvl == NULL)
+		return (EIO);
+
+	rules->nr = nvlist_get_number(nvl, "nr");
+	rules->ticket = nvlist_get_number(nvl, "ticket");
+
+	nvlist_destroy(nvl);
+	return (0);
+}
+
+int
+pfctl_get_eth_rule(int dev, uint32_t nr, uint32_t ticket,
+    struct pfctl_eth_rule *rule, bool clear)
+{
+	uint8_t buf[1024];
+	struct pfioc_nv nv;
+	nvlist_t *nvl;
+	void *data;
+	size_t len;
+
+	nvl = nvlist_create(0);
+
+	nvlist_add_number(nvl, "ticket", ticket);
+	nvlist_add_number(nvl, "nr", nr);
+	nvlist_add_bool(nvl, "clear", clear);
+
+	data = nvlist_pack(nvl, &len);
+	nv.data = buf;
+	memcpy(buf, data, len);
+	free(data);
+	nv.len = len;
+	nv.size = sizeof(buf);
+	if (ioctl(dev, DIOCGETETHRULE, &nv)) {
+		nvlist_destroy(nvl);
+		return (errno);
+	}
+	nvlist_destroy(nvl);
+
+	nvl = nvlist_unpack(buf, nv.len, 0);
+	if (nvl == NULL) {
+		return (EIO);
+	}
+
+	pfctl_nveth_rule_to_eth_rule(nvl, rule);
+
+	nvlist_destroy(nvl);
+	return (0);
+}
+
+int
+pfctl_add_eth_rule(int dev, const struct pfctl_eth_rule *r, uint32_t ticket)
+{
+	struct pfioc_nv nv;
+	nvlist_t *nvl, *addr;
+	void *packed;
+	int error;
+	size_t size;
+
+	nvl = nvlist_create(0);
+
+	nvlist_add_number(nvl, "ticket", ticket);
+
+	nvlist_add_number(nvl, "nr", r->nr);
+	nvlist_add_bool(nvl, "quick", r->quick);
+	nvlist_add_string(nvl, "ifname", r->ifname);
+	nvlist_add_bool(nvl, "ifnot", r->ifnot);
+	nvlist_add_number(nvl, "direction", r->direction);
+	nvlist_add_number(nvl, "proto", r->proto);
+
+	addr = pfctl_eth_addr_to_nveth_addr(&r->src);
+	if (addr == NULL) {
+		nvlist_destroy(nvl);
+		return (ENOMEM);
+	}
+	nvlist_add_nvlist(nvl, "src", addr);
+	nvlist_destroy(addr);
+
+	addr = pfctl_eth_addr_to_nveth_addr(&r->dst);
+	if (addr == NULL) {
+		nvlist_destroy(nvl);
+		return (ENOMEM);
+	}
+	nvlist_add_nvlist(nvl, "dst", addr);
+	nvlist_destroy(addr);
+
+	nvlist_add_string(nvl, "qname", r->qname);
+	nvlist_add_string(nvl, "tagname", r->tagname);
+	nvlist_add_number(nvl, "action", r->action);
+
+	packed = nvlist_pack(nvl, &size);
+	if (packed == NULL) {
+		nvlist_destroy(nvl);
+		return (ENOMEM);
+	}
+
+	nv.len = size;
+	nv.size = size;
+	nv.data = packed;
+
+	error = ioctl(dev, DIOCADDETHRULE, &nv);
+
+	free(packed);
+	nvlist_destroy(nvl);
+
+	return (error);
+}
+
 int
 pfctl_add_rule(int dev, const struct pfctl_rule *r, const char *anchor,
     const char *anchor_call, uint32_t ticket, uint32_t pool_ticket)
diff --git a/lib/libpfctl/libpfctl.h b/lib/libpfctl/libpfctl.h
index 6b516b0a7649..e2b3711f9ffd 100644
--- a/lib/libpfctl/libpfctl.h
+++ b/lib/libpfctl/libpfctl.h
@@ -65,6 +65,43 @@ struct pfctl_status {
 	uint64_t	bcounters[2][2];
 };
 
+struct pfctl_eth_rules_info {
+	uint32_t	nr;
+	uint32_t	ticket;
+};
+
+struct pfctl_eth_addr {
+	uint8_t	addr[ETHER_ADDR_LEN];
+	bool	neg;
+};
+
+struct pfctl_eth_rule {
+	uint32_t		 nr;
+
+	bool			 quick;
+
+	/* Filter */
+	char			 ifname[IFNAMSIZ];
+	uint8_t			 ifnot;
+	uint8_t			 direction;
+	uint16_t		 proto;
+	struct pfctl_eth_addr	 src, dst;
+
+	/* Stats */
+	uint64_t		 evaluations;
+	uint64_t		 packets[2];
+	uint64_t		 bytes[2];
+
+	/* Action */
+	char			 qname[PF_QNAME_SIZE];
+	char			 tagname[PF_TAG_NAME_SIZE];
+	uint8_t			 action;
+
+	TAILQ_ENTRY(pfctl_eth_rule)	 entries;
+};
+
+TAILQ_HEAD(pfctl_eth_rules, pfctl_eth_rule);
+
 struct pfctl_pool {
 	struct pf_palist	 list;
 	struct pf_pooladdr	*cur;
@@ -291,6 +328,11 @@ struct pfctl_syncookies {
 struct pfctl_status* pfctl_get_status(int dev);
 void	pfctl_free_status(struct pfctl_status *status);
 
+int	pfctl_get_eth_rules_info(int dev, struct pfctl_eth_rules_info *rules);
+int	pfctl_get_eth_rule(int dev, uint32_t nr, uint32_t ticket,
+	    struct pfctl_eth_rule *rule, bool clear);
+int	pfctl_add_eth_rule(int dev, const struct pfctl_eth_rule *r,
+	    uint32_t ticket);
 int	pfctl_get_rule(int dev, uint32_t nr, uint32_t ticket,
 	    const char *anchor, uint32_t ruleset, struct pfctl_rule *rule,
 	    char *anchor_call);
diff --git a/sbin/pfctl/parse.y b/sbin/pfctl/parse.y
index f931d1c062b9..1bf9372cd7a6 100644
--- a/sbin/pfctl/parse.y
+++ b/sbin/pfctl/parse.y
@@ -122,12 +122,19 @@ int		 atoul(char *, u_long *);
 enum {
 	PFCTL_STATE_NONE,
 	PFCTL_STATE_OPTION,
+	PFCTL_STATE_ETHER,
 	PFCTL_STATE_SCRUB,
 	PFCTL_STATE_QUEUE,
 	PFCTL_STATE_NAT,
 	PFCTL_STATE_FILTER
 };
 
+struct node_etherproto {
+	u_int16_t		 proto;
+	struct node_etherproto	*next;
+	struct node_etherproto	*tail;
+};
+
 struct node_proto {
 	u_int8_t		 proto;
 	struct node_proto	*next;
@@ -341,6 +348,8 @@ void		 expand_label_port(const char *, char *, size_t,
 void		 expand_label_proto(const char *, char *, size_t, u_int8_t);
 void		 expand_label_nr(const char *, char *, size_t,
 		    struct pfctl_rule *);
+void		 expand_eth_rule(struct pfctl_eth_rule *,
+		    struct node_if *, struct node_etherproto *);
 void		 expand_rule(struct pfctl_rule *, struct node_if *,
 		    struct node_host *, struct node_proto *, struct node_os *,
 		    struct node_host *, struct node_port *, struct node_host *,
@@ -396,6 +405,7 @@ typedef struct {
 		}			 range;
 		struct node_if		*interface;
 		struct node_proto	*proto;
+		struct node_etherproto	*etherproto;
 		struct node_icmp	*icmp;
 		struct node_host	*host;
 		struct node_os		*os;
@@ -408,6 +418,17 @@ typedef struct {
 			struct peer	 src, dst;
 			struct node_os	*src_os;
 		}			 fromto;
+		struct {
+			u_int8_t	 src[ETHER_ADDR_LEN];
+			u_int8_t	 srcneg;
+			u_int8_t	 dst[ETHER_ADDR_LEN];
+			u_int8_t	 dstneg;
+		}			 etherfromto;
+		u_int8_t		 mac[ETHER_ADDR_LEN];
+		struct {
+			uint8_t		 mac[ETHER_ADDR_LEN];
+			u_int8_t	 neg;
+		} etheraddr;
 		struct {
 			struct node_host	*host;
 			u_int8_t		 rt;
@@ -470,6 +491,7 @@ int	parseport(char *, struct range *r, int);
 %token	SET OPTIMIZATION TIMEOUT LIMIT LOGINTERFACE BLOCKPOLICY FAILPOLICY
 %token	RANDOMID REQUIREORDER SYNPROXY FINGERPRINTS NOSYNC DEBUG SKIP HOSTID
 %token	ANTISPOOF FOR INCLUDE KEEPCOUNTERS SYNCOOKIES
+%token	ETHER
 %token	BITMASK RANDOM SOURCEHASH ROUNDROBIN STATICPORT PROBABILITY MAPEPORTSET
 %token	ALTQ CBQ CODEL PRIQ HFSC FAIRQ BANDWIDTH TBRSIZE LINKSHARE REALTIME
 %token	UPPERLIMIT QUEUE PRIORITY QLIMIT HOGS BUCKETS RTABLE TARGET INTERVAL
@@ -488,6 +510,7 @@ int	parseport(char *, struct range *r, int);
 %type	<v.probability>		probability
 %type	<v.i>			no dir af fragcache optimizer syncookie_val
 %type	<v.i>			sourcetrack flush unaryop statelock
+%type	<v.i>			etherprotoval
 %type	<v.b>			action nataction natpasslog scrubaction
 %type	<v.b>			flags flag blockspec prio
 %type	<v.range>		portplain portstar portrange
@@ -515,7 +538,7 @@ int	parseport(char *, struct range *r, int);
 %type	<v.state_opt>		state_opt_spec state_opt_list state_opt_item
 %type	<v.logquick>		logquick quick log logopts logopt
 %type	<v.interface>		antispoof_ifspc antispoof_iflst antispoof_if
-%type	<v.qassign>		qname
+%type	<v.qassign>		qname etherqname
 %type	<v.queue>		qassign qassign_list qassign_item
 %type	<v.queue_options>	scheduler
 %type	<v.number>		cbqflags_list cbqflags_item
@@ -524,7 +547,7 @@ int	parseport(char *, struct range *r, int);
 %type	<v.fairq_opts>		fairqopts_list fairqopts_item fairq_opts
 %type	<v.codel_opts>		codelopts_list codelopts_item codel_opts
 %type	<v.queue_bwspec>	bandwidth
-%type	<v.filter_opts>		filter_opts filter_opt filter_opts_l
+%type	<v.filter_opts>		filter_opts filter_opt filter_opts_l etherfilter_opts etherfilter_opt etherfilter_opts_l
 %type	<v.filter_opts>		filter_sets filter_set filter_sets_l
 %type	<v.antispoof_opts>	antispoof_opts antispoof_opt antispoof_opts_l
 %type	<v.queue_opts>		queue_opts queue_opt queue_opts_l
@@ -534,12 +557,17 @@ int	parseport(char *, struct range *r, int);
 %type	<v.tagged>		tagged
 %type	<v.rtableid>		rtable
 %type	<v.watermarks>		syncookie_opts
+%type	<v.etherproto>		etherproto etherproto_list etherproto_item
+%type	<v.etherfromto>		etherfromto
+%type	<v.etheraddr>		etherfrom etherto
+%type	<v.mac>			mac
 %%
 
 ruleset		: /* empty */
 		| ruleset include '\n'
 		| ruleset '\n'
 		| ruleset option '\n'
+		| ruleset etherrule '\n'
 		| ruleset scrubrule '\n'
 		| ruleset natrule '\n'
 		| ruleset binatrule '\n'
@@ -1151,6 +1179,58 @@ scrubaction	: no SCRUB {
 		}
 		;
 
+etherrule	: ETHER action dir quick interface etherproto etherfromto etherfilter_opts
+		{
+			struct pfctl_eth_rule	r;
+
+			bzero(&r, sizeof(r));
+
+			if (check_rulestate(PFCTL_STATE_ETHER))
+				YYERROR;
+
+			r.action = $2.b1;
+			r.direction = $3;
+			r.quick = $4.quick;
+			/* XXX TODO: ! support */
+			memcpy(&r.src.addr, $7.src, sizeof(r.src.addr));
+			r.src.neg = $7.srcneg;
+			memcpy(&r.dst.addr, $7.dst, sizeof(r.dst.addr));
+			r.dst.neg = $7.dstneg;
+			if ($8.tag != NULL)
+				memcpy(&r.tagname, $8.tag, sizeof(r.tagname));
+			if ($8.queues.qname != NULL)
+				memcpy(&r.qname, $8.queues.qname, sizeof(r.qname));
+
+			expand_eth_rule(&r, $5, $6);
+		}
+		;
+
+etherfilter_opts	:	{
+				bzero(&filter_opts, sizeof filter_opts);
+			}
+		    etherfilter_opts_l
+			{ $$ = filter_opts; }
+		| /* empty */	{
+			bzero(&filter_opts, sizeof filter_opts);
+			$$ = filter_opts;
+		}
+		;
+
+etherfilter_opts_l	: etherfilter_opts_l etherfilter_opt
+			| etherfilter_opt
+
+etherfilter_opt	: etherqname	{
+			if (filter_opts.queues.qname) {
+				yyerror("queue cannot be redefined");
+				YYERROR;
+			}
+			filter_opts.queues = $1;
+		}
+		| TAG string				{
+			filter_opts.tag = $2;
+		}
+		;
+
 scrubrule	: scrubaction dir logquick interface af proto fromto scrub_opts
 		{
 			struct pfctl_rule	r;
@@ -2978,6 +3058,56 @@ af		: /* empty */			{ $$ = 0; }
 		| INET6				{ $$ = AF_INET6; }
 		;
 
+etherproto	: /* empty */				{ $$ = NULL; }
+		| PROTO etherproto_item			{ $$ = $2; }
+		| PROTO '{' optnl etherproto_list '}'	{ $$ = $4; }
+		;
+
+etherproto_list	: etherproto_item optnl			{ $$ = $1; }
+		| etherproto_list comma etherproto_item optnl	{
+			$1->tail->next = $3;
+			$1->tail = $3;
+			$$ = $1;
+		}
+		;
+
+etherproto_item	: etherprotoval		{
+			u_int16_t	pr;
+
+			pr = (u_int16_t)$1;
+			if (pr == 0) {
+				yyerror("proto 0 cannot be used");
+				YYERROR;
+			}
+			$$ = calloc(1, sizeof(struct node_proto));
+			if ($$ == NULL)
+				err(1, "proto_item: calloc");
+			$$->proto = pr;
+			$$->next = NULL;
+			$$->tail = $$;
+		}
+		;
+
+etherprotoval	: NUMBER			{
+			if ($1 < 0 || $1 > 65565) {
+				yyerror("protocol outside range");
+				YYERROR;
+			}
+		}
+		| STRING
+		{
+			if (!strncmp($1, "0x", 2)) {
+				if (sscanf($1, "0x%4x", &$$) != 1) {
+					free($1);
+					yyerror("invalid EtherType hex");
+					YYERROR;
+				}
+			} else {
+				yyerror("Symbolic EtherType not yet supported");
+			}
+		}
+		;
+
 proto		: /* empty */				{ $$ = NULL; }
 		| PROTO proto_item			{ $$ = $2; }
 		| PROTO '{' optnl proto_list '}'	{ $$ = $4; }
@@ -3028,6 +3158,50 @@ protoval	: STRING			{
 		}
 		;
 
+etherfromto	: ALL				{
+			bzero($$.src, sizeof($$.src));
+			$$.srcneg = 0;
+			bzero($$.dst, sizeof($$.dst));
+			$$.dstneg = 0;
+		}
+		| etherfrom etherto		{
+			memcpy(&$$.src, $1.mac, sizeof($$.src));
+			$$.srcneg = $1.neg;
+			memcpy(&$$.dst, $2.mac, sizeof($$.dst));
+			$$.dstneg = $2.neg;
+		}
+		;
+
+etherfrom	: /* emtpy */			{
+			bzero(&$$, sizeof($$));
+		}
+		| FROM not mac			{
+			memcpy(&$$.mac, $3, sizeof($$));
+			$$.neg = $2;
+		}
+		;
+
+etherto		: /* empty */			{
+			bzero(&$$, sizeof($$));
+		}
+		| TO not mac			{
+			memcpy(&$$.mac, $3, sizeof($$));
+			$$.neg = $2;
+		}
+		;
+
+mac		: string			{
+			if (sscanf($1, "%02hhx:%02hhx:%02hhx:%02hhx:%02hhx:%02hhx",
+			    &$$[0], &$$[1], &$$[2], &$$[3], &$$[4],
+			    &$$[5]) != 6) {
+				free($$);
+				free($1);
+				yyerror("invalid MAC address");
+				YYERROR;
+			}
+		}
+		;
+
 fromto		: ALL				{
 			$$.src.host = NULL;
 			$$.src.port = NULL;
@@ -3965,6 +4139,14 @@ label		: LABEL STRING			{
 		}
 		;
 
+etherqname	: QUEUE STRING				{
+			$$.qname = $2;
+		}
+		| QUEUE '(' STRING ')'			{
+			$$.qname = $3;
+		}
+		;
+
 qname		: QUEUE STRING				{
 			$$.qname = $2;
 			$$.pqname = NULL;
@@ -5422,6 +5604,31 @@ expand_queue(struct pf_altq *a, struct node_if *interfaces,
 		return (0);
 }
 
+void
+expand_eth_rule(struct pfctl_eth_rule *r,
+    struct node_if *interfaces, struct node_etherproto *protos)
+{
+	struct pfctl_eth_rule *rule;
+
+	LOOP_THROUGH(struct node_if, interface, interfaces,
+	LOOP_THROUGH(struct node_etherproto, proto, protos,
+		r->nr = pf->eth_nr++;
+		strlcpy(r->ifname, interface->ifname,
+		    sizeof(r->ifname));
+		r->ifnot = interface->not;
+		r->proto = proto->proto;
+
+		if ((rule = calloc(1, sizeof(*rule))) == NULL)
+			err(1, "calloc");
+		bcopy(r, rule, sizeof(*rule));
+
+		TAILQ_INSERT_TAIL(&pf->eth_rules, rule, entries);
+	));
+
+	FREE_LIST(struct node_if, interfaces);
+	FREE_LIST(struct node_etherproto, protos);
+}
+
 void
 expand_rule(struct pfctl_rule *r,
     struct node_if *interfaces, struct node_host *rpool_hosts,
@@ -5638,8 +5845,8 @@ int
 check_rulestate(int desired_state)
 {
 	if (require_order && (rulestate > desired_state)) {
-		yyerror("Rules must be in order: options, normalization, "
-		    "queueing, translation, filtering");
+		yyerror("Rules must be in order: options, ethernet, "
+		    "normalization, queueing, translation, filtering");
 		return (1);
 	}
 	rulestate = desired_state;
@@ -5682,6 +5889,7 @@ lookup(char *s)
 		{ "drop",		DROP},
 		{ "drop-ovl",		FRAGDROP},
 		{ "dup-to",		DUPTO},
+		{ "ether",		ETHER},
 		{ "fail-policy",	FAILPOLICY},
 		{ "fairq",		FAIRQ},
 		{ "fastroute",		FASTROUTE},
diff --git a/sbin/pfctl/pfctl.c b/sbin/pfctl/pfctl.c
index a0eec1b09289..83b3c1db0613 100644
--- a/sbin/pfctl/pfctl.c
+++ b/sbin/pfctl/pfctl.c
@@ -96,7 +96,9 @@ int	 pfctl_load_hostid(struct pfctl *, u_int32_t);
 int	 pfctl_load_syncookies(struct pfctl *, u_int8_t);
 int	 pfctl_get_pool(int, struct pfctl_pool *, u_int32_t, u_int32_t, int,
 	    char *);
+void	 pfctl_print_eth_rule_counters(struct pfctl_eth_rule *, int);
 void	 pfctl_print_rule_counters(struct pfctl_rule *, int);
+int	 pfctl_show_eth_rules(int, int);
 int	 pfctl_show_rules(int, char *, int, enum pfctl_show, char *, int);
 int	 pfctl_show_nat(int, int, char *);
 int	 pfctl_show_src_nodes(int, int);
@@ -109,6 +111,7 @@ void	 pfctl_debug(int, u_int32_t, int);
 int	 pfctl_test_altqsupport(int, int);
 int	 pfctl_show_anchors(int, int, char *);
 int	 pfctl_ruleset_trans(struct pfctl *, char *, struct pfctl_anchor *);
+int	 pfctl_load_eth_ruleset(struct pfctl *);
 int	 pfctl_load_ruleset(struct pfctl *, char *,
 		struct pfctl_ruleset *, int, int);
 int	 pfctl_load_rule(struct pfctl *, char *, struct pfctl_rule *, int);
@@ -222,9 +225,9 @@ static const char * const clearopt_list[] = {
 };
 
 static const char * const showopt_list[] = {
-	"nat", "queue", "rules", "Anchors", "Sources", "states", "info",
-	"Interfaces", "labels", "timeouts", "memory", "Tables", "osfp",
-	"Running", "all", NULL
+	"ether", "nat", "queue", "rules", "Anchors", "Sources", "states",
+	"info", "Interfaces", "labels", "timeouts", "memory", "Tables",
+	"osfp", "Running", "all", NULL
 };
 
 static const char * const tblcmdopt_list[] = {
@@ -986,6 +989,20 @@ pfctl_clear_pool(struct pfctl_pool *pool)
 	}
 }
 
+void
+pfctl_print_eth_rule_counters(struct pfctl_eth_rule *rule, int opts)
+{
+	if (opts & PF_OPT_VERBOSE) {
+		printf("  [ Evaluations: %-8llu  Packets: %-8llu  "
+			    "Bytes: %-10llu]\n",
+			    (unsigned long long)rule->evaluations,
+			    (unsigned long long)(rule->packets[0] +
+			    rule->packets[1]),
+			    (unsigned long long)(rule->bytes[0] +
+			    rule->bytes[1]));
+	}
+}
+
 void
 pfctl_print_rule_counters(struct pfctl_rule *rule, int opts)
 {
@@ -1034,6 +1051,35 @@ pfctl_print_title(char *title)
 	printf("%s\n", title);
 }
 
+int
+pfctl_show_eth_rules(int dev, int opts)
+{
+	struct pfctl_eth_rules_info info;
+	struct pfctl_eth_rule rule;
+	int dotitle = opts & PF_OPT_SHOWALL;
+
+	if (pfctl_get_eth_rules_info(dev, &info)) {
+		warn("DIOCGETETHRULES");
+		return (-1);
+	}
+	for (int nr = 0; nr < info.nr; nr++) {
+		if (pfctl_get_eth_rule(dev, nr, info.ticket, &rule, false)
+		    != 0) {
+			warn("DIOCGETETHRULE");
+			return (-1);
+		}
+		if (dotitle) {
+			pfctl_print_title("ETH RULES:");
+			dotitle = 0;
+		}
+		print_eth_rule(&rule, opts & (PF_OPT_VERBOSE2 | PF_OPT_DEBUG));
+		printf("\n");
+		pfctl_print_eth_rule_counters(&rule, opts);
+	}
+
+	return (0);
+}
+
 int
 pfctl_show_rules(int dev, char *path, int opts, enum pfctl_show format,
     char *anchorname, int depth)
@@ -1466,6 +1512,12 @@ pfctl_ruleset_trans(struct pfctl *pf, char *path, struct pfctl_anchor *a)
 {
 	int osize = pf->trans->pfrb_size;
 
+	if ((pf->loadopt & PFCTL_FLAG_ETH) != 0) {
+		if (! path[0]) {
+			if (pfctl_add_trans(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) ||
@@ -1491,6 +1543,27 @@ pfctl_ruleset_trans(struct pfctl *pf, char *path, struct pfctl_anchor *a)
 	return (0);
 }
 
+int
+pfctl_load_eth_ruleset(struct pfctl *pf)
+{
+	struct pfctl_eth_rule	*r;
+	int	error;
+
+	while ((r = TAILQ_FIRST(&pf->eth_rules)) != NULL) {
+		TAILQ_REMOVE(&pf->eth_rules, r, entries);
+
+		if ((pf->opts & PF_OPT_NOACTION) == 0) {
+			error = pfctl_add_eth_rule(pf->dev, r, pf->eth_ticket);
+			if (error)
+				return (error);
+		}
+
+		free(r);
+	}
+
+	return (0);
+}
+
 int
 pfctl_load_ruleset(struct pfctl *pf, char *path, struct pfctl_ruleset *rs,
     int rs_num, int depth)
@@ -1669,6 +1742,7 @@ pfctl_rules(int dev, char *filename, int opts, int optimize,
 	pf.opts = opts;
 	pf.optimize = optimize;
 	pf.loadopt = loadopt;
+	TAILQ_INIT(&pf.eth_rules);
 
 	/* non-brace anchor, create without resolving the path */
 	if ((pf.anchor = calloc(1, sizeof(*pf.anchor))) == NULL)
@@ -1700,6 +1774,8 @@ pfctl_rules(int dev, char *filename, int opts, int optimize,
 		 */
 		if (pfctl_ruleset_trans(&pf, anchorname, pf.anchor))
 			ERRX("pfctl_rules");
+		if (pf.loadopt & PFCTL_FLAG_ETH)
+			pf.eth_ticket = pfctl_get_ticket(t, PF_RULESET_ETH, anchorname);
 		if (altqsupport && (pf.loadopt & PFCTL_FLAG_ALTQ))
 			pa.ticket =
 			    pfctl_get_ticket(t, PF_RULESET_ALTQ, anchorname);
@@ -1720,6 +1796,8 @@ pfctl_rules(int dev, char *filename, int opts, int optimize,
 
 	if ((pf.loadopt & PFCTL_FLAG_FILTER &&
 	    (pfctl_load_ruleset(&pf, path, rs, PF_RULESET_SCRUB, 0))) ||
+	    (pf.loadopt & PFCTL_FLAG_ETH &&
+	    (pfctl_load_eth_ruleset(&pf))) ||
 	    (pf.loadopt & PFCTL_FLAG_NAT &&
 	    (pfctl_load_ruleset(&pf, path, rs, PF_RULESET_NAT, 0) ||
 	    pfctl_load_ruleset(&pf, path, rs, PF_RULESET_RDR, 0) ||
@@ -2561,10 +2639,15 @@ main(int argc, char *argv[])
 		case 'm':
 			pfctl_show_limits(dev, opts);
 			break;
+		case 'e':
+			pfctl_show_eth_rules(dev, opts);
+			break;
 		case 'a':
 			opts |= PF_OPT_SHOWALL;
 			pfctl_load_fingerprints(dev, opts);
 
+			pfctl_show_eth_rules(dev, opts);
+
 			pfctl_show_nat(dev, opts, anchorname);
 			pfctl_show_rules(dev, path, opts, 0, anchorname, 0);
 			pfctl_show_altq(dev, ifaceopt, opts, 0);
diff --git a/sbin/pfctl/pfctl_parser.c b/sbin/pfctl/pfctl_parser.c
index 5a01c30a076e..8814dc38b23c 100644
--- a/sbin/pfctl/pfctl_parser.c
+++ b/sbin/pfctl/pfctl_parser.c
@@ -691,6 +691,50 @@ print_src_node(struct pf_src_node *sn, int opts)
 	}
 }
 
+static void
+print_eth_addr(const struct pfctl_eth_addr *a)
+{
+	printf("%s%02x:%02x:%02x:%02x:%02x:%02x", a->neg ? "! " : "",
+	    a->addr[0], a->addr[1], a->addr[2], a->addr[3], a->addr[4],
+	    a->addr[5]);
+}
+
+void
+print_eth_rule(struct pfctl_eth_rule *r, int rule_numbers)
+{
+	static const char *actiontypes[] = { "pass", "block" };
+
+	if (rule_numbers)
+		printf("@%u ", r->nr);
+
+	printf("ether %s", actiontypes[r->action]);
+	if (r->direction == PF_IN)
+		printf(" in");
+	else if (r->direction == PF_OUT)
+		printf(" out");
+
+	if (r->quick)
+		printf(" quick");
+	if (r->ifname[0]) {
+		if (r->ifnot)
+			printf(" on ! %s", r->ifname);
+		else
+			printf(" on %s", r->ifname);
+	}
+	if (r->proto)
+		printf(" proto 0x%04x", r->proto);
+
+	printf(" from ");
+	print_eth_addr(&r->src);
+	printf(" to ");
+	print_eth_addr(&r->dst);
+
+	if (r->qname[0])
+		printf(" queue %s", r->qname);
+	if (r->tagname[0])
+		printf(" tag %s", r->tagname);
+}
+
 void
 print_rule(struct pfctl_rule *r, const char *anchor_call, int verbose, int numeric)
 {
diff --git a/sbin/pfctl/pfctl_parser.h b/sbin/pfctl/pfctl_parser.h
index 0cd19a560f8d..1d69682b1b6c 100644
--- a/sbin/pfctl/pfctl_parser.h
+++ b/sbin/pfctl/pfctl_parser.h
@@ -90,6 +90,9 @@ struct pfctl {
 	struct pfioc_queue *pqueue;
 	struct pfr_buffer *trans;
 	struct pfctl_anchor *anchor, *alast;
+	int eth_nr;
+	struct pfctl_eth_rules eth_rules;
+	u_int32_t eth_ticket;
 	const char *ruleset;
 
 	/* 'set foo' options */
@@ -284,6 +287,7 @@ int	pfctl_load_anchors(int, struct pfctl *, struct pfr_buffer *);
 
 void	print_pool(struct pfctl_pool *, u_int16_t, u_int16_t, sa_family_t, int);
 void	print_src_node(struct pf_src_node *, int);
+void	print_eth_rule(struct pfctl_eth_rule *, int);
 void	print_rule(struct pfctl_rule *, const char *, int, int);
 void	print_tabledef(const char *, int, int, struct node_tinithead *);
 void	print_status(struct pfctl_status *, struct pfctl_syncookies *, int);
@@ -336,6 +340,7 @@ struct pf_timeout {
 #define PFCTL_FLAG_OPTION	0x08
 #define PFCTL_FLAG_ALTQ		0x10
 #define PFCTL_FLAG_TABLE	0x20
+#define PFCTL_FLAG_ETH		0x40
 
 extern const struct pf_timeout pf_timeouts[];