Index | Thread | Search

From:
David Gwynne <david@gwynne.id.au>
Subject:
make veb(4) VLAN aware
To:
tech@openbsd.org
Date:
Wed, 29 Oct 2025 15:54:42 +1000

Download raw body.

Thread
veb(4) is currently vlan unaware, meaning that it assumes that there's a
single "namespace" for the mac addresses used by packets handled by the
bridge. by default it blocks vlan (and svlan) packets, but if you allow
it carry vlan packets it ignores the vlan tag when doing the mac address
lookups.

adding vlan awareness means that every mac address the bridge learns
is now associated with a vlan identifier (vid). ie, the same mac
in two different vlans will get separate entries in the forwarding
database.

this means ports in a veb now get vlan configuration. if you're used to
operating vlans on switches with "industry standard" style config, this
should feel pretty familiar to you.

when an interface is added to a veb as a port, it is configured with a
default vid. this vid is assigned to "untagged" packets (packets on
the wire without a VLAN tag) received by this port inside the veb, and
then stripped again when transmitted on another port with the same vid.

veb can also handle vlan tagged packets directly now by adding "tagged"
vid configuration to ports. this means you don't need to configure
vlan(4) interfaces and add them as ports on a veb to handle tagged
traffic.

if you only want to handle tagged traffic on a port, you can disable
"untagged" packet handling too.

the result of all this is that you can implement vlan aware switching
without a ton of boilerplate now. previously you were supposed to create
a veb(4) per VLAN you wanted to wire up. if you wanted to handle tagged
packets you had to create vlan(4) interfaces and add them to their
respective veb(4)s. if you're using the same VIDs consistently on every
port, this basically all collapses down to a single veb(4) with the
approriate set of "tagged" vids on each port.

vlan(4) still gets the first run at packets in ethernet input though.
if you do want to remap vlan tags on the wire to a vlan within a
veb(4), you can still create a vlan(4) interface to catch that vid
and add it as an untagged port to that VLAN in the bridge.

the actual implementation of this in the kernel was surprisingly simple.
the etherbridge code was extended to include a vid next to the ethernet
address it does lookups against, and Just Works(tm). the other users of
the etherbridge code hardcode 0 as the vid they use. the veb(4)
forwarding path was extended to figure out the right vid for the packet
and carry it around.

the majority of the changes are in the ioctl and ifconfig changes needed
to support it. ive tried to keep the changes as unintrusive as possible,
and as backwards compatible as possible.

the main changes to ifconfig are the addition of "untagged" and
"+tagged" commands and variations of them to manage which vids a
port can use and how, and extending the mac addresses in "static",
"deladdr" and the display of the address cache with @vid.

if you have a simple veb(4) setup now, the config you have now should
still work with the new code. simple means you haven't enabled link0.
if you have enabled link0, this will be a breaking change that requires
you to explicitly add tagged vids to the ports you want to carry vlans
on.

as a quick and dirty example here's a config i've been using, part of
which replaces a vlan(4) interface on vmx0 with vnetid 871 with a
tagged vid on the physical port and a vport interface:

$ doas cat /etc/hostname.veb0
add vmx0
-untagged vmx0
+tagged vmx0 871
+tagged vmx0 780
+tagged vmx0 781
add vport0
untagged vport0 871
add tap0
untagged vport0 780
add tap1
untagged vport0 781
up
$ ifconfig veb0                                               
veb0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST>
	index 7 llprio 3
	encap: vnetid 1 txprio packet rxprio outer
	groups: veb
	vmx0 flags=3<LEARNING,DISCOVER>
		port 1 ifpriority 0 ifcost 0 -untagged
		tagged: 780,781,871
	vport0 flags=3<LEARNING,DISCOVER>
		port 8 ifpriority 0 ifcost 0 untagged 871
	tap0 flags=3<LEARNING,DISCOVER>
		port 5 ifpriority 0 ifcost 0 untagged 780
		tagged: none
	tap1 flags=3<LEARNING,DISCOVER>
		port 6 ifpriority 0 ifcost 0 untagged 781
		tagged: none
	tap2 flags=3<LEARNING,DISCOVER>
		port 9 ifpriority 0 ifcost 0 untagged 780
		tagged: none
	Addresses (max cache: 100, timeout: 240):
		00:00:5e:00:01:50@780 vmx0 created 167591 used 0 flags=0<>
		8c:04:ba:cf:60:c0@780 vmx0 created 167590 used 1 flags=0<>
		00:00:5e:00:01:51@781 vmx0 created 167591 used 0 flags=0<>
		8c:04:ba:cf:60:c0@781 vmx0 created 167591 used 1 flags=0<>
		00:00:5e:00:01:11@871 vmx0 created 167591 used 1 flags=0<>
		00:00:5e:00:01:47@871 vmx0 created 167591 used 1 flags=0<>
		00:50:56:a1:75:f0@871 vport0 created 167592 used 0 flags=0<>
		fe:e1:ba:d0:74:ef@871 vmx0 created 86776 used 9 flags=0<>

you can add a static entry (and delete them) with "static" and
"deladdr", you just have to add @vid to the address. eg

# ifconfig veb0 deladdr 00:50:56:a1:75:f0@871 vport0
# ifconfig veb0 static 00:50:56:a1:75:f0@871 vport0

it is possible to add static entries pointing at ports that do not
have the relevant vid configured. in that situation the tagged/untagged
config takes precedence and packets using that address entry will
be dropped.

i dont think it makes sense to allow tagged packets over vport
interfaces so the ability to configure that is blocked. vport
interfaces plug the ip stack into the bridge, which requires untagged
traffic. if you want to plug the stack into different VLANs on the
same bridge, rather than create vlan interfaces on top of a vport,
just create vport interfaces associated with the right vids.

the ifconfig changes are pretty simplistic, there's some improvements
around handling sets of vids that should be made, but the diff is
big enough.

finally, the main reason i wanted this was to improve the semantics
of handling vlans on bridges. flipping the link0 flag is too easy
and makes the network too open, and setting up a ton of veb(4)s and
vlan(4)s per VLAN is too much boilerplate. i also need to properly
get my head around PVLANs as per RFC 5517, which i find is easiest
if i can actually implement it. being able to hack it into veb(4)
on top of this diff is my current plan for that.

Index: sbin/ifconfig/brconfig.c
===================================================================
RCS file: /cvs/src/sbin/ifconfig/brconfig.c,v
diff -u -p -r1.34 brconfig.c
--- sbin/ifconfig/brconfig.c	21 Oct 2025 05:14:22 -0000	1.34
+++ sbin/ifconfig/brconfig.c	29 Oct 2025 04:15:50 -0000
@@ -46,9 +46,12 @@
 #include <getopt.h>
 #include <limits.h>
 #include <arpa/inet.h>
+#include <time.h>
 
 #include "ifconfig.h"
 
+#define VID_SEP	'@'
+
 void bridge_ifsetflag(const char *, u_int32_t);
 void bridge_ifclrflag(const char *, u_int32_t);
 
@@ -57,6 +60,7 @@ void bridge_cfg(const char *);
 void bridge_badrule(int, char **, int);
 void bridge_showrule(struct ifbrlreq *);
 int bridge_arprule(struct ifbrlreq *, int *, char ***);
+void bridge_vid_map(const char *);
 
 #define	IFBAFBITS	"\020\1STATIC"
 #define	IFBIFBITS	\
@@ -355,6 +359,16 @@ bridge_list(char *delim)
 		printf("port %u ifpriority %u ifcost %u",
 		    reqp->ifbr_portno, reqp->ifbr_priority,
 		    reqp->ifbr_path_cost);
+		switch (reqp->ifbr_pvid) {
+		case IFBR_PVID_NULL:
+			break;
+		case IFBR_PVID_NONE:
+			printf(" -untagged");
+			break;
+		default:
+			printf(" untagged %u", reqp->ifbr_pvid);
+			break;
+		}
 		if (reqp->ifbr_protected) {
 			int v;
 
@@ -370,6 +384,7 @@ bridge_list(char *delim)
 			    stpstates[reqp->ifbr_state],
 			    stproles[reqp->ifbr_role]);
 		printf("\n");
+		bridge_vid_map(buf);
 		bridge_rules(buf, 1);
 	}
 	free(bifc.ifbic_buf);
@@ -517,6 +532,87 @@ bridge_unprotect(const char *ifsname, in
 }
 
 void
+bridge_pvid(const char *ifsname, const char *val)
+{
+	struct ifbreq breq;
+	const char *errstr;
+
+	strlcpy(breq.ifbr_name, ifname, sizeof(breq.ifbr_name));
+	strlcpy(breq.ifbr_ifsname, ifsname, sizeof(breq.ifbr_ifsname));
+
+	if (strcmp(val, "default") == 0)
+		breq.ifbr_pvid = IFBR_PVID_NULL;
+	else if (strcmp(val, "none") == 0)
+		breq.ifbr_pvid = IFBR_PVID_NONE;
+	else {
+		breq.ifbr_pvid = strtonum(val,
+		    IFBR_PVID_MIN, IFBR_PVID_MAX, &errstr);
+		if (errstr != NULL) {
+			err(1, "%s untagged %s: %s is %s",
+			    ifname, ifsname, val, errstr);
+		}
+	}
+
+	if (ioctl(sock, SIOCBRDGSPVID, &breq) == -1)
+		err(1, "%s untagged %s %s", ifname, ifsname, val);
+}
+
+void
+bridge_unpvid(const char *ifsname, int d)
+{
+	struct ifbreq breq;
+	const char *errstr;
+
+	strlcpy(breq.ifbr_name, ifname, sizeof(breq.ifbr_name));
+	strlcpy(breq.ifbr_ifsname, ifsname, sizeof(breq.ifbr_ifsname));
+	breq.ifbr_pvid = IFBR_PVID_NONE;
+
+	if (ioctl(sock, SIOCBRDGSPVID, &breq) == -1)
+		err(1, "%s -untagged %s", ifname, ifsname);
+}
+
+static void
+bridge_mod_vidmap(const char *ifsname, const char *val,
+    unsigned long req, const char *op)
+{
+	struct ifbrvidmap ifbrvm;
+	const char *errstr;
+	uint16_t vid;
+	unsigned int voff, vbit;
+
+	strlcpy(ifbrvm.ifbrvm_name, ifname, sizeof(ifbrvm.ifbrvm_name));
+	strlcpy(ifbrvm.ifbrvm_ifsname, ifsname, sizeof(ifbrvm.ifbrvm_ifsname));
+
+	vid = strtonum(val, EVL_VLID_MIN, EVL_VLID_MAX, &errstr);
+	if (errstr != NULL) {
+		err(1, "%s %s %s: %s is %s", ifname, op, ifsname, val,
+		    errstr);
+	}
+
+	memset(ifbrvm.ifbrvm_tags, 0, sizeof(ifbrvm.ifbrvm_tags));
+	voff = vid / 8;
+	vbit = vid % 8;
+
+	ifbrvm.ifbrvm_tags[voff] = 1U << vbit;
+	if (ioctl(sock, req, &ifbrvm) == -1)
+		err(1, "%s %s %s %s", ifname, op, ifsname, val);
+}
+
+void
+bridge_add_vid(const char *ifsname, const char *val)
+{
+	/* add tags to the vid map on a port */
+	bridge_mod_vidmap(ifsname, val, SIOCBRDGAVMAP, "+tagged");
+}
+
+void
+bridge_clr_vid(const char *ifsname, const char *val)
+{
+	/* clear tags from the vid map on a port */
+	bridge_mod_vidmap(ifsname, val, SIOCBRDGCVMAP, "-tagged");
+}
+
+void
 bridge_proto(const char *arg, int d)
 {
 	struct ifbrparam bp;
@@ -584,23 +680,6 @@ bridge_maxaddr(const char *arg, int d)
 }
 
 void
