git: 95e7d47a4bce - main - bridge: allow vlan(4) interfaces on a bridge

From: Lexi Winter <ivy_at_FreeBSD.org>
Date: Sat, 05 Jul 2025 07:13:19 UTC
The branch main has been updated by ivy:

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

commit 95e7d47a4bcea741f44aac4cbdcdb41bbbff6d70
Author:     Lexi Winter <ivy@FreeBSD.org>
AuthorDate: 2025-07-05 06:36:00 +0000
Commit:     Lexi Winter <ivy@FreeBSD.org>
CommitDate: 2025-07-05 07:04:31 +0000

    bridge: allow vlan(4) interfaces on a bridge
    
    A vlan interface on top of a bridge will act as a layer 3 port for
    bridge traffic on that vlan, sometimes called an "SVI".  This allows
    the host to send/receive traffic on that vlan without having to create
    a separate epair(4) and vlan(4) to tag and untag the traffic.
    
    Reviewed by:    zlei, kp, des
    Approved by:    des (mentor)
    Differential Revision:  https://reviews.freebsd.org/D50504
---
 share/man/man4/bridge.4         |  6 ++++++
 sys/net/if_bridge.c             | 39 +++++++++++++++++++++++++++++++++++++--
 sys/net/if_vlan.c               |  1 +
 tests/sys/net/if_bridge_test.sh | 41 +++++++++++++++++++++++++++++++++++++++++
 4 files changed, 85 insertions(+), 2 deletions(-)

diff --git a/share/man/man4/bridge.4 b/share/man/man4/bridge.4
index 06f7fed06477..d3dea9eaa938 100644
--- a/share/man/man4/bridge.4
+++ b/share/man/man4/bridge.4
@@ -281,6 +281,11 @@ multiple interfaces on different VLANs.
 Incoming frames with an 802.1Q tag will be assigned to the appropriate
 VLAN.
 .Pp
+Traffic sent to or from the host is not assigned to a VLAN by default.
+To allow the host to communicate on a VLAN, configure a
+.Xr vlan 4
+interface on the bridge and (if necessary) assign IP addresses there.
+.Pp
 By default no access control is enabled, so any interface may
 participate in any VLAN.
 .Pp
@@ -581,6 +586,7 @@ ifconfig bridge0 addm fxp0 addm gif0 up
 .Xr ipfw 4 ,
 .Xr netmap 4 ,
 .Xr pf 4 ,
+.Xr vlan 4 ,
 .Xr ifconfig 8
 .Sh HISTORY
 The
diff --git a/sys/net/if_bridge.c b/sys/net/if_bridge.c
index ba4e0425c945..5b3ee740d75e 100644
--- a/sys/net/if_bridge.c
+++ b/sys/net/if_bridge.c
@@ -847,6 +847,7 @@ bridge_clone_create(struct if_clone *ifc, char *name, size_t len,
 	ifp->if_softc = sc;
 	if_initname(ifp, bridge_name, ifd->unit);
 	ifp->if_flags = IFF_BROADCAST | IFF_SIMPLEX | IFF_MULTICAST;
+	ifp->if_capabilities = ifp->if_capenable = IFCAP_VLAN_HWTAGGING;
 	ifp->if_ioctl = bridge_ioctl;
 #ifdef ALTQ
 	ifp->if_start = bridge_altq_start;
@@ -2484,16 +2485,17 @@ bridge_transmit(struct ifnet *ifp, struct mbuf *m)
 	struct ether_header *eh;
 	struct ifnet *dst_if;
 	int error = 0;
+	ether_vlanid_t vlan;
 
 	sc = ifp->if_softc;
 
 	ETHER_BPF_MTAP(ifp, m);
 
 	eh = mtod(m, struct ether_header *);
+	vlan = VLANTAGOF(m);
 
 	if (((m->m_flags & (M_BCAST|M_MCAST)) == 0) &&
-	    (dst_if = bridge_rtlookup(sc, eh->ether_dhost, DOT1Q_VID_NULL)) !=
-	    NULL) {
+	    (dst_if = bridge_rtlookup(sc, eh->ether_dhost, vlan)) != NULL) {
 		error = bridge_enqueue(sc, dst_if, m, NULL);
 	} else
 		bridge_broadcast(sc, ifp, m, 0);
@@ -2894,6 +2896,15 @@ bridge_input(struct ifnet *ifp, struct mbuf *m)
 		}							\
 		if ((iface) != bifp)					\
 			ETHER_BPF_MTAP(iface, m);			\
