git: b590f17a11aa - main - pf: support masking mac addresses

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

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

commit b590f17a11aacaeb0a70d075df04d40c6b7eb947
Author:     Kristof Provost <kp@FreeBSD.org>
AuthorDate: 2022-01-20 17:31:45 +0000
Commit:     Kristof Provost <kp@FreeBSD.org>
CommitDate: 2022-03-02 16:00:08 +0000

    pf: support masking mac addresses
    
    When filtering Ethernet packets allow rules to specify a mac address
    with a mask. This indicates which bits of the specified address are
    significant. This allows users to do things like filter based on device
    manufacturer.
    
    Sponsored by:   Rubicon Communications, LLC ("Netgate")
---
 lib/libpfctl/libpfctl.c   |  5 +++
 lib/libpfctl/libpfctl.h   |  1 +
 sbin/pfctl/parse.y        | 97 +++++++++++++++++++++++++++++++++++++++++------
 sbin/pfctl/pfctl_parser.c | 33 +++++++++++++++-
 sbin/pfctl/pfctl_parser.h |  1 +
 share/man/man5/pf.conf.5  |  2 +
 sys/net/pfvar.h           |  1 +
 sys/netpfil/pf/pf.c       | 12 ++++--
 sys/netpfil/pf/pf_nv.c    |  4 ++
 9 files changed, 140 insertions(+), 16 deletions(-)

diff --git a/lib/libpfctl/libpfctl.c b/lib/libpfctl/libpfctl.c
index 90733d421572..07699d66deaa 100644
--- a/lib/libpfctl/libpfctl.c
+++ b/lib/libpfctl/libpfctl.c
@@ -557,6 +557,10 @@ pfctl_nveth_addr_to_eth_addr(const nvlist_t *nvl, struct pfctl_eth_addr *addr)
 	assert(len == sizeof(addr->addr));
 	memcpy(addr->addr, data, sizeof(addr->addr));
 
+	data = nvlist_get_binary(nvl, "mask", &len);
+	assert(len == sizeof(addr->mask));
+	memcpy(addr->mask, data, sizeof(addr->mask));
+
 	addr->neg = nvlist_get_bool(nvl, "neg");
 
 	/* To make checks for 'is this address set?' easier. */
@@ -574,6 +578,7 @@ pfctl_eth_addr_to_nveth_addr(const struct pfctl_eth_addr *addr)
 
 	nvlist_add_bool(nvl, "neg", addr->neg);
 	nvlist_add_binary(nvl, "addr", &addr->addr, ETHER_ADDR_LEN);
+	nvlist_add_binary(nvl, "mask", &addr->mask, ETHER_ADDR_LEN);
 
 	return (nvl);
 }
