git: 43ac5806a5a3 - main - bridge: Divorce ifuntagged from vlanfilter

From: Lexi Winter <ivy_at_FreeBSD.org>
Date: Sun, 10 Aug 2025 15:37:18 UTC
The branch main has been updated by ivy:

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

commit 43ac5806a5a3150f561e14a11866dd56cf1b4d42
Author:     Lexi Winter <ivy@FreeBSD.org>
AuthorDate: 2025-08-10 15:11:32 +0000
Commit:     Lexi Winter <ivy@FreeBSD.org>
CommitDate: 2025-08-10 15:36:40 +0000

    bridge: Divorce ifuntagged from vlanfilter
    
    The ifuntagged option was added as part of the VLAN filtering feature,
    but it's useful on its own to be able to place interface traffic in a
    VLAN without having to configure every interface for VLAN filtering.
    
    Always do the pvid processing in bridge even if IFBRF_VLANFILTER isn't
    enabled, and don't prohibit configuring it.
    
    Add a test for the specific case of setting untagged without vlanfilter.
    
    This has no effect on bridges which don't have at least one interface
    configured with ifuntagged.
    
    Differential Revision:  https://reviews.freebsd.org/D51760
---
 sys/net/if_bridge.c             | 28 ++++++++++++---------
 tests/sys/net/if_bridge_test.sh | 54 +++++++++++++++++++++++++++++++++++++++--
 2 files changed, 68 insertions(+), 14 deletions(-)

diff --git a/sys/net/if_bridge.c b/sys/net/if_bridge.c
index 1e444be93e9f..66555fd1feb5 100644
--- a/sys/net/if_bridge.c
+++ b/sys/net/if_bridge.c
@@ -1500,8 +1500,7 @@ bridge_ioctl_add(struct bridge_softc *sc, void *arg)
 	bif->bif_flags = IFBIF_LEARNING | IFBIF_DISCOVER;
 	bif->bif_savedcaps = ifs->if_capenable;
 	bif->bif_vlanproto = ETHERTYPE_VLAN;
-	if (sc->sc_flags & IFBRF_VLANFILTER)
-		bif->bif_pvid = sc->sc_defpvid;
+	bif->bif_pvid = sc->sc_defpvid;
 	if (sc->sc_flags & IFBRF_DEFQINQ)
 		bif->bif_flags |= IFBIF_QINQ;
 
@@ -1970,9 +1969,6 @@ bridge_ioctl_sifpvid(struct bridge_softc *sc, void *arg)
 	struct ifbreq *req = arg;
 	struct bridge_iflist *bif;
 
-	if ((sc->sc_flags & IFBRF_VLANFILTER) == 0)
-		return (EXTERROR(EINVAL, "VLAN filtering not enabled"));
-
 	bif = bridge_lookup_member(sc, req->ifbr_ifsname);
 	if (bif == NULL)
 		return (EXTERROR(ENOENT, "Interface is not a bridge member"));