+		/* Pass tagged packets to if_vlan, if it's loaded */	\
+		if (VLANTAGOF(m) != 0) {				\
+			if (bifp->if_vlantrunk == NULL) {		\
+				m_freem(m);				\
+				return (NULL);				\
+			}						\
+			(*vlan_input_p)(bifp, m);			\
+			return (NULL);					\
+		}							\
 		return (m);						\
 	}								\
 									\
@@ -2950,6 +2961,30 @@ bridge_inject(struct ifnet *ifp, struct mbuf *m)
 {
 	struct bridge_softc *sc;
 
+	if (ifp->if_type == IFT_L2VLAN) {
+		/*
+		 * vlan(4) gives us the vlan ifnet, so we need to get the
+		 * bridge softc to get a pointer to ether_input to send the
+		 * packet to.
+		 */
+		struct ifnet *bifp = NULL;
+
+		if (vlan_trunkdev_p == NULL) {
+			m_freem(m);
+			return;
+		}
+
+		bifp = vlan_trunkdev_p(ifp);
+		if (bifp == NULL) {
+			m_freem(m);
+			return;
+		}
+
+		sc = if_getsoftc(bifp);
+		sc->sc_if_input(ifp, m);
+		return;
+	}
+
 	KASSERT((if_getcapenable(ifp) & IFCAP_NETMAP) != 0,
 	    ("%s: iface %s is not running in netmap mode",
 	    __func__, if_name(ifp)));
diff --git a/sys/net/if_vlan.c b/sys/net/if_vlan.c
index e9e1c82cb688..22fcb7bf7c64 100644
--- a/sys/net/if_vlan.c
+++ b/sys/net/if_vlan.c
@@ -1673,6 +1673,7 @@ vlan_config(struct ifvlan *ifv, struct ifnet *p, uint16_t vid,
 	 */
 	if (p->if_type != IFT_ETHER &&
 	    p->if_type != IFT_L2VLAN &&
+	    p->if_type != IFT_BRIDGE &&
 	    (p->if_capenable & IFCAP_VLAN_HWTAGGING) == 0)
 		return (EPROTONOSUPPORT);
 	if ((p->if_flags & VLAN_IFFLAGS) != VLAN_IFFLAGS)
diff --git a/tests/sys/net/if_bridge_test.sh b/tests/sys/net/if_bridge_test.sh
index 4815c1aef570..d057d2997486 100755
--- a/tests/sys/net/if_bridge_test.sh
+++ b/tests/sys/net/if_bridge_test.sh
@@ -1124,6 +1124,46 @@ vlan_ifconfig_tagged_cleanup()
 	vnet_cleanup
 }
 
+#
+# Test a vlan(4) "SVI" interface on top of a bridge.
+#
+atf_test_case "vlan_svi" "cleanup"
+vlan_svi_head()
+{
+	atf_set descr 'vlan bridge with an SVI'
+	atf_set require.user root
+}
+
+vlan_svi_body()
+{
+	vnet_init
+	vnet_init_bridge
+
+	epone=$(vnet_mkepair)
+
+	vnet_mkjail one ${epone}b
+
+	jexec one ifconfig ${epone}b up
+	jexec one ifconfig ${epone}b.20 create 192.0.2.1/24 up
+
+	bridge=$(vnet_mkbridge)
+
+	ifconfig ${bridge} up
+	ifconfig ${epone}a up
+	ifconfig ${bridge} addm ${epone}a tagged ${epone}a 20
+
+	svi=$(vnet_mkvlan)
+	ifconfig ${svi} vlan 20 vlandev ${bridge}
+	ifconfig ${svi} inet 192.0.2.2/24 up
+
+	atf_check -s exit:0 -o ignore ping -c 3 -t 1 192.0.2.1
+}
+
+vlan_svi_cleanup()
+{
+	vnet_cleanup
+}
+
 atf_init_test_cases()
 {
 	atf_add_test_case "bridge_transmit_ipv4_unicast"
@@ -1148,4 +1188,5 @@ atf_init_test_cases()
 	atf_add_test_case "vlan_pvid_tagged"
 	atf_add_test_case "vlan_filtering"
 	atf_add_test_case "vlan_ifconfig_tagged"
+	atf_add_test_case "vlan_svi"
 }