-bridge_deladdr(const char *addr, int d)
-{
-	struct ifbareq ifba;
-	struct ether_addr *ea;
-
-	strlcpy(ifba.ifba_name, ifname, sizeof(ifba.ifba_name));
-	ea = ether_aton(addr);
-	if (ea == NULL)
-		err(1, "Invalid address: %s", addr);
-
-	bcopy(ea, &ifba.ifba_dst, sizeof(struct ether_addr));
-
-	if (ioctl(sock, SIOCBRDGDADDR, &ifba) == -1)
-		err(1, "%s: %s", ifname, addr);
-}
-
-void
 bridge_ifprio(const char *ifsname, const char *val)
 {
 	struct ifbreq breq;
@@ -648,12 +727,72 @@ bridge_noifcost(const char *ifsname, int
 		err(1, "%s", ifname);
 }
 
+static int
+bridge_addr_vid_parse(struct ifbvareq *ifbva,
+    const char *addr, const char *vid)
+{
+	struct ether_addr *ea;
+	const char *errstr;
+
+	memset(ifbva, 0, sizeof(*ifbva));
+	strlcpy(ifbva->ifbva_name, ifname, sizeof(ifbva->ifbva_name));
+
+	ea = ether_aton(addr);
+	if (ea == NULL)
+		errx(1, "Invalid address: %s", addr);
+
+	ifbva->ifbva_dst = *ea;
+	ifbva->ifbva_vid = strtonum(vid, EVL_VLID_MIN, EVL_VLID_MAX, &errstr);
+	if (errstr != NULL)
+		errx(1, "Invalid vid %s: %s", vid, errstr);
+
+	return (0);
+}
+
+static int
+bridge_addr_vid(struct ifbvareq *ifbva, const char *addr, size_t sep)
+{
+	char *buf;
+	int rv;
+
+	buf = strdup(addr);
+	if (buf == NULL)
+		err(1, NULL);
+
+	buf[sep] = '\0';
+	rv = bridge_addr_vid_parse(ifbva, buf, buf + sep + 1);
+	free(buf);
+	return (rv);
+}
+
+void
+bridge_addvaddr(const char *ifsname, const char *addr, size_t sep)
+{
+	struct ifbvareq ifbva;
+
+	if (bridge_addr_vid(&ifbva, addr, sep) == -1)
+		errx(1, "unable to parse address%cvid", VID_SEP);
+
+	strlcpy(ifbva.ifbva_ifsname, ifsname, sizeof(ifbva.ifbva_ifsname));
+	ifbva.ifbva_flags = IFBAF_STATIC;
+
+	if (ioctl(sock, SIOCBRDGSVADDR, &ifbva) == -1)
+		err(1, "%s static %s %s", ifname, ifsname, addr);
+}
+
 void
 bridge_addaddr(const char *ifsname, const char *addr)
 {
+	char *chr;
 	struct ifbareq ifba;
 	struct ether_addr *ea;
 
+	chr = strchr(addr, VID_SEP);
+	if (chr != NULL) {
+		bridge_addvaddr(ifsname, addr, chr - addr);
+		return;
+	}
+
 	strlcpy(ifba.ifba_name, ifname, sizeof(ifba.ifba_name));
 	strlcpy(ifba.ifba_ifsname, ifsname, sizeof(ifba.ifba_ifsname));
 
@@ -668,6 +807,42 @@ bridge_addaddr(const char *ifsname, cons
 		err(1, "%s: %s", ifname, addr);
 }
 
+static void
+bridge_delvaddr(const char *addr, size_t sep)
+{
+	struct ifbvareq ifbva;
+
+	if (bridge_addr_vid(&ifbva, addr, sep) == -1)
+		errx(1, "unable to parse address%cvid", VID_SEP);
+
+	if (ioctl(sock, SIOCBRDGDVADDR, &ifbva) == -1)
+		err(1, "%s deladdr %s", ifname, addr);
+}
+
+void
+bridge_deladdr(const char *addr, int d)
+{
+	char *chr;
+	struct ifbareq ifba;
+	struct ether_addr *ea;
+
+	chr = strchr(addr, VID_SEP);
+	if (chr != NULL) {
+		bridge_delvaddr(addr, chr - addr);
+		return;
+	}
+
+	strlcpy(ifba.ifba_name, ifname, sizeof(ifba.ifba_name));
+	ea = ether_aton(addr);
+	if (ea == NULL)
+		err(1, "Invalid address: %s", addr);
+
+	bcopy(ea, &ifba.ifba_dst, sizeof(struct ether_addr));
+
+	if (ioctl(sock, SIOCBRDGDADDR, &ifba) == -1)
+		err(1, "%s: %s", ifname, addr);
+}
+
 void
 bridge_addendpoint(const char *endpoint, const char *addr)
 {
@@ -727,6 +902,79 @@ bridge_delendpoint(const char *addr, int
 		err(1, "%s -endpoint %s", ifname, addr);
 }
 
+static int
+bridge_vaddrs_try(const char *delim)
+{
+	char dstaddr[NI_MAXHOST];
+	char dstport[NI_MAXSERV];
+	const int niflag = NI_NUMERICHOST|NI_DGRAM;
+	struct ifbaconf ifbac;
+	struct ifbvareq *ifbva;
+	char *inbuf = NULL, buf[sizeof(ifbva->ifbva_ifsname) + 1], *inb;
+	struct sockaddr *sa;
+	int i, len = 8192;
+	struct timespec now;
+
+	while (1) {
+		ifbac.ifbac_len = len;
+		inb = realloc(inbuf, len);
+		if (inb == NULL)
+			err(1, "malloc");
+		ifbac.ifbac_buf = inbuf = inb;
+		strlcpy(ifbac.ifbac_name, ifname, sizeof(ifbac.ifbac_name));
+		if (ioctl(sock, SIOCBRDGVRTS, &ifbac) == -1) {
+			switch (errno) {
+			case ENETDOWN:
+				return (0);
+			case ENOTTY:
+				return (-1);
+			default:
+				err(1, "%s", ifname);
+			}
+		}
+		if (ifbac.ifbac_len + sizeof(*ifbva) < len)
+			break;
+		len *= 2;
+	}
+
+	clock_gettime(CLOCK_MONOTONIC, &now);
+
+	for (i = 0; i < ifbac.ifbac_len / sizeof(*ifbva); i++) {
+		ifbva = ifbac.ifbac_vreq + i;
+		strlcpy(buf, ifbva->ifbva_ifsname, sizeof(buf));
+		printf("%s%s", delim, ether_ntoa(&ifbva->ifbva_dst));
+		if (ifbva->ifbva_vid != EVL_VLID_NULL)
+			printf("%c%u", VID_SEP, ifbva->ifbva_vid);
+		if (buf[0] != '\0')
+			printf(" %s", buf);
+		sa = (struct sockaddr *)&ifbva->ifbva_dstsa;
+		printf(" created %lld used %lld",
+		    now.tv_sec - ifbva->ifbva_created,
+		    now.tv_sec - ifbva->ifbva_used);
+		printb(" flags", ifbva->ifbva_flags, IFBAFBITS);
+		if (sa->sa_family != AF_UNSPEC &&
+		    getnameinfo(sa, sa->sa_len,
+		    dstaddr, sizeof(dstaddr),
+		    dstport, sizeof(dstport), niflag) == 0)
+			printf(" tunnel %s:%s", dstaddr, dstport);
+		printf("\n");
+	}
+	free(inbuf);
+
+	return (0);
+}
+
+void
+bridge_vaddrs(const char *delim, int d)
+{
+	/* ifconfig will call us with the argv of the command */
+	if (strcmp(delim, "vaddr") == 0)
+		delim = "";
+
+	if (bridge_vaddrs_try(delim) == -1)
+		err(1, "%s", ifname);
+}
+
 void
 bridge_addrs(const char *delim, int d)
 {
@@ -847,7 +1095,50 @@ bridge_status(void)
 	printf("\tAddresses (max cache: %u, timeout: %u):\n",
 	    bp1.ifbrp_csize, bp2.ifbrp_ctime);
 
+	/* try the new version of the addrs ioctl first */
+	if (bridge_vaddrs_try("\t\t") == 0)
+		return;
+
 	bridge_addrs("\t\t", 0);
+}
+
+void
+bridge_vid_map(const char *ifsname)
+{
+	struct ifbrvidmap ifbrvm;
+	size_t i;
+	char sep = ' ';
+
+	strlcpy(ifbrvm.ifbrvm_name, ifname, sizeof(ifbrvm.ifbrvm_name));
+	strlcpy(ifbrvm.ifbrvm_ifsname, ifsname, sizeof(ifbrvm.ifbrvm_ifsname));
+
+	if (ioctl(sock, SIOCBRDGGVMAP, &ifbrvm) == -1) {
+		if (errno != ENOTTY)
+			warn("%s port %s get tagged", ifname, ifsname);
+		return;
+	}
+
+	printf("\t\t" "tagged:");
+
+	for (i = 0; i < sizeof(ifbrvm.ifbrvm_tags); i++) {
+		unsigned int voff = i * 8;
+		unsigned int vbit;
+		uint8_t tag = ifbrvm.ifbrvm_tags[i];
+
+		if (tag == 0)
+			continue;
+
+		for (vbit = 0; vbit < 8; vbit++) {
+			if (tag & (1U << vbit)) {
+				printf("%c%u", sep, voff + vbit);
+				sep = ',';
+			}
+		}
+	}
+
+	if (sep == ' ')
+		printf(" none");
+	printf("\n");
 }
 
 void
Index: sbin/ifconfig/ifconfig.8
===================================================================
RCS file: /cvs/src/sbin/ifconfig/ifconfig.8,v
diff -u -p -r1.406 ifconfig.8
--- sbin/ifconfig/ifconfig.8	21 Oct 2025 05:27:04 -0000	1.406
+++ sbin/ifconfig/ifconfig.8	29 Oct 2025 04:15:50 -0000
@@ -2078,7 +2078,7 @@ device will try to establish a data conn
 .Op Cm add Ar child-iface
 .Op Cm addspan Ar child-iface
 .Op Cm del Ar child-iface
-.Op Cm deladdr Ar address
+.Op Cm deladdr Ar address Ns Oo @ Ns Ar vid Oc
 .Op Cm delspan Ar child-iface
 .Op Oo Fl Oc Ns Cm discover Ar child-iface
 .Op Cm flushrule Ar interface
@@ -2091,8 +2091,15 @@ device will try to establish a data conn
 .Op Cm rule Ar filtering-rule
 .Op Cm rulefile Ar filename
 .Op Cm rules Ar interface
-.Op Cm static Ar interface Ar address
+.Op Cm static Ar child-iface Ar address Ns Oo @ Ns Ar vid Oc
 .Op Cm timeout Ar time
+.Op Cm +tagged Ar child-iface Ar vid
+.Op Cm -tagged Ar child-iface Ar vid
+.Op Cm untagged Ar child-iface Ar vid
+.Op Cm -untagged Ar child-iface
+.Op Cm rxprio Ar prio
+.Op Cm txprio Ar prio
+.Op Cm vnetid Ar vid
 .Op Cm up
 .Ek
 .nr nS 0
@@ -2112,10 +2119,15 @@ as a span port on the bridge.
 .It Cm del Ar child-iface
 Remove the member
 .Ar child-iface .
-.It Cm deladdr Ar address
+.It Cm deladdr Ar address Ns Oo @ Ns Ar vid Oc
 Delete
 .Ar address
+on VLAN
+.Ar vid
 from the cache.
+if
+.Ar vid
+is not specified it uses the default VLAN identifier on the bridge.
 .It Cm delspan Ar child-iface
 Delete
 .Ar child-iface
@@ -2268,9 +2280,17 @@ Load a set of rules from the file
 .It Cm rules Ar interface
 Display the active filtering rules in use on
 .Ar interface .
-.It Cm static Ar interface Ar address
-Add a static entry into the address cache pointing to
-.Ar interface .
+.It Cm static Ar child-iface Ar address Ns Oo @ Ns Ar vid Oc
+Add a static entry for
+.Ar address
+on VLAN
+.Ar vid
+into the address cache pointing to
+.Ar child-iface .
+If
+.Ar vid
+is not specified it defaults to the VLAN used by untagged packets on
+.Ar child-iface .
 Static entries are never aged out of the cache or replaced, even if the address
 is seen on a different interface.
 .It Cm timeout Ar time
@@ -2280,6 +2300,47 @@ The default is 240 seconds.
 If
 .Ar time
 is set to zero, then entries will not be expired.
+.It Cm +tagged Ar child-iface Ar vid
+Add VLAN
+.Ar vid
+to the set of identifiers than can be sent and received as VLAN
+tagged traffic on
+.Ar child-iface .
+By default the set of tagged VLAN is empty.
+.It Cm -tagged Ar child-iface Ar vid
+Remove VLAN
+.Ar vid
+to the set of identifiers than can be sent and received as VLAN
+tagged traffic on
+.Ar child-iface .
+.It Cm untagged Ar child-iface Ar vid
+Set untagged traffic on
+.Ar child-iface
+to operate in VLAN
+.Ar vid .
+By default ports are configured with the default VLAN identifier
+configured on the bridge.
+.It Cm -untagged Ar child-iface
+Don't allow untagged traffic on
+.Ar child-iface .
+.It Cm rxprio Ar prio
+Configure the handling of the VLAN priority field in received packets.
+This is compatible with the configuration of
+.Cm rxprio
+on
+.Xr vlan 4
+interfaces.
+.It Cm txprio Ar prio
+Configure the handling of the VLAN priority field in transmitted packets.
+This is compatible with the configuration of
+.Cm txprio
+on
+.Xr vlan 4
+interfaces.
+.It Cm vnetid Ar vid
+Set the default VLAN identifier for untagged packets for ports added
+to the bridge.
+The default is VLAN 1.
 .It Cm up
 Start forwarding packets.
 .El
Index: sbin/ifconfig/ifconfig.c
===================================================================
RCS file: /cvs/src/sbin/ifconfig/ifconfig.c,v
diff -u -p -r1.477 ifconfig.c
--- sbin/ifconfig/ifconfig.c	21 Oct 2025 05:14:22 -0000	1.477
+++ sbin/ifconfig/ifconfig.c	29 Oct 2025 04:15:50 -0000
@@ -572,6 +572,10 @@ const struct	cmd {
 	{ "-autoedge",	NEXTARG,	0,		unsetautoedge },
 	{ "protected",	NEXTARG2,	0,		NULL, bridge_protect },
 	{ "-protected",	NEXTARG,	0,		bridge_unprotect },
+	{ "untagged",	NEXTARG2,	0,		NULL, bridge_pvid },
+	{ "-untagged",	NEXTARG,	0,		bridge_unpvid },
+	{ "+tagged",	NEXTARG2,	0,		NULL, bridge_add_vid },
+	{ "-tagged",	NEXTARG2,	0,		NULL, bridge_clr_vid },
 	{ "ptp",	NEXTARG,	0,		setptp },
 	{ "-ptp",	NEXTARG,	0,		unsetptp },
 	{ "autoptp",	NEXTARG,	0,		setautoptp },
@@ -584,6 +588,7 @@ const struct	cmd {
 	{ "deladdr",	NEXTARG,	0,		bridge_deladdr },
 	{ "maxaddr",	NEXTARG,	0,		bridge_maxaddr },
 	{ "addr",	0,		0,		bridge_addrs },
+	{ "vaddr",	0,		0,		bridge_vaddrs },
 	{ "hellotime",	NEXTARG,	0,		bridge_hellotime },
 	{ "fwddelay",	NEXTARG,	0,		bridge_fwddelay },
 	{ "maxage",	NEXTARG,	0,		bridge_maxage },
Index: sbin/ifconfig/ifconfig.h
===================================================================
RCS file: /cvs/src/sbin/ifconfig/ifconfig.h,v
diff -u -p -r1.7 ifconfig.h
--- sbin/ifconfig/ifconfig.h	21 Oct 2025 05:14:22 -0000	1.7
+++ sbin/ifconfig/ifconfig.h	29 Oct 2025 04:15:50 -0000
@@ -55,11 +55,16 @@ void bridge_delendpoint(const char *, in
 void bridge_deladdr(const char *, int);
 void bridge_maxaddr(const char *, int);
 void bridge_addrs(const char *, int);
+void bridge_vaddrs(const char *, int);
 void bridge_hellotime(const char *, int);
 void bridge_fwddelay(const char *, int);
 void bridge_maxage(const char *, int);
 void bridge_protect(const char *, const char *);
 void bridge_unprotect(const char *, int);
+void bridge_pvid(const char *, const char *);
+void bridge_unpvid(const char *, int);
+void bridge_add_vid(const char *, const char *);
+void bridge_clr_vid(const char *, const char *);
 void bridge_proto(const char *, int);
 void bridge_ifprio(const char *, const char *);
 void bridge_ifcost(const char *, const char *);
Index: sys/net/if.c
===================================================================
RCS file: /cvs/src/sys/net/if.c,v
diff -u -p -r1.741 if.c
--- sys/net/if.c	9 Sep 2025 09:16:18 -0000	1.741
+++ sys/net/if.c	29 Oct 2025 04:15:50 -0000
@@ -2457,11 +2457,14 @@ forceup:
 	case SIOCBRDGADDS:
 	case SIOCBRDGDELS:
 	case SIOCBRDGSADDR:
+	case SIOCBRDGSVADDR:
 	case SIOCBRDGSTO:
 	case SIOCBRDGDADDR:
+	case SIOCBRDGDVADDR:
 	case SIOCBRDGFLUSH:
 	case SIOCBRDGADDL:
 	case SIOCBRDGSIFPROT:
+	case SIOCBRDGSPVID:
 	case SIOCBRDGARL:
 	case SIOCBRDGFRL:
 	case SIOCBRDGSPRI:
@@ -2472,6 +2475,9 @@ forceup:
 	case SIOCBRDGSIFCOST:
 	case SIOCBRDGSTXHC:
 	case SIOCBRDGSPROTO:
+	case SIOCBRDGSVMAP:
+	case SIOCBRDGAVMAP:
+	case SIOCBRDGCVMAP:
 #endif
 		if ((error = suser(p)) != 0)
 			break;
Index: sys/net/if_bpe.c
===================================================================
RCS file: /cvs/src/sys/net/if_bpe.c,v
diff -u -p -r1.25 if_bpe.c
--- sys/net/if_bpe.c	7 Jul 2025 02:28:50 -0000	1.25
+++ sys/net/if_bpe.c	29 Oct 2025 04:15:50 -0000
@@ -265,7 +265,7 @@ bpe_start(struct ifnet *ifp)
 
 			smr_read_enter();
 			endpoint = etherbridge_resolve_ea(&sc->sc_eb,
-			    (struct ether_addr *)ceh->ether_dhost);
+			    0, (struct ether_addr *)ceh->ether_dhost);
 			if (endpoint == NULL) {
 				/* "flood" to unknown hosts */
 				endpoint = &sc->sc_group;
@@ -710,13 +710,13 @@ bpe_add_addr(struct bpe_softc *sc, const
 	/* check endpoint for multicast or broadcast? */
 
 	return (etherbridge_add_addr(&sc->sc_eb, (void *)endpoint,
-	    &ifba->ifba_dst, type));
+	    0, &ifba->ifba_dst, type));
 }
 
 static int
 bpe_del_addr(struct bpe_softc *sc, const struct ifbareq *ifba)
 {
-	return (etherbridge_del_addr(&sc->sc_eb, &ifba->ifba_dst));
+	return (etherbridge_del_addr(&sc->sc_eb, 0, &ifba->ifba_dst));
 }
 
 static inline struct bpe_softc *
@@ -770,7 +770,7 @@ bpe_input(struct ifnet *ifp0, struct mbu
 	ceh = (struct ether_header *)(itagp + 1);
 
 	etherbridge_map_ea(&sc->sc_eb, ceh->ether_shost,
-	    (struct ether_addr *)beh->ether_shost);
+	    0, (struct ether_addr *)beh->ether_shost);
 
 	m_adj(m, sizeof(*beh) + sizeof(*itagp));
 
Index: sys/net/if_bridge.h
===================================================================
RCS file: /cvs/src/sys/net/if_bridge.h,v
diff -u -p -r1.74 if_bridge.h
--- sys/net/if_bridge.h	21 Oct 2025 05:09:32 -0000	1.74
+++ sys/net/if_bridge.h	29 Oct 2025 04:15:50 -0000
@@ -39,6 +39,11 @@
 #include <sys/timeout.h>
 #include <net/pfvar.h>
 
+#define IFBR_PVID_NULL		EVL_VLID_NULL
+#define IFBR_PVID_MIN		EVL_VLID_MIN
+#define IFBR_PVID_MAX		EVL_VLID_MAX
+#define IFBR_PVID_NONE		0xffff
+
 /*
  * Bridge control request: add/delete member interfaces.
  */
@@ -51,6 +56,7 @@ struct ifbreq {
 
 	u_int8_t	ifbr_state;		/* member stp state */
 	u_int8_t	ifbr_priority;		/* member stp priority */
+	u_int16_t	ifbr_pvid;		/* member port vlan id */
 	u_int32_t	ifbr_path_cost;		/* member stp path cost */
 	u_int32_t	ifbr_stpflags;          /* member stp flags */
 	u_int8_t	ifbr_proto;		/* member stp protocol */
@@ -134,15 +140,19 @@ struct ifbareq {
 #define	IFBAF_DYNAMIC		0x00		/* dynamically learned */
 #define	IFBAF_STATIC		0x01		/* static address */
 
+struct ifbvareq;
+
 struct ifbaconf {
 	char			ifbac_name[IFNAMSIZ];	/* bridge ifs name */
 	u_int32_t		ifbac_len;		/* buffer size */
 	union {
 		caddr_t	ifbacu_buf;			/* buffer */
 		struct ifbareq *ifbacu_req;		/* request pointer */
+		struct ifbvareq *ifbacu_vreq;		/* request pointer */
 	} ifbac_ifbacu;
 #define	ifbac_buf	ifbac_ifbacu.ifbacu_buf
 #define	ifbac_req	ifbac_ifbacu.ifbacu_req
+#define	ifbac_vreq	ifbac_ifbacu.ifbacu_vreq
 };
 
 struct ifbrparam {
@@ -150,6 +160,7 @@ struct ifbrparam {
 	union {
 		u_int32_t	ifbrpu_csize;		/* cache size */
 		int		ifbrpu_ctime;		/* cache time (sec) */
+		int		ifbrpu_dflt_vid;	/* default vlan */
 		u_int16_t	ifbrpu_prio;		/* bridge priority */
 		u_int8_t	ifbrpu_hellotime;	/* hello time (sec) */
 		u_int8_t	ifbrpu_fwddelay;	/* fwd delay (sec) */
@@ -236,6 +247,23 @@ struct ifbrlconf {
 	} ifbrl_ifbrlu;
 #define	ifbrl_buf	ifbrl_ifbrlu.ifbrlu_buf
 #define	ifbrl_req	ifbrl_ifbrlu.ifbrlu_req
+};
+
+struct ifbvareq {
+	char			ifbva_name[IFNAMSIZ];	/* bridge name */
+	char			ifbva_ifsname[IFNAMSIZ];	/* destination ifs */
+	time_t			ifbva_created;		/* monotime */
+	time_t			ifbva_used;		/* monotime */
+	unsigned int		ifbva_flags;		/* address flags */
+	uint16_t		ifbva_vid;		/* vlan */
+	struct ether_addr	ifbva_dst;		/* destination addr */
+	struct sockaddr_storage	ifbva_dstsa;		/* tunnel endpoint */
+};
+
+struct ifbrvidmap {
+	char			ifbrvm_name[IFNAMSIZ];
+	char			ifbrvm_ifsname[IFNAMSIZ];
+	uint8_t			ifbrvm_tags[512];
 };
 
 #ifdef _KERNEL
Index: sys/net/if_etherbridge.c
===================================================================
RCS file: /cvs/src/sys/net/if_etherbridge.c,v
diff -u -p -r1.8 if_etherbridge.c
--- sys/net/if_etherbridge.c	7 Jul 2025 02:28:50 -0000	1.8
+++ sys/net/if_etherbridge.c	29 Oct 2025 04:15:50 -0000
@@ -239,20 +239,22 @@ ebe_free(void *arg)
 }
 
 void *
-etherbridge_resolve_ea(struct etherbridge *eb,
+etherbridge_resolve_ea(struct etherbridge *eb, uint16_t vid,
     const struct ether_addr *ea)
 {
-	return (etherbridge_resolve(eb, ether_addr_to_e64(ea)));
+	return (etherbridge_resolve(eb, vid, ether_addr_to_e64(ea)));
 }
 
 void *
-etherbridge_resolve(struct etherbridge *eb, uint64_t eba)
+etherbridge_resolve(struct etherbridge *eb, uint16_t vid, uint64_t eba)
 {
-	struct eb_list *ebl = etherbridge_list(eb, eba);
+	struct eb_list *ebl;
 	struct eb_entry *ebe;
 
 	SMR_ASSERT_CRITICAL();
 
+	eba |= (uint64_t)vid << 48;
+	ebl = etherbridge_list(eb, eba);
 	ebe = ebl_find(ebl, eba);
 	if (ebe != NULL) {
 		if (ebe->ebe_type == EBE_DYNAMIC) {
@@ -268,14 +270,14 @@ etherbridge_resolve(struct etherbridge *
 }
 
 void
-etherbridge_map_ea(struct etherbridge *eb, void *port,
+etherbridge_map_ea(struct etherbridge *eb, void *port, uint16_t vid,
     const struct ether_addr *ea)
 {
-	etherbridge_map(eb, port, ether_addr_to_e64(ea));
+	etherbridge_map(eb, port, vid, ether_addr_to_e64(ea));
 }
 
 void
-etherbridge_map(struct etherbridge *eb, void *port, uint64_t eba)
+etherbridge_map(struct etherbridge *eb, void *port, uint16_t vid, uint64_t eba)
 {
 	struct eb_list *ebl;
 	struct eb_entry *oebe, *nebe;
@@ -288,6 +290,8 @@ etherbridge_map(struct etherbridge *eb, 
 		return;
 
 	now = getuptime();
+
+	eba |= (uint64_t)vid << 48;
 	ebl = etherbridge_list(eb, eba);
 
 	smr_read_enter();
@@ -332,6 +336,7 @@ etherbridge_map(struct etherbridge *eb, 
 	nebe->ebe_addr = eba;
 	nebe->ebe_port = nport;
 	nebe->ebe_type = EBE_DYNAMIC;
+	nebe->ebe_created = now;
 	nebe->ebe_age = now;
 
 	mtx_enter(&eb->eb_lock);
@@ -385,14 +390,15 @@ etherbridge_map(struct etherbridge *eb, 
 
 int
 etherbridge_add_addr(struct etherbridge *eb, void *port,
-    const struct ether_addr *ea, unsigned int type)
+    uint16_t vid, const struct ether_addr *ea, unsigned int type)
 {
-	uint64_t eba = ether_addr_to_e64(ea);
+	uint64_t eba = ether_addr_to_e64(ea) | (uint64_t)vid << 48;
 	struct eb_list *ebl;
 	struct eb_entry *nebe;
 	unsigned int num;
 	void *nport;
 	int error = 0;
+	time_t now;
 
 	if (ETH64_IS_MULTICAST(eba) || ETH64_IS_ANYADDR(eba))
 		return (EADDRNOTAVAIL);
@@ -407,13 +413,16 @@ etherbridge_add_addr(struct etherbridge 
 		return (ENOMEM);
 	}
 
+	now = getuptime();
+
 	smr_init(&nebe->ebe_smr_entry);
 	nebe->ebe_etherbridge = eb;
 
 	nebe->ebe_addr = eba;
 	nebe->ebe_port = nport;
 	nebe->ebe_type = type;
-	nebe->ebe_age = getuptime();
+	nebe->ebe_created = now;
+	nebe->ebe_age = now;
 
 	ebl = etherbridge_list(eb, eba);
 
@@ -441,9 +450,10 @@ etherbridge_add_addr(struct etherbridge 
 	return (error);
 }
 int
-etherbridge_del_addr(struct etherbridge *eb, const struct ether_addr *ea)
+etherbridge_del_addr(struct etherbridge *eb, uint16_t vid,
+    const struct ether_addr *ea)
 {
-	uint64_t eba = ether_addr_to_e64(ea);
+	uint64_t eba = ether_addr_to_e64(ea) | (uint64_t)vid << 48;
 	struct eb_list *ebl;
 	struct eb_entry *oebe;
 	const struct eb_entry key = {
@@ -654,6 +664,67 @@ etherbridge_rtfind(struct etherbridge *e
 	}
 	nlen = baconf->ifbac_len;
 	baconf->ifbac_len = eb->eb_num * sizeof(bareq);
+	mtx_leave(&eb->eb_lock);
+
+	error = copyout(buf, baconf->ifbac_buf, len);
+	free(buf, M_TEMP, nlen);
+
+	return (error);
+}
+
+int
+etherbridge_vareq(struct etherbridge *eb, struct ifbaconf *baconf)
+{
+	struct eb_entry *ebe;
+	struct ifbvareq bvareq;
+	caddr_t buf;
+	size_t len, nlen;
+	int error;
+
+	if (baconf->ifbac_len == 0) {
+		/* single read is atomic */
+		baconf->ifbac_len = eb->eb_num * sizeof(bvareq);
+		return (0);
+	}
+
+	buf = malloc(baconf->ifbac_len, M_TEMP, M_WAITOK|M_CANFAIL);
+	if (buf == NULL)
+		return (ENOMEM);
+	len = 0;
+
+	mtx_enter(&eb->eb_lock);
+	RBT_FOREACH(ebe, eb_tree, &eb->eb_tree) {
+		nlen = len + sizeof(bvareq);
+		if (nlen > baconf->ifbac_len)
+			break;
+
+		strlcpy(bvareq.ifbva_name, eb->eb_name,
+		    sizeof(bvareq.ifbva_name));
+		eb_port_ifname(eb,
+		    bvareq.ifbva_ifsname, sizeof(bvareq.ifbva_ifsname),
+		    ebe->ebe_port);
+		bvareq.ifbva_created = ebe->ebe_created;
+		bvareq.ifbva_used = ebe->ebe_age;
+		bvareq.ifbva_vid = ebe->ebe_addr >> 48;
+		ether_e64_to_addr(&bvareq.ifbva_dst, ebe->ebe_addr);
+
+		memset(&bvareq.ifbva_dstsa, 0, sizeof(bvareq.ifbva_dstsa));
+		eb_port_sa(eb, &bvareq.ifbva_dstsa, ebe->ebe_port);
+
+		switch (ebe->ebe_type) {
+		case EBE_DYNAMIC:
+			bvareq.ifbva_flags = IFBAF_DYNAMIC;
+			break;
+		case EBE_STATIC:
+			bvareq.ifbva_flags = IFBAF_STATIC;
+			break;
+		}
+
+		memcpy(buf + len, &bvareq, sizeof(bvareq));
+		len = nlen;
+	}
+	nlen = baconf->ifbac_len;
+	baconf->ifbac_len = eb->eb_num * sizeof(bvareq);
 	mtx_leave(&eb->eb_lock);
 
 	error = copyout(buf, baconf->ifbac_buf, len);
Index: sys/net/if_etherbridge.h
===================================================================
RCS file: /cvs/src/sys/net/if_etherbridge.h,v
diff -u -p -r1.5 if_etherbridge.h
--- sys/net/if_etherbridge.h	4 Nov 2024 00:13:15 -0000	1.5
+++ sys/net/if_etherbridge.h	29 Oct 2025 04:15:50 -0000
@@ -48,6 +48,7 @@ struct eb_entry {
 #define EBE_DYNAMIC				0x0
 #define EBE_STATIC				0x1
 #define EBE_DEAD				0xdead
+	time_t				 ebe_created;
 	time_t				 ebe_age;
 
 	struct etherbridge		*ebe_etherbridge;
@@ -80,12 +81,12 @@ int	 etherbridge_up(struct etherbridge *
 int	 etherbridge_down(struct etherbridge *);
 void	 etherbridge_destroy(struct etherbridge *);
 
-void	 etherbridge_map(struct etherbridge *, void *, uint64_t);
+void	 etherbridge_map(struct etherbridge *, void *, uint16_t, uint64_t);
 void	 etherbridge_map_ea(struct etherbridge *, void *,
-	     const struct ether_addr *);
-void	*etherbridge_resolve(struct etherbridge *, uint64_t);
+	     uint16_t, const struct ether_addr *);
+void	*etherbridge_resolve(struct etherbridge *, uint16_t, uint64_t);
 void	*etherbridge_resolve_ea(struct etherbridge *,
-	     const struct ether_addr *);
+	     uint16_t, const struct ether_addr *);
 void	 etherbridge_detach_port(struct etherbridge *, void *);
 
 /* ioctl support */
@@ -94,9 +95,11 @@ int	 etherbridge_get_max(struct etherbri
 int	 etherbridge_set_tmo(struct etherbridge *, struct ifbrparam *);
 int	 etherbridge_get_tmo(struct etherbridge *, struct ifbrparam *);
 int	 etherbridge_rtfind(struct etherbridge *, struct ifbaconf *);
+int	 etherbridge_vareq(struct etherbridge *, struct ifbaconf *);
 int	 etherbridge_add_addr(struct etherbridge *, void *,
-	     const struct ether_addr *, unsigned int);
-int	 etherbridge_del_addr(struct etherbridge *, const struct ether_addr *);
+	     uint16_t, const struct ether_addr *, unsigned int);
+int	 etherbridge_del_addr(struct etherbridge *,
+	     uint16_t, const struct ether_addr *);
 void	 etherbridge_flush(struct etherbridge *, uint32_t);
 
 #endif /* _NET_ETHERBRIDGE_H_ */
Index: sys/net/if_gre.c
===================================================================
RCS file: /cvs/src/sys/net/if_gre.c,v
diff -u -p -r1.190 if_gre.c
--- sys/net/if_gre.c	7 Jul 2025 02:28:50 -0000	1.190
+++ sys/net/if_gre.c	29 Oct 2025 04:15:50 -0000
@@ -1460,7 +1460,7 @@ nvgre_input(const struct gre_tunnel *key
 
 	eh = mtod(m, struct ether_header *);
 	etherbridge_map_ea(&sc->sc_eb, (void *)&key->t_dst,
-	    (struct ether_addr *)eh->ether_shost);
+	    0, (struct ether_addr *)eh->ether_shost);
 
 	SET(m->m_pkthdr.csum_flags, M_FLOWID);
 	m->m_pkthdr.ph_flowid = bemtoh32(&key->t_key) & ~GRE_KEY_ENTROPY;
@@ -3711,13 +3711,13 @@ nvgre_add_addr(struct nvgre_softc *sc, c
 	}
 
 	return (etherbridge_add_addr(&sc->sc_eb, &endpoint,
-	    &ifba->ifba_dst, type));
+	    0, &ifba->ifba_dst, type));
 }
 
 static int
 nvgre_del_addr(struct nvgre_softc *sc, const struct ifbareq *ifba)
 {
-	return (etherbridge_del_addr(&sc->sc_eb, &ifba->ifba_dst));
+	return (etherbridge_del_addr(&sc->sc_eb, 0, &ifba->ifba_dst));
 }
 
 static void
@@ -3753,7 +3753,7 @@ nvgre_start(struct ifnet *ifp)
 
 			smr_read_enter();
 			endpoint = etherbridge_resolve_ea(&sc->sc_eb,
-			    (struct ether_addr *)eh->ether_dhost);
+			    0, (struct ether_addr *)eh->ether_dhost);
 			if (endpoint == NULL) {
 				/* "flood" to unknown hosts */
 				endpoint = &tunnel->t_dst;
Index: sys/net/if_veb.c
===================================================================
RCS file: /cvs/src/sys/net/if_veb.c,v
diff -u -p -r1.45 if_veb.c
--- sys/net/if_veb.c	21 Oct 2025 05:13:20 -0000	1.45
+++ sys/net/if_veb.c	29 Oct 2025 04:15:50 -0000
@@ -63,6 +63,11 @@
 #include <net/if_vlan_var.h>
 #endif
 
+/* there are (basically) 4096 vids (vlan tags) */
+#define VEB_VID_COUNT		4096
+#define VEB_VID_BYTES		(VEB_VID_COUNT / 8)
+#define VEB_VID_WORDS		(VEB_VID_BYTES / sizeof(uint32_t))
+
 /* SIOCBRDGIFFLGS, SIOCBRDGIFFLGS */
 #define VEB_IFBIF_FLAGS	\
 	(IFBIF_LOCKED|IFBIF_LEARNING|IFBIF_DISCOVER|IFBIF_BLOCKNONIP)
@@ -125,6 +130,8 @@ struct veb_port {
 	unsigned int			 p_link_state;
 	unsigned int			 p_bif_flags;
 	uint32_t			 p_protected;
+	uint16_t			 p_pvid;
+	uint32_t			*p_vid_map;
 
 	struct veb_rules		 p_vrl;
 	unsigned int			 p_nvrl;
@@ -145,6 +152,9 @@ struct veb_softc {
 	unsigned int			 sc_dead;
 
 	struct etherbridge		 sc_eb;
+	int				 sc_dflt_vid;
+	int				 sc_txprio;
+	int				 sc_rxprio;
 
 	struct rwlock			 sc_rule_lock;
 	struct veb_ports		*sc_ports;
@@ -165,6 +175,8 @@ static int	veb_enqueue(struct ifnet *, s
 static int	veb_output(struct ifnet *, struct mbuf *, struct sockaddr *,
 		    struct rtentry *);
 static void	veb_start(struct ifqueue *);
+static struct mbuf *
+		veb_offload(struct ifnet *, struct ifnet *, struct mbuf *);
 
 static int	veb_up(struct veb_softc *);
 static int	veb_down(struct veb_softc *);
@@ -204,8 +216,17 @@ static int	veb_port_set_flags(struct veb
 static int	veb_port_get_flags(struct veb_softc *, struct ifbreq *);
 static int	veb_port_set_protected(struct veb_softc *,
 		    const struct ifbreq *);
+static int	veb_port_set_pvid(struct veb_softc *,
+		    const struct ifbreq *);
 static int	veb_add_addr(struct veb_softc *, const struct ifbareq *);
+static int	veb_add_vid_addr(struct veb_softc *, const struct ifbvareq *);
 static int	veb_del_addr(struct veb_softc *, const struct ifbareq *);
+static int	veb_del_vid_addr(struct veb_softc *, const struct ifbvareq *);
+
+static int	veb_get_vid_map(struct veb_softc *, struct ifbrvidmap *);
+static int	veb_set_vid_map(struct veb_softc *, const struct ifbrvidmap *);
+static int	veb_add_vid_map(struct veb_softc *, const struct ifbrvidmap *);
+static int	veb_clr_vid_map(struct veb_softc *, const struct ifbrvidmap *);
 
 static int	veb_rule_add(struct veb_softc *, const struct ifbrlreq *);
 static int	veb_rule_list_flush(struct veb_softc *,
@@ -294,6 +315,10 @@ veb_clone_create(struct if_clone *ifc, i
 		return (error);
 	}
 
+	sc->sc_dflt_vid = 1;
+	sc->sc_txprio = IF_HDRPRIO_PACKET;
+	sc->sc_rxprio = IF_HDRPRIO_OUTER;
+
 	ifp->if_softc = sc;
 	ifp->if_type = IFT_BRIDGE;
 	ifp->if_hdrlen = ETHER_HDR_LEN;
@@ -441,6 +466,9 @@ veb_span(struct veb_softc *sc, struct mb
 			continue;
 		}
 
+		if ((m = veb_offload(&sc->sc_if, ifp0, m)) == NULL)
+			continue;
+
 		if_enqueue(ifp0, m); /* XXX count error */
 	}
 	refcnt_rele_wake(&sm->m_refs);
@@ -451,9 +479,6 @@ veb_ip_filter(const struct mbuf *m)
 {
 	const struct ether_header *eh;
 
-	if (ISSET(m->m_flags, M_VLANTAG))
-		return (1);
-
 	eh = mtod(m, struct ether_header *);
 	switch (ntohs(eh->ether_type)) {
 	case ETHERTYPE_IP:
@@ -469,23 +494,32 @@ veb_ip_filter(const struct mbuf *m)
 }
 
 static int
-veb_vlan_filter(const struct mbuf *m)
+veb_svlan_filter(const struct mbuf *m)
 {
 	const struct ether_header *eh;
 
-	if (ISSET(m->m_flags, M_VLANTAG))
-		return (1);
-
 	eh = mtod(m, struct ether_header *);
-	switch (ntohs(eh->ether_type)) {
-	case ETHERTYPE_VLAN:
-	case ETHERTYPE_QINQ:
-		return (1);
-	default:
-		break;
+
+	return (eh->ether_type == htons(ETHERTYPE_QINQ));
+}
+
+static int
+veb_vid_map_filter(struct veb_port *p, uint16_t vid)
+{
+	uint32_t *map;
+	int drop = 1;
+
+	smr_read_enter();
+	map = SMR_PTR_GET(&p->p_vid_map);
+	if (map != NULL) {
+		unsigned int off = vid / 32;
+		unsigned int bit = vid % 32;
+
+		drop = !ISSET(map[off], 1U << bit);
 	}
+	smr_read_leave();
 
-	return (0);
+	return (drop);
 }
 
 static int
@@ -494,9 +528,6 @@ veb_rule_arp_match(const struct veb_rule
 	struct ether_header *eh;
 	struct ether_arp ea;
 
-	if (ISSET(m->m_flags, M_VLANTAG))
-		return (0);
-
 	eh = mtod(m, struct ether_header *);
 
 	if (eh->ether_type != htons(ETHERTYPE_ARP))
@@ -544,11 +575,13 @@ veb_rule_arp_match(const struct veb_rule
 
 static int
 veb_rule_list_test(struct veb_rule *vr, int dir, struct mbuf *m,
-    uint64_t src, uint64_t dst)
+    uint64_t src, uint64_t dst, uint16_t vid)
 {
 	SMR_ASSERT_CRITICAL();
 
 	do {
+		/* XXX check vid */
+
 		if (ISSET(vr->vr_flags, VEB_R_F_ARP|VEB_R_F_RARP) &&
 		    !veb_rule_arp_match(vr, m))
 			continue;
@@ -574,7 +607,7 @@ veb_rule_list_test(struct veb_rule *vr, 
 
 static inline int
 veb_rule_filter(struct veb_port *p, int dir, struct mbuf *m,
-    uint64_t src, uint64_t dst)
+    uint64_t src, uint64_t dst, uint16_t vid)
 {
 	struct veb_rule *vr;
 	int filter = VEB_R_PASS;
@@ -582,7 +615,7 @@ veb_rule_filter(struct veb_port *p, int 
 	smr_read_enter();
 	vr = SMR_TAILQ_FIRST(&p->p_vr_list[dir]);
 	if (vr != NULL)
-		filter = veb_rule_list_test(vr, dir, m, src, dst);
+		filter = veb_rule_list_test(vr, dir, m, src, dst, vid);
 	smr_read_leave();
 
 	return (filter == VEB_R_BLOCK);
@@ -627,9 +660,6 @@ veb_pf(struct ifnet *ifp0, int dir, stru
 	if (ifp0->if_enqueue == vport_enqueue)
 		return (m);
 
-	if (ISSET(m->m_flags, M_VLANTAG))
-		return (m);
-
 	eh = mtod(m, struct ether_header *);
 	switch (ntohs(eh->ether_type)) {
 	case ETHERTYPE_IP:
@@ -798,9 +828,6 @@ veb_ipsec_in(struct ifnet *ifp0, struct 
 	if (ifp0->if_enqueue == vport_enqueue)
 		return (m);
 
-	if (ISSET(m->m_flags, M_VLANTAG))
-		return (m);
-
 	eh = mtod(m, struct ether_header *);
 	switch (ntohs(eh->ether_type)) {
 	case ETHERTYPE_IP:
@@ -903,9 +930,6 @@ veb_ipsec_out(struct ifnet *ifp0, struct
 	if (ifp0->if_enqueue == vport_enqueue)
 		return (m);
 
-	if (ISSET(m->m_flags, M_VLANTAG))
-		return (m);
-
 	eh = mtod(m, struct ether_header *);
 	switch (ntohs(eh->ether_type)) {
 	case ETHERTYPE_IP:
@@ -1017,7 +1041,7 @@ veb_offload(struct ifnet *ifp, struct if
 
 static void
 veb_broadcast(struct veb_softc *sc, struct veb_port *rp, struct mbuf *m0,
-    uint64_t src, uint64_t dst, struct netstack *ns)
+    uint64_t src, uint64_t dst, uint16_t vid, struct netstack *ns)
 {
 	struct ifnet *ifp = &sc->sc_if;
 	struct veb_ports *pm;
@@ -1027,23 +1051,25 @@ veb_broadcast(struct veb_softc *sc, stru
 	struct mbuf *m;
 	unsigned int i;
 
+	if (rp->p_pvid == vid) { /* XXX which vlan is the right one? */
 #if NPF > 0
-	/*
-	 * we couldn't find a specific port to send this packet to,
-	 * but pf should still have a chance to apply policy to it.
-	 * let pf look at it, but use the veb interface as a proxy.
-	 */
-	if (ISSET(ifp->if_flags, IFF_LINK1) &&
-	    (m0 = veb_pf(ifp, PF_FWD, m0, ns)) == NULL)
-		return;
+		/*
+		 * we couldn't find a specific port to send this packet to,
+		 * but pf should still have a chance to apply policy to it.
+		 * let pf look at it, but use the veb interface as a proxy.
+		 */
+		if (ISSET(ifp->if_flags, IFF_LINK1) &&
+		    (m0 = veb_pf(ifp, PF_FWD, m0, ns)) == NULL)
+			return;
 #endif
 
 #if 0 && defined(IPSEC)
-	/* same goes for ipsec */
-	if (ISSET(ifp->if_flags, IFF_LINK2) &&
-	    (m0 = veb_ipsec_out(ifp, m0)) == NULL)
-		return;
+		/* same goes for ipsec */
+		if (ISSET(ifp->if_flags, IFF_LINK2) &&
+		    (m0 = veb_ipsec_out(ifp, m0)) == NULL)
+			return;
 #endif
+	}
 
 	counters_pkt(ifp->if_counters, ifc_opackets, ifc_obytes,
 	    m0->m_pkthdr.len);
@@ -1069,6 +1095,11 @@ veb_broadcast(struct veb_softc *sc, stru
 			continue;
 		}
 
+		if (vid != tp->p_pvid) {
+			if (veb_vid_map_filter(tp, vid))
+				continue;
+		}
+
 		ifp0 = tp->p_ifp0;
 		if (!ISSET(ifp0->if_flags, IFF_RUNNING)) {
 			/* don't waste time */
@@ -1081,18 +1112,21 @@ veb_broadcast(struct veb_softc *sc, stru
 			continue;
 		}
 
-		if (veb_rule_filter(tp, VEB_RULE_LIST_OUT, m0, src, dst))
+		if (veb_rule_filter(tp, VEB_RULE_LIST_OUT, m0, src, dst, vid))
 			continue;
 
-		if ((m0 = veb_offload(ifp, ifp0, m0)) == NULL)
-			goto rele;
-
 		m = m_dup_pkt(m0, max_linkhdr + ETHER_ALIGN, M_NOWAIT);
 		if (m == NULL) {
 			/* XXX count error? */
 			continue;
 		}
 
+		if (vid == tp->p_pvid)
+			CLR(m->m_flags, M_VLANTAG);
+
+		if ((m = veb_offload(ifp, ifp0, m)) == NULL)
+			goto rele;
+
 		(*tp->p_enqueue)(ifp0, m); /* XXX count error */
 	}
  rele:
@@ -1104,7 +1138,8 @@ done:
 
 static struct mbuf *
 veb_transmit(struct veb_softc *sc, struct veb_port *rp, struct veb_port *tp,
-    struct mbuf *m, uint64_t src, uint64_t dst, struct netstack *ns)
+    struct mbuf *m, uint64_t src, uint64_t dst, uint16_t vid,
+    struct netstack *ns)
 {
 	struct ifnet *ifp = &sc->sc_if;
 	struct ifnet *ifp0;
@@ -1120,23 +1155,33 @@ veb_transmit(struct veb_softc *sc, struc
 		goto drop;
 	}
 
-	if (veb_rule_filter(tp, VEB_RULE_LIST_OUT, m, src, dst))
+	/* pvid or tagged config can override address entries */
+	if (vid != tp->p_pvid) {
+		if (veb_vid_map_filter(tp, vid))
+			goto drop;
+	}
+
+	if (veb_rule_filter(tp, VEB_RULE_LIST_OUT, m, src, dst, vid))
 		goto drop;
 
 	ifp0 = tp->p_ifp0;
 
+	if (vid == tp->p_pvid) {
 #if 0 && defined(IPSEC)
-	if (ISSET(ifp->if_flags, IFF_LINK2) &&
-	    (m = veb_ipsec_out(ifp0, m0)) == NULL)
-		return;
+		if (ISSET(ifp->if_flags, IFF_LINK2) &&
+		    (m = veb_ipsec_out(ifp0, m0)) == NULL)
+			return;
 #endif
 
 #if NPF > 0
-	if (ISSET(ifp->if_flags, IFF_LINK1) &&
-	    (m = veb_pf(ifp0, PF_FWD, m, ns)) == NULL)
-		return (NULL);
+		if (ISSET(ifp->if_flags, IFF_LINK1) &&
+		    (m = veb_pf(ifp0, PF_FWD, m, ns)) == NULL)
+			return (NULL);
 #endif
 
+		CLR(m->m_flags, M_VLANTAG);
+	}
+
 	counters_pkt(ifp->if_counters, ifc_opackets, ifc_obytes,
 	    m->m_pkthdr.len);
 
@@ -1158,6 +1203,7 @@ veb_vport_input(struct ifnet *ifp0, stru
 	return (m);
 }
 
+
 static struct mbuf *
 veb_port_input(struct ifnet *ifp0, struct mbuf *m, uint64_t dst, void *brport,
     struct netstack *ns)
@@ -1167,6 +1213,8 @@ veb_port_input(struct ifnet *ifp0, struc
 	struct ifnet *ifp = &sc->sc_if;
 	struct ether_header *eh;
 	uint64_t src;
+	uint16_t vid = p->p_pvid;
+	int prio;
 #if NBPFILTER > 0
 	caddr_t if_bpf;
 #endif
@@ -1174,20 +1222,6 @@ veb_port_input(struct ifnet *ifp0, struc
 	if (!ISSET(ifp->if_flags, IFF_RUNNING))
 		return (m);
 
-	eh = mtod(m, struct ether_header *);
-	src = ether_addr_to_e64((struct ether_addr *)eh->ether_shost);
-
-	if (ISSET(p->p_bif_flags, IFBIF_LOCKED)) {
-		struct veb_port *rp;
-
-		smr_read_enter();
-		rp = etherbridge_resolve(&sc->sc_eb, src);
-		smr_read_leave();
-
-		if (rp != p)
-			goto drop;
-	}
-
 	/* Is this a MAC Bridge component Reserved address? */
 	if (ETH64_IS_8021_RSVD(dst)) {
 		if (!ISSET(ifp->if_flags, IFF_LINK0)) {
@@ -1198,7 +1232,7 @@ veb_port_input(struct ifnet *ifp0, struc
 			goto drop;
 		}
 
-		 /* look at the last nibble of the 802.1 reserved address */
+		/* look at the last nibble of the 802.1 reserved address */
 		switch (dst & 0xf) {
 		case 0x0: /* Nearest Customer Bridge Group Address */
 		case 0xb: /* EDE-SS PEP (IEEE Std 802.1AEcg) */
@@ -1211,9 +1245,77 @@ veb_port_input(struct ifnet *ifp0, struc
 		}
 	}
 
+	eh = mtod(m, struct ether_header *);
+	if (!ISSET(m->m_flags, M_VLANTAG) &&
+	    eh->ether_type == htons(ETHERTYPE_VLAN)) {
+		struct ether_vlan_header *evl;
+
+		evl = mtod(m, struct ether_vlan_header *);
+		m->m_pkthdr.ether_vtag = ntohs(evl->evl_tag);
+		SET(m->m_flags, M_VLANTAG);
+
+		memmove((caddr_t)evl + EVL_ENCAPLEN, evl,
+		    offsetof(struct ether_vlan_header, evl_encap_proto));
+		m_adj(m, EVL_ENCAPLEN);
+
+		eh = mtod(m, struct ether_header *);
+	}
+
+	if (ISSET(m->m_flags, M_VLANTAG)) {
+		uint16_t tvid = EVL_VLANOFTAG(m->m_pkthdr.ether_vtag);
+
+		if (tvid == EVL_VLID_NULL) {
+			CLR(m->m_flags, M_VLANTAG);
+		} else if (veb_vid_map_filter(p, tvid)) {
+			/* count vlan tagged drop */
+			goto drop;
+		} else
+			vid = tvid;
+
+		prio = sc->sc_rxprio;
+		switch (prio) {
+		case IF_HDRPRIO_PACKET:
+			break;
+		case IF_HDRPRIO_OUTER:
+			prio = EVL_PRIOFTAG(m->m_pkthdr.ether_vtag);
+			/* IEEE 802.1p has prio 0 and 1 swapped */
+			if (prio <= 1)
+				prio = !prio;
+			/* FALLTHROUGH */
+		default:
+			m->m_pkthdr.pf.prio = prio;
+			break;
+		}
+	}
+
+	if (vid == IFBR_PVID_NONE)
+		goto drop;
+
+	src = ether_addr_to_e64((struct ether_addr *)eh->ether_shost);
+
+	if (ISSET(p->p_bif_flags, IFBIF_LOCKED)) {
+		struct veb_port *rp;
+
+		smr_read_enter();
+		rp = etherbridge_resolve(&sc->sc_eb, vid, src);
+		smr_read_leave();
+
+		if (rp != p)
+			goto drop;
+	}
+
 	counters_pkt(ifp->if_counters, ifc_ipackets, ifc_ibytes,
 	    m->m_pkthdr.len);
 
+	/*
+	 * set things up so we show BPF on veb which vlan this
+	 * packet is on. i can't decide if the txprio or rxprio is
+	 * better here, so i went with the third option of doing
+	 * nothing. - dlg
+	 */
+	SET(m->m_flags, M_VLANTAG);
+	m->m_pkthdr.ether_vtag = vid;
+
 	/* force packets into the one routing domain for pf */
 	m->m_pkthdr.ph_rtableid = ifp->if_rdomain;
 
@@ -1232,10 +1334,10 @@ veb_port_input(struct ifnet *ifp0, struc
 		goto drop;
 
 	if (!ISSET(ifp->if_flags, IFF_LINK0) &&
-	    veb_vlan_filter(m))
+	    veb_svlan_filter(m))
 		goto drop;
 
-	if (veb_rule_filter(p, VEB_RULE_LIST_IN, m, src, dst))
+	if (veb_rule_filter(p, VEB_RULE_LIST_IN, m, src, dst, vid))
 		goto drop;
 
 #if NPF > 0
@@ -1253,7 +1355,14 @@ veb_port_input(struct ifnet *ifp0, struc
 	eh = mtod(m, struct ether_header *);
 
 	if (ISSET(p->p_bif_flags, IFBIF_LEARNING))
-		etherbridge_map(&sc->sc_eb, p, src);
+		etherbridge_map(&sc->sc_eb, p, vid, src);
+
+	prio = sc->sc_txprio;
+	prio = (prio == IF_HDRPRIO_PACKET) ? m->m_pkthdr.pf.prio : prio;
+	/* IEEE 802.1p has prio 0 and 1 swapped */
+	if (prio <= 1)
+		prio = !prio;
+	m->m_pkthdr.ether_vtag |= (prio << EVL_PRIO_BITS);
 
 	CLR(m->m_flags, M_BCAST|M_MCAST);
 
@@ -1261,12 +1370,12 @@ veb_port_input(struct ifnet *ifp0, struc
 		struct veb_port *tp = NULL;
 
 		smr_read_enter();
-		tp = etherbridge_resolve(&sc->sc_eb, dst);
+		tp = etherbridge_resolve(&sc->sc_eb, vid, dst);
 		if (tp != NULL)
 			veb_eb_port_take(NULL, tp);
 		smr_read_leave();
 		if (tp != NULL) {
-			m = veb_transmit(sc, p, tp, m, src, dst, ns);
+			m = veb_transmit(sc, p, tp, m, src, dst, vid, ns);
 			veb_eb_port_rele(NULL, tp);
 		}
 
@@ -1278,7 +1387,7 @@ veb_port_input(struct ifnet *ifp0, struc
 		SET(m->m_flags, ETH64_IS_BROADCAST(dst) ? M_BCAST : M_MCAST);
 	}
 
-	veb_broadcast(sc, p, m, src, dst, ns);
+	veb_broadcast(sc, p, m, src, dst, vid, ns);
 	return (NULL);
 
 drop:
@@ -1317,6 +1426,7 @@ static int
 veb_ioctl(struct ifnet *ifp, u_long cmd, caddr_t data)
 {
 	struct veb_softc *sc = ifp->if_softc;
+	struct ifreq *ifr = (struct ifreq *)data;
 	struct ifbrparam *bparam = (struct ifbrparam *)data;
 	int error = 0;
 
@@ -1334,6 +1444,40 @@ veb_ioctl(struct ifnet *ifp, u_long cmd,
 		}
 		break;
 
+	case SIOCSVNETID:
+		if (ifr->ifr_vnetid < EVL_VLID_MIN ||
+		    ifr->ifr_vnetid > EVL_VLID_MAX) {
+			error = EINVAL;
+			break;
+		}
+
+		sc->sc_dflt_vid = ifr->ifr_vnetid;
+		break;
+	case SIOCGVNETID:
+		ifr->ifr_vnetid = (int64_t)sc->sc_dflt_vid;
+		break;
+	case SIOCSTXHPRIO:
+		error = if_txhprio_l2_check(ifr->ifr_hdrprio);
+		if (error != 0)
+			break;
+
+		sc->sc_txprio = ifr->ifr_hdrprio;
+		break;
+	case SIOCGTXHPRIO:
+		ifr->ifr_hdrprio = sc->sc_txprio;
+		break;
+
+	case SIOCSRXHPRIO:
+		error = if_rxhprio_l2_check(ifr->ifr_hdrprio);
+		if (error != 0)
+			break;
+
+		sc->sc_rxprio = ifr->ifr_hdrprio;
+		break;
+	case SIOCGRXHPRIO:
+		ifr->ifr_hdrprio = sc->sc_rxprio;
+		break;
+
 	case SIOCBRDGADD:
 		error = suser(curproc);
 		if (error != 0)
@@ -1388,6 +1532,9 @@ veb_ioctl(struct ifnet *ifp, u_long cmd,
 	case SIOCBRDGRTS:
 		error = etherbridge_rtfind(&sc->sc_eb, (struct ifbaconf *)data);
 		break;
+	case SIOCBRDGVRTS:
+		error = etherbridge_vareq(&sc->sc_eb, (struct ifbaconf *)data);
+		break;
 	case SIOCBRDGIFS:
 		error = veb_port_list(sc, (struct ifbifconf *)data);
 		break;
@@ -1401,10 +1548,32 @@ veb_ioctl(struct ifnet *ifp, u_long cmd,
 	case SIOCBRDGDADDR:
 		error = veb_del_addr(sc, (struct ifbareq *)data);
 		break;
+	case SIOCBRDGSVADDR:
+		error = veb_add_vid_addr(sc, (struct ifbvareq *)data);
+		break;
+	case SIOCBRDGDVADDR:
+		error = veb_del_vid_addr(sc, (struct ifbvareq *)data);
+		break;
 
 	case SIOCBRDGSIFPROT:
 		error = veb_port_set_protected(sc, (struct ifbreq *)data);
 		break;
+	case SIOCBRDGSPVID:
+		error = veb_port_set_pvid(sc, (struct ifbreq *)data);
+		break;
+
+	case SIOCBRDGSVMAP:
+		error = veb_set_vid_map(sc, (const struct ifbrvidmap *)data);
+		break;
+	case SIOCBRDGAVMAP:
+		error = veb_add_vid_map(sc, (const struct ifbrvidmap *)data);
+		break;
+	case SIOCBRDGCVMAP:
+		error = veb_clr_vid_map(sc, (const struct ifbrvidmap *)data);
+		break;
+	case SIOCBRDGGVMAP:
+		error = veb_get_vid_map(sc, (struct ifbrvidmap *)data);
+		break;
 
 	case SIOCBRDGSIFFLGS:
 		error = veb_port_set_flags(sc, (struct ifbreq *)data);
@@ -1565,6 +1734,7 @@ veb_add_port(struct veb_softc *sc, const
 
 	p->p_ifp0 = ifp0;
 	p->p_veb = sc;
+	p->p_pvid = sc->sc_dflt_vid;
 
 	refcnt_init(&p->p_refs);
 	TAILQ_INIT(&p->p_vrl);
@@ -1733,6 +1903,269 @@ veb_port_set_protected(struct veb_softc 
 }
 
 static int
+veb_port_set_pvid(struct veb_softc *sc, const struct ifbreq *ifbr)
+{
+	struct veb_port *p;
+	uint16_t vid;
+	int error = 0;
+
+	switch (ifbr->ifbr_pvid) {
+	case EVL_VLID_NULL:
+		vid = sc->sc_dflt_vid;
+		break;
+	default:
+		if (ifbr->ifbr_pvid < EVL_VLID_MIN ||
+		    ifbr->ifbr_pvid > EVL_VLID_MAX)
+			return (EINVAL);
+
+		/* FALLTHROUGH */
+	case IFBR_PVID_NONE:
+		vid = ifbr->ifbr_pvid;
+		break;
+	}
+
+	p = veb_port_get(sc, ifbr->ifbr_ifsname);
+	if (p == NULL)
+		return (ESRCH);
+
+	if (vid == IFBR_PVID_NONE &&
+	    p->p_ifp0->if_enqueue == vport_enqueue) {
+		error = EOPNOTSUPP;
+		goto put;
+	}
+	p->p_pvid = vid;
+
+put:
+	veb_port_put(sc, p);
+
+	return (error);
+}
+
+static int
+veb_get_vid_map(struct veb_softc *sc, struct ifbrvidmap *ifbrvm)
+{
+	struct veb_port *p;
+	uint32_t *map;
+	int error = 0;
+
+	p = veb_port_get(sc, ifbrvm->ifbrvm_ifsname);
+	if (p == NULL)
+		return (ESRCH);
+
+	if (p->p_ifp0->if_enqueue == vport_enqueue) {
+		error = ENOTTY;
+		goto put;
+	}
+
+	smr_read_enter();
+	map = p->p_vid_map;
+	if (map == NULL)
+		memset(ifbrvm->ifbrvm_tags, 0, sizeof(ifbrvm->ifbrvm_tags));
+	else {
+		size_t w;
+
+		for (w = 0; w < VEB_VID_WORDS; w++) {
+			uint32_t e = map[w];
+			size_t t = w * sizeof(e);
+			size_t b;
+
+			for (b = 0; b < sizeof(e); b++)
+				ifbrvm->ifbrvm_tags[t + b] = e >> (b * 8);
+		}
+	}
+	smr_read_leave();
+
+put:
+	veb_port_put(sc, p);
+	return (error);
+}
+
+static int
+veb_chk_vid_map(const struct ifbrvidmap *ifbrvm)
+{
+	size_t off;
+	size_t bit;
+
+	/*
+	 * vlan 0 and 4095 are not valid vlan tags
+	 */
+
+	off = 0 / 8;
+	bit = 0 % 8;
+	if (ISSET(ifbrvm->ifbrvm_tags[off], 1U << bit))
+		return (EINVAL);
+
+	off = 4095 / 8;
+	bit = 4095 % 8;
+	if (ISSET(ifbrvm->ifbrvm_tags[off], 1U << bit))
+		return (EINVAL);
+	
+	return (0);
+}
+
+static uint32_t *
+veb_new_vid_map(const struct ifbrvidmap *ifbrvm)
+{
+	uint32_t *map;
+	size_t w;
+
+	map = mallocarray(VEB_VID_WORDS, sizeof(*map), M_IFADDR,
+	    M_WAITOK|M_CANFAIL);
+	if (map == NULL)
+		return (NULL);
+
+	for (w = 0; w < VEB_VID_WORDS; w++) {
+		uint32_t e = 0;
+		size_t t = w * sizeof(e);
+		size_t b;
+
+		for (b = 0; b < sizeof(e); b++)
+			e |= (uint32_t)ifbrvm->ifbrvm_tags[t + b] << (b * 8);
+
+		map[w] = e;
+	}
+
+	return (map);
+}
+
+static inline void
+veb_free_vid_map(uint32_t *map)
+{
+	free(map, M_IFADDR, VEB_VID_BYTES);
+}
+
+struct veb_vid_map_dtor {
+	struct smr_entry	 smr;
+	uint32_t		*map;
+};
+
+static void
+veb_dtor_vid_map(void *arg)
+{
+	struct veb_vid_map_dtor *dtor = arg;
+	veb_free_vid_map(dtor->map);
+	free(dtor, M_TEMP, sizeof(*dtor));
+}
+
+static void
+veb_destroy_vid_map(uint32_t *map)
+{
+	struct veb_vid_map_dtor *dtor;
+
+	dtor = malloc(sizeof(*dtor), M_TEMP, M_NOWAIT); 
+	if (dtor == NULL) {
+		/* oh well, the proc can sleep instead */
+		smr_barrier();
+		veb_free_vid_map(map);
+		return;
+	}
+
+	smr_init(&dtor->smr);
+	dtor->map = map;
+	smr_call(&dtor->smr, veb_dtor_vid_map, dtor);
+}
+
+static int
+veb_mod_vid_map(struct veb_softc *sc, const struct ifbrvidmap *ifbrvm,
+	void (*apply)(uint32_t *, const uint32_t *))
+{
+	struct veb_port *p;
+	uint32_t *nmap = NULL, *omap = NULL;
+	int error = 0;
+
+	error = veb_chk_vid_map(ifbrvm);
+	if (error != 0)
+		return (error);
+
+	p = veb_port_get(sc, ifbrvm->ifbrvm_ifsname);
+	if (p == NULL)
+		return (ESRCH);
+
+	if (p->p_ifp0->if_enqueue == vport_enqueue) {
+		error = ENOTTY;
+		goto put;
+	}
+
+	nmap = veb_new_vid_map(ifbrvm);
+	if (nmap == NULL) {
+		error = ENOMEM;
+		goto put;
+	}
+
+	error = rw_enter(&sc->sc_rule_lock, RW_WRITE|RW_INTR);
+	if (error != 0)
+		goto put;
+
+	omap = SMR_PTR_GET_LOCKED(&p->p_vid_map);
+	apply(nmap, omap);
+	SMR_PTR_SET_LOCKED(&p->p_vid_map, nmap);
+	rw_exit(&sc->sc_rule_lock);
+	nmap = NULL;
+
+put:
+	veb_port_put(sc, p);
+	if (omap != NULL)
+		veb_destroy_vid_map(omap);
+	if (nmap != NULL)
+		veb_free_vid_map(nmap);
+	return (error);
+}
+
+static void
+veb_set_vid_map_apply(uint32_t *nmap, const uint32_t *omap)
+{
+	/* nop - nmap replaces (sets) the vid map */
+}
+
+static int
+veb_set_vid_map(struct veb_softc *sc, const struct ifbrvidmap *ifbrvm)
+{
+	return veb_mod_vid_map(sc, ifbrvm, veb_set_vid_map_apply);
+}
+
+static void
+veb_add_vid_map_apply(uint32_t *nmap, const uint32_t *omap)
+{
+	size_t w;
+
+	if (omap == NULL)
+		return;
+
+	for (w = 0; w < VEB_VID_WORDS; w++)
+		nmap[w] |= omap[w];
+}
+
+static int
+veb_add_vid_map(struct veb_softc *sc, const struct ifbrvidmap *ifbrvm)
+{
+	return veb_mod_vid_map(sc, ifbrvm, veb_add_vid_map_apply);
+}
+
+static void
+veb_clr_vid_map_apply(uint32_t *nmap, const uint32_t *omap)
+{
+	size_t w;
+
+	if (omap == NULL) {
+		/* empty set, clear everything */
+		for (w = 0; w < VEB_VID_WORDS; w++)
+			nmap[w] = 0;
+		return;
+	}
+
+	for (w = 0; w < VEB_VID_WORDS; w++) {
+		uint32_t e = nmap[w];
+		nmap[w] = omap[w] & ~e;
+	}
+}
+
+static int
+veb_clr_vid_map(struct veb_softc *sc, const struct ifbrvidmap *ifbrvm)
+{
+	return veb_mod_vid_map(sc, ifbrvm, veb_clr_vid_map_apply);
+}
+
+static int
 veb_rule_add(struct veb_softc *sc, const struct ifbrlreq *ifbr)
 {
 	const struct ifbrarpf *brla = &ifbr->ifbr_arpf;
@@ -2055,6 +2488,7 @@ veb_port_list(struct veb_softc *sc, stru
 			breq.ifbr_ifsflags = p->p_bif_flags;
 			breq.ifbr_portno = ifp0->if_index;
 			breq.ifbr_protected = p->p_protected;
+			breq.ifbr_pvid = p->p_pvid;
 			if ((error = copyout(&breq, bifc->ifbic_req + n,
 			    sizeof(breq))) != 0)
 				goto done;
@@ -2159,7 +2593,56 @@ veb_add_addr(struct veb_softc *sc, const
 	if (p == NULL)
 		return (ESRCH);
 
-	error = etherbridge_add_addr(&sc->sc_eb, p, &ifba->ifba_dst, type);
+	error = etherbridge_add_addr(&sc->sc_eb, p,
+	    p->p_pvid, &ifba->ifba_dst, type);
+
+	veb_port_put(sc, p);
+
+	return (error);
+}
+
+static int
+veb_add_vid_addr(struct veb_softc *sc, const struct ifbvareq *ifbva)
+{
+	struct veb_port *p;
+	int error = 0;
+	unsigned int type;
+	uint16_t vid;
+
+	if (ISSET(ifbva->ifbva_flags, ~IFBAF_TYPEMASK))
+		return (EINVAL);
+	switch (ifbva->ifbva_flags & IFBAF_TYPEMASK) {
+	case IFBAF_DYNAMIC:
+		type = EBE_DYNAMIC;
+		break;
+	case IFBAF_STATIC:
+		type = EBE_STATIC;
+		break;
+	default:
+		return (EINVAL);
+	}
+
+	if (ifbva->ifbva_dstsa.ss_family != AF_UNSPEC)
+		return (EAFNOSUPPORT);
+
+	if (ifbva->ifbva_vid != EVL_VLID_NULL) {
+		if (ifbva->ifbva_vid < EVL_VLID_MIN ||
+		    ifbva->ifbva_vid > EVL_VLID_MAX)
+			return (EINVAL);
+	}
+
+	p = veb_port_get(sc, ifbva->ifbva_ifsname);
+	if (p == NULL)
+		return (ESRCH);
+
+	vid = ifbva->ifbva_vid;
+	if (vid == EVL_VLID_NULL)
+		vid = p->p_pvid;
+
+uprintf("%s: vid %u\n", __func__, vid);
+
+	error = etherbridge_add_addr(&sc->sc_eb, p,
+	    vid, &ifbva->ifbva_dst, type);
 
 	veb_port_put(sc, p);
 
@@ -2169,7 +2652,19 @@ veb_add_addr(struct veb_softc *sc, const
 static int
 veb_del_addr(struct veb_softc *sc, const struct ifbareq *ifba)
 {
-	return (etherbridge_del_addr(&sc->sc_eb, &ifba->ifba_dst));
+	return (etherbridge_del_addr(&sc->sc_eb,
+	    sc->sc_dflt_vid, &ifba->ifba_dst));
+}
+
+static int
+veb_del_vid_addr(struct veb_softc *sc, const struct ifbvareq *ifbva)
+{
+	if (ifbva->ifbva_vid < EVL_VLID_MIN ||
+	    ifbva->ifbva_vid > EVL_VLID_MAX)
+		return (EINVAL);
+
+	return (etherbridge_del_addr(&sc->sc_eb,
+	    ifbva->ifbva_vid, &ifbva->ifbva_dst));
 }
 
 static int
@@ -2270,6 +2765,7 @@ veb_p_fini(struct veb_port *p)
 	veb_rule_list_free(TAILQ_FIRST(&p->p_vrl));
 
 	if_put(ifp0);
+	veb_free_vid_map(p->p_vid_map);
 	free(p, M_DEVBUF, sizeof(*p)); /* hope you didn't forget smr_barrier */
 }
 
Index: sys/net/if_vxlan.c
===================================================================
RCS file: /cvs/src/sys/net/if_vxlan.c,v
diff -u -p -r1.104 if_vxlan.c
--- sys/net/if_vxlan.c	7 Jul 2025 02:28:50 -0000	1.104
+++ sys/net/if_vxlan.c	29 Oct 2025 04:15:50 -0000
@@ -336,7 +336,7 @@ vxlan_encap(struct vxlan_softc *sc, stru
 
 		smr_read_enter();
 		endpoint = etherbridge_resolve_ea(&sc->sc_eb,
-		    (struct ether_addr *)eh->ether_dhost);
+		    0, (struct ether_addr *)eh->ether_dhost);
 		if (endpoint != NULL) {
 			gateway = *endpoint;
 			endpoint = &gateway;
@@ -695,7 +695,7 @@ vxlan_input(void *arg, struct mbuf *m, s
 	if (sc->sc_mode == VXLAN_TMODE_LEARNING) {
 		eh = mtod(m, struct ether_header *);
 		etherbridge_map_ea(&sc->sc_eb, &addr,
-		    (struct ether_addr *)eh->ether_shost);
+		    0, (struct ether_addr *)eh->ether_shost);
 	}
 
 	rxhprio = sc->sc_rxhprio;
@@ -1721,13 +1721,13 @@ vxlan_add_addr(struct vxlan_softc *sc, c
 	}
 
 	return (etherbridge_add_addr(&sc->sc_eb, &endpoint,
-	    &ifba->ifba_dst, type));
+	    0, &ifba->ifba_dst, type));
 }
 
 static int
 vxlan_del_addr(struct vxlan_softc *sc, const struct ifbareq *ifba)
 {
-	return (etherbridge_del_addr(&sc->sc_eb, &ifba->ifba_dst));
+	return (etherbridge_del_addr(&sc->sc_eb, 0, &ifba->ifba_dst));
 }
 
 void
Index: sys/sys/sockio.h
===================================================================
RCS file: /cvs/src/sys/sys/sockio.h,v
diff -u -p -r1.84 sockio.h
--- sys/sys/sockio.h	11 Nov 2021 10:03:10 -0000	1.84
+++ sys/sys/sockio.h	29 Oct 2025 04:15:50 -0000
@@ -85,12 +85,20 @@
 #define	SIOCBRDGDELS	 _IOW('i', 66, struct ifbreq)	/* del span port */
 #define	SIOCBRDGRTS	_IOWR('i', 67, struct ifbaconf)	/* get addresses */
 #define	SIOCBRDGSADDR	_IOWR('i', 68, struct ifbareq)	/* set addr flags */
+#define	SIOCBRDGSVADDR	 _IOW('i', 68, struct ifbvareq)	/* add addr@vid */
 #define	SIOCBRDGSTO	 _IOW('i', 69, struct ifbrparam)/* cache timeout */
 #define	SIOCBRDGGTO	_IOWR('i', 70, struct ifbrparam)/* cache timeout */
 #define	SIOCBRDGDADDR	 _IOW('i', 71, struct ifbareq)	/* delete addr */
+#define	SIOCBRDGDVADDR	 _IOW('i', 71, struct ifbvareq)	/* delete addr@vid */
 #define	SIOCBRDGFLUSH	 _IOW('i', 72, struct ifbreq)	/* flush addr cache */
 #define	SIOCBRDGADDL	 _IOW('i', 73, struct ifbreq)	/* add local port */
 #define	SIOCBRDGSIFPROT	 _IOW('i', 74, struct ifbreq)	/* set protected grp */
+#define	SIOCBRDGSPVID	 _IOW('i', 76, struct ifbreq)	/* set pvid */
+
+#define SIOCBRDGSVMAP	 _IOW('i', 76, struct ifbrvidmap) /* set vid map */
+#define SIOCBRDGAVMAP	 _IOW('i', 77, struct ifbrvidmap) /* add vid map bits */
+#define SIOCBRDGCVMAP	 _IOW('i', 78, struct ifbrvidmap) /* clr vid map bits */
+#define SIOCBRDGGVMAP	_IOWR('i', 79, struct ifbrvidmap) /* get vid map */
 
 #define SIOCBRDGARL	 _IOW('i', 77, struct ifbrlreq)	/* add bridge rule */
 #define SIOCBRDGFRL	 _IOW('i', 78, struct ifbrlreq)	/* flush brdg rules */
@@ -105,6 +113,7 @@
 #define	SIOCBRDGSMA	 _IOW('i', 83, struct ifbrparam)/* set max age */
 #define	SIOCBRDGSIFPRIO	 _IOW('i', 84, struct ifbreq)	/* set if priority */
 #define	SIOCBRDGSIFCOST  _IOW('i', 85, struct ifbreq)	/* set if cost */
+#define	SIOCBRDGVRTS	_IOWR('i', 86, struct ifbaconf)	/* get vaddresses */
 
 #define SIOCBRDGGPARAM  _IOWR('i', 88, struct ifbropreq)/* get brdg STP parms */
 #define SIOCBRDGSTXHC    _IOW('i', 89, struct ifbrparam)/* set tx hold count */