diff --git a/lib/libpfctl/libpfctl.h b/lib/libpfctl/libpfctl.h
index 256fa49c4f25..4f8f55945d4b 100644
--- a/lib/libpfctl/libpfctl.h
+++ b/lib/libpfctl/libpfctl.h
@@ -73,6 +73,7 @@ struct pfctl_eth_rules_info {
 
 struct pfctl_eth_addr {
 	uint8_t	addr[ETHER_ADDR_LEN];
+	uint8_t	mask[ETHER_ADDR_LEN];
 	bool	neg;
 	bool	isset;
 };
diff --git a/sbin/pfctl/parse.y b/sbin/pfctl/parse.y
index 5f10c4ab2e17..346ec9d9a587 100644
--- a/sbin/pfctl/parse.y
+++ b/sbin/pfctl/parse.y
@@ -377,6 +377,9 @@ int	 invalid_redirect(struct node_host *, sa_family_t);
 u_int16_t parseicmpspec(char *, sa_family_t);
 int	 kw_casecmp(const void *, const void *);
 int	 map_tos(char *string, int *);
+struct node_mac* node_mac_from_string(const char *);
+struct node_mac* node_mac_from_string_masklen(const char *, int);
+struct node_mac* node_mac_from_string_mask(const char *, const char *);
 
 static TAILQ_HEAD(loadanchorshead, loadanchors)
     loadanchorshead = TAILQ_HEAD_INITIALIZER(loadanchorshead);
@@ -3277,22 +3280,25 @@ etherto		: /* empty */			{
 		}
 		;
 
-mac		: string			{
-			$$ = calloc(1, sizeof(struct node_mac));
+mac		: string '/' NUMBER		{
+			$$ = node_mac_from_string_masklen($1, $3);
+			free($1);
 			if ($$ == NULL)
-				err(1, "mac: calloc");
-
-			if (sscanf($1, "%02hhx:%02hhx:%02hhx:%02hhx:%02hhx:%02hhx",
-			    &$$->mac[0], &$$->mac[1], &$$->mac[2], &$$->mac[3], &$$->mac[4],
-			    &$$->mac[5]) != 6) {
-				free($$);
-				free($1);
-				yyerror("invalid MAC address");
 				YYERROR;
+		}
+		| string			{
+			if (strchr($1, '&')) {
+				/* mac&mask */
+				char *mac = strtok($1, "&");
+				char *mask = strtok(NULL, "&");
+				$$ = node_mac_from_string_mask(mac, mask);
+			} else {
+				$$ = node_mac_from_string($1);
 			}
 			free($1);
-			$$->next = NULL;
-			$$->tail = $$;
+			if ($$ == NULL)
+				YYERROR;
+
 		}
 xmac		: not mac {
 			struct node_mac	*n;
@@ -5741,8 +5747,10 @@ expand_eth_rule(struct pfctl_eth_rule *r,
 		r->ifnot = interface->not;
 		r->proto = proto->proto;
 		bcopy(src->mac, r->src.addr, ETHER_ADDR_LEN);
+		bcopy(src->mask, r->src.mask, ETHER_ADDR_LEN);
 		r->src.neg = src->neg;
 		bcopy(dst->mac, r->dst.addr, ETHER_ADDR_LEN);
+		bcopy(dst->mask, r->dst.mask, ETHER_ADDR_LEN);
 		r->dst.neg = dst->neg;
 		r->nr = pf->eastack[pf->asd]->match++;
 
@@ -6899,3 +6907,68 @@ rt_tableid_max(void)
 	return (RT_TABLEID_MAX);
 #endif
 }
+
+struct node_mac*
+node_mac_from_string(const char *str)
+{
+	struct node_mac *m;
+
+	m = calloc(1, sizeof(struct node_mac));
+	if (m == NULL)
+		err(1, "mac: calloc");
+
+	if (sscanf(str, "%02hhx:%02hhx:%02hhx:%02hhx:%02hhx:%02hhx",
+	    &m->mac[0], &m->mac[1], &m->mac[2], &m->mac[3], &m->mac[4],
+	    &m->mac[5]) != 6) {
+		free(m);
+		yyerror("invalid MAC address");
+		return (NULL);
+	}
+
+	memset(m->mask, 0xff, ETHER_ADDR_LEN);
+	m->next = NULL;
+	m->tail = m;
+
+	return (m);
+}
+
+struct node_mac*
+node_mac_from_string_masklen(const char *str, int masklen)
+{
+	struct node_mac *m;
+
+	if (masklen < 0 || masklen > (ETHER_ADDR_LEN * 8)) {
+		yyerror("invalid MAC mask length");
+		return (NULL);
+	}
+
+	m = node_mac_from_string(str);
+	if (m == NULL)
+		return (NULL);
+
+	memset(m->mask, 0, ETHER_ADDR_LEN);
+	for (int i = 0; i < masklen; i++)
+		m->mask[i / 8] |= 1 << (i % 8);
+
+	return (m);
+}
+
+struct node_mac*
+node_mac_from_string_mask(const char *str, const char *mask)
+{
+	struct node_mac *m;
+
+	m = node_mac_from_string(str);
+	if (m == NULL)
+		return (NULL);
+
+	if (sscanf(mask, "%02hhx:%02hhx:%02hhx:%02hhx:%02hhx:%02hhx",
+	    &m->mask[0], &m->mask[1], &m->mask[2], &m->mask[3], &m->mask[4],
+	    &m->mask[5]) != 6) {
+		free(m);
+		yyerror("invalid MAC mask");
+		return (NULL);
+	}
+
+	return (m);
+}
diff --git a/sbin/pfctl/pfctl_parser.c b/sbin/pfctl/pfctl_parser.c
index 0db0ad355cf7..1637d7358d0d 100644
--- a/sbin/pfctl/pfctl_parser.c
+++ b/sbin/pfctl/pfctl_parser.c
@@ -694,7 +694,9 @@ print_src_node(struct pf_src_node *sn, int opts)
 static void
 print_eth_addr(const struct pfctl_eth_addr *a)
 {
-	int i;
+	int i, masklen = ETHER_ADDR_LEN * 8;
+	bool seen_unset = false;
+
 	for (i = 0; i < ETHER_ADDR_LEN; i++) {
 		if (a->addr[i] != 0)
 			break;
@@ -707,6 +709,35 @@ 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]);
+
+	for (i = 0; i < (ETHER_ADDR_LEN * 8); i++) {
+		bool isset = a->mask[i / 8] & (1 << i % 8);
+
+		if (! seen_unset) {
+			if (isset)
+				continue;
+			seen_unset = true;
+			masklen = i;
+		} else {
+			/* Not actually a continuous mask, so print the whole
+			 * thing. */
+			if (isset)
+				break;
+			continue;
+		}
+	}
+
+	if (masklen == (ETHER_ADDR_LEN * 8))
+		return;
+
+	if (i == (ETHER_ADDR_LEN * 8)) {
+		printf("/%d", masklen);
+		return;
+	}
+
+	printf("&%02x:%02x:%02x:%02x:%02x:%02x",
+	    a->mask[0], a->mask[1], a->mask[2], a->mask[3], a->mask[4],
+	    a->mask[5]);
 }
 
 void
diff --git a/sbin/pfctl/pfctl_parser.h b/sbin/pfctl/pfctl_parser.h
index e60132d15855..60bbae7a3fcd 100644
--- a/sbin/pfctl/pfctl_parser.h
+++ b/sbin/pfctl/pfctl_parser.h
@@ -138,6 +138,7 @@ struct node_host {
 
 struct node_mac {
 	u_int8_t	 mac[ETHER_ADDR_LEN];
+	u_int8_t	 mask[ETHER_ADDR_LEN];
 	bool		 neg;
 	struct node_mac	*next;
 	struct node_mac	*tail;
diff --git a/share/man/man5/pf.conf.5 b/share/man/man5/pf.conf.5
index 1bed985901f2..13ff88586d8e 100644
--- a/share/man/man5/pf.conf.5
+++ b/share/man/man5/pf.conf.5
@@ -3178,6 +3178,8 @@ protospec      = "proto" ( proto-name | proto-number |
 proto-list     = ( proto-name | proto-number ) [ [ "," ] proto-list ]
 
 etherhosts     = "from" macaddress "to" macaddress
+macaddress     = mac | mac "/" masklen | mac "&" mask
+
 hosts          = "all" |
                  "from" ( "any" | "no-route" | "urpf-failed" | "self" | host |
                  "{" host-list "}" ) [ port ] [ os ]
diff --git a/sys/net/pfvar.h b/sys/net/pfvar.h
index a2265e7cc1c5..252b3adbe958 100644
--- a/sys/net/pfvar.h
+++ b/sys/net/pfvar.h
@@ -579,6 +579,7 @@ union pf_keth_rule_ptr {
 
 struct pf_keth_rule_addr {
 	uint8_t	addr[ETHER_ADDR_LEN];
+	uint8_t	mask[ETHER_ADDR_LEN];
 	bool neg;
 	uint8_t	isset;
 };
diff --git a/sys/netpfil/pf/pf.c b/sys/netpfil/pf/pf.c
index bd82c2a9904e..fb0abc8cd035 100644
--- a/sys/netpfil/pf/pf.c
+++ b/sys/netpfil/pf/pf.c
@@ -3809,14 +3809,20 @@ pf_tcp_iss(struct pf_pdesc *pd)
 static bool
 pf_match_eth_addr(const uint8_t *a, const struct pf_keth_rule_addr *r)
 {
+	bool match = true;
+
 	/* Always matches if not set */
 	if (! r->isset)
 		return (!r->neg);
 
-	if (memcmp(a, r->addr, ETHER_ADDR_LEN) == 0)
-		return (!r->neg);
+	for (int i = 0; i < ETHER_ADDR_LEN; i++) {
+		if ((a[i] & r->mask[i]) != (r->addr[i] & r->mask[i])) {
+			match = false;
+			break;
+		}
+	}
 
-	return (r->neg);
+	return (match ^ r->neg);
 }
 
 static int
diff --git a/sys/netpfil/pf/pf_nv.c b/sys/netpfil/pf/pf_nv.c
index 5fc222ff3b79..42434dabf565 100644
--- a/sys/netpfil/pf/pf_nv.c
+++ b/sys/netpfil/pf/pf_nv.c
@@ -1013,6 +1013,9 @@ pf_nveth_rule_addr_to_keth_rule_addr(const nvlist_t *nvl,
 
 	PFNV_CHK(pf_nvbinary(nvl, "addr", &krule->addr, sizeof(krule->addr)));
 	PFNV_CHK(pf_nvbool(nvl, "neg", &krule->neg));
+	if (nvlist_exists_binary(nvl, "mask"))
+		PFNV_CHK(pf_nvbinary(nvl, "mask", &krule->mask,
+		    sizeof(krule->mask)));
 
 	/* To make checks for 'is this address set?' easier. */
 	if (memcmp(krule->addr, EMPTY_MAC, ETHER_ADDR_LEN) != 0)
@@ -1032,6 +1035,7 @@ pf_keth_rule_addr_to_nveth_rule_addr(const struct pf_keth_rule_addr *krule)
 		return (NULL);
 
 	nvlist_add_binary(nvl, "addr", &krule->addr, sizeof(krule->addr));
+	nvlist_add_binary(nvl, "mask", &krule->mask, sizeof(krule->mask));
 	nvlist_add_bool(nvl, "neg", krule->neg);
 
 	return (nvl);