@@ -2410,12 +2406,10 @@ bridge_enqueue(struct bridge_softc *sc, struct ifnet *dst_ifp, struct mbuf *m,
 		mflags = m->m_flags;
 
 		/*
-		 * If VLAN filtering is enabled, and the native VLAN ID of the
-		 * outgoing interface matches the VLAN ID of the frame, remove
-		 * the VLAN header.
+		 * If the native VLAN ID of the outgoing interface matches the
+		 * VLAN ID of the frame, remove the VLAN tag.
 		 */
-		if ((sc->sc_flags & IFBRF_VLANFILTER) &&
-		    bif->bif_pvid != DOT1Q_VID_NULL &&
+		if (bif->bif_pvid != DOT1Q_VID_NULL &&
 		    VLANTAGOF(m) == bif->bif_pvid) {
 			m->m_flags &= ~M_VLANTAG;
 			m->m_pkthdr.ether_vtag = 0;
@@ -3296,9 +3290,19 @@ bridge_vfilter_in(const struct bridge_iflist *sbif, struct mbuf *m)
 	if (vlan > DOT1Q_VID_MAX)
 		return (false);
 
-	/* If VLAN filtering isn't enabled, pass everything. */
-	if ((sbif->bif_sc->sc_flags & IFBRF_VLANFILTER) == 0)
+	/*
+	 * If VLAN filtering isn't enabled, pass everything, but add a tag
+	 * if the port has a pvid configured.
+	 */
+	if ((sbif->bif_sc->sc_flags & IFBRF_VLANFILTER) == 0) {
+		if (vlan == DOT1Q_VID_NULL &&
+		    sbif->bif_pvid != DOT1Q_VID_NULL) {
+			m->m_pkthdr.ether_vtag = sbif->bif_pvid;
+			m->m_flags |= M_VLANTAG;
+		}
+
 		return (true);
+	}
 
 	/* If Q-in-Q is disabled, check for stacked tags. */
 	if ((sbif->bif_flags & IFBIF_QINQ) == 0) {
diff --git a/tests/sys/net/if_bridge_test.sh b/tests/sys/net/if_bridge_test.sh
index 906f586d2483..0c19903714b1 100755
--- a/tests/sys/net/if_bridge_test.sh
+++ b/tests/sys/net/if_bridge_test.sh
@@ -899,7 +899,7 @@ member_ifaddrs_vlan_cleanup()
 atf_test_case "vlan_pvid" "cleanup"
 vlan_pvid_head()
 {
-	atf_set descr 'bridge with two ports with pvid set'
+	atf_set descr 'bridge with two ports with pvid and vlanfilter set'
 	atf_set require.user root
 }
 
@@ -1327,6 +1327,56 @@ bridge_svi_in_bridge_cleanup()
 	vnet_cleanup
 }
 
+atf_test_case "vlan_untagged" "cleanup"
+vlan_untagged_head()
+{
+	atf_set descr 'bridge with two ports with untagged set'
+	atf_set require.user root
+}
+
+vlan_untagged_body()
+{
+	vnet_init
+	vnet_init_bridge
+
+	epone=$(vnet_mkepair)
+	eptwo=$(vnet_mkepair)
+
+	vnet_mkjail one ${epone}b
+	vnet_mkjail two ${eptwo}b
+
+	jexec one ifconfig ${epone}b 192.0.2.1/24 up
+	jexec two ifconfig ${eptwo}b 192.0.2.2/24 up
+
+	bridge=$(vnet_mkbridge)
+
+	ifconfig ${bridge} up
+	ifconfig ${epone}a up
+	ifconfig ${eptwo}a up
+	ifconfig ${bridge} addm ${epone}a untagged 20
+	ifconfig ${bridge} addm ${eptwo}a untagged 30
+
+	# With two ports on different VLANs, traffic should not be passed.
+	atf_check -s exit:2 -o ignore jexec one ping -c 3 -t 1 192.0.2.2
+	atf_check -s exit:2 -o ignore jexec two ping -c 3 -t 1 192.0.2.1
+
+	# Move the second port to VLAN 20; now traffic should be passed.
+	atf_check -s exit:0 ifconfig ${bridge} ifuntagged ${eptwo}a 20
+	atf_check -s exit:0 -o ignore jexec one ping -c 3 -t 1 192.0.2.2
+	atf_check -s exit:0 -o ignore jexec two ping -c 3 -t 1 192.0.2.1
+
+	# Remove the first's port untagged config, now traffic should
+	# not pass again.
+	atf_check -s exit:0 ifconfig ${bridge} -ifuntagged ${epone}a
+	atf_check -s exit:2 -o ignore jexec one ping -c 3 -t 1 192.0.2.2
+	atf_check -s exit:2 -o ignore jexec two ping -c 3 -t 1 192.0.2.1
+}
+
+vlan_untagged_cleanup()
+{
+	vnet_cleanup
+}
+
 atf_test_case "vlan_defuntagged" "cleanup"
 vlan_defuntagged_head()
 {
@@ -1340,7 +1390,6 @@ vlan_defuntagged_body()
 	vnet_init_bridge
 
 	bridge=$(vnet_mkbridge)
-	atf_check -s exit:0 ifconfig ${bridge} vlanfilter
 
 	# Invalid VLAN IDs
 	atf_check -s exit:1 -ematch:"invalid vlan id: 0" \
@@ -1407,6 +1456,7 @@ atf_init_test_cases()
 	atf_add_test_case "vlan_ifconfig_iftagged"
 	atf_add_test_case "vlan_svi"
 	atf_add_test_case "vlan_qinq"
+	atf_add_test_case "vlan_untagged"
 	atf_add_test_case "vlan_defuntagged"
 	atf_add_test_case "bridge_svi_in_bridge"
 }