Thread (30 messages) 30 messages, 4 authors, 22d ago
COLD22d

[PATCH net-next v2 4/6] net: bridge: allow MDB_FLAGS_STREAM_RESERVED on host groups

From: Luke Howard <hidden>
Date: 2026-06-02 00:44:39
Also in: bridge, linux-kselftest, lkml
Subsystem: ethernet bridge, kernel selftest framework, networking drivers, networking [general], networking [ipv4/ipv6], the rest · Maintainers: Nikolay Aleksandrov, Ido Schimmel, Shuah Khan, Andrew Lunn, "David S. Miller", Eric Dumazet, Jakub Kicinski, Paolo Abeni, David Ahern, Linus Torvalds

Allow the local bridge host to declare itself a reserved stream listener
for a MDB group, for example on a device which is both an AVB end station
and bridge.

Only MDB_FLAGS_STREAM_RESERVED is accepted on host groups; the other
MDB_FLAGS_* bits remain port-group-only.

Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Luke Howard <redacted>
---
 include/uapi/linux/if_bridge.h                     |   7 +-
 net/bridge/br_input.c                              |   2 +-
 net/bridge/br_mdb.c                                |  21 +++-
 net/bridge/br_multicast.c                          |  37 ++++--
 net/bridge/br_private.h                            |  15 ++-
 .../net/forwarding/bridge_mdb_stream_reserved.sh   | 125 ++++++++++++++++++++-
 6 files changed, 182 insertions(+), 25 deletions(-)
diff --git a/include/uapi/linux/if_bridge.h b/include/uapi/linux/if_bridge.h
index 01955a575528c..989d13a866be4 100644
--- a/include/uapi/linux/if_bridge.h
+++ b/include/uapi/linux/if_bridge.h
@@ -748,9 +748,10 @@ enum {
  *    [MDBE_ATTR_xxx]
  *    ...
  *    [MDBE_ATTR_FLAGS]
- *       u32, a mask of MDB_FLAGS_* values to set on the entry. Valid only
- *       for port-group entries; currently only MDB_FLAGS_STREAM_RESERVED
- *       may be set from user space.
+ *       u32, a mask of MDB_FLAGS_* values to set on the entry. Currently
+ *       only MDB_FLAGS_STREAM_RESERVED may be set from user space, and is
+ *       accepted on both port-group and host-group entries (on the latter
+ *       it declares the local bridge host as a reserved-stream listener).
  * }
  */
 enum {
diff --git a/net/bridge/br_input.c b/net/bridge/br_input.c
index 2e8aa19a9b542..649b819906bf8 100644
--- a/net/bridge/br_input.c
+++ b/net/bridge/br_input.c
@@ -105,7 +105,7 @@ static bool br_sr_admission_denied(const struct net_bridge_port *p,
 	if (!mdst)
 		return true;
 
-	if (mdst->flags & BRIDGE_MDBE_F_HOST_STREAM_RESERVED)
+	if ((mdst->flags & BRIDGE_MDBE_F_HOST_MASK) == BRIDGE_MDBE_F_HOST_MASK)
 		return false;
 
 	for (pg = rcu_dereference(mdst->ports); pg;
diff --git a/net/bridge/br_mdb.c b/net/bridge/br_mdb.c
index b95ca72ec6347..93127a8ea54f7 100644
--- a/net/bridge/br_mdb.c
+++ b/net/bridge/br_mdb.c
@@ -250,6 +250,9 @@ static int __mdb_fill_info(struct sk_buff *skb,
 	} else {
 		ifindex = mp->br->dev->ifindex;
 		mtimer = &mp->timer;
+		if (mp->flags & BRIDGE_MDBE_F_HOST_STREAM_RESERVED)
+			flags = MDB_PG_FLAGS_PERMANENT |
+				MDB_PG_FLAGS_STREAM_RESERVED;
 	}
 
 	__mdb_entry_fill_flags(&e, flags);
@@ -1059,7 +1062,10 @@ static int br_mdb_add_group(const struct br_mdb_config *cfg,
 			return -EEXIST;
 		}
 
-		br_multicast_host_join(brmctx, mp, false);
+		br_multicast_host_join(brmctx, mp,
+				       cfg->pg_flags & MDB_PG_FLAGS_STREAM_RESERVED ?
+				       BR_MCAST_SR_SET : BR_MCAST_SR_CLEAR,
+				       false);
 		br_mdb_notify(br->dev, mp, NULL, RTM_NEWMDB);
 
 		return 0;
@@ -1219,11 +1225,14 @@ static int br_mdb_config_attrs_init(struct nlattr *set_attrs,
 	}
 
 	if (mdb_attrs[MDBE_ATTR_FLAGS]) {
-		if (!cfg->p) {
-			NL_SET_ERR_MSG_MOD(extack, "Flags cannot be set for host groups");
+		u32 attr_flags = nla_get_u32(mdb_attrs[MDBE_ATTR_FLAGS]);
+
+		if (!cfg->p && (attr_flags & ~MDB_FLAGS_STREAM_RESERVED)) {
+			NL_SET_ERR_MSG_MOD(extack,
+					   "Only stream_reserved may be set on host groups");
 			return -EINVAL;
 		}
-		if (nla_get_u32(mdb_attrs[MDBE_ATTR_FLAGS]) & MDB_FLAGS_STREAM_RESERVED)
+		if (attr_flags & MDB_FLAGS_STREAM_RESERVED)
 			cfg->pg_flags |= MDB_PG_FLAGS_STREAM_RESERVED;
 	}
 
@@ -1320,8 +1329,8 @@ int br_mdb_add(struct net_device *dev, struct nlattr *tb[], u16 nlmsg_flags,
 
 	/* host join errors which can happen before creating the group */
 	if (!cfg.p && !br_group_is_l2(&cfg.group)) {
-		/* don't allow any flags for host-joined IP groups */
-		if (cfg.entry->state) {
+		if (cfg.entry->state &&
+		    !(cfg.pg_flags & MDB_PG_FLAGS_STREAM_RESERVED)) {
 			NL_SET_ERR_MSG_MOD(extack, "Flags are not allowed for host groups");
 			goto out;
 		}
diff --git a/net/bridge/br_multicast.c b/net/bridge/br_multicast.c
index 4107bf7bd271f..e3fc61bb63092 100644
--- a/net/bridge/br_multicast.c
+++ b/net/bridge/br_multicast.c
@@ -397,10 +397,10 @@ static void br_multicast_sg_host_state(struct net_bridge_mdb_entry *star_mp,
 	sg_mp = br_mdb_ip_get(star_mp->br, &sg->key.addr);
 	if (!sg_mp)
 		return;
-	sg_mp->flags |= BRIDGE_MDBE_F_HOST_JOINED;
+	sg_mp->flags |= star_mp->flags & BRIDGE_MDBE_F_HOST_MASK;
 }
 
-/* set the host_joined state of all of *,G's S,G entries */
+/* set the host state of all of *,G's S,G entries */
 static void br_multicast_star_g_host_state(struct net_bridge_mdb_entry *star_mp)
 {
 	struct net_bridge *br = star_mp->br;
@@ -425,8 +425,8 @@ static void br_multicast_star_g_host_state(struct net_bridge_mdb_entry *star_mp)
 			sg_mp = br_mdb_ip_get(br, &sg_ip);
 			if (!sg_mp)
 				continue;
-			sg_mp->flags &= ~BRIDGE_MDBE_F_HOST_JOINED;
-			sg_mp->flags |= star_mp->flags & BRIDGE_MDBE_F_HOST_JOINED;
+			sg_mp->flags &= ~BRIDGE_MDBE_F_HOST_MASK;
+			sg_mp->flags |= star_mp->flags & BRIDGE_MDBE_F_HOST_MASK;
 		}
 	}
 }
@@ -454,7 +454,7 @@ static void br_multicast_sg_del_exclude_ports(struct net_bridge_mdb_entry *sgmp)
 	 * we treat it as EXCLUDE {}, so for an S,G it's considered a
 	 * STAR_EXCLUDE entry and we can safely leave it
 	 */
-	sgmp->flags &= ~BRIDGE_MDBE_F_HOST_JOINED;
+	sgmp->flags &= ~BRIDGE_MDBE_F_HOST_MASK;
 
 	for (pp = &sgmp->ports;
 	     (p = mlock_dereference(*pp, sgmp->br)) != NULL;) {
@@ -1470,10 +1470,18 @@ void br_multicast_del_port_group(struct net_bridge_port_group *p)
 }
 
 void br_multicast_host_join(const struct net_bridge_mcast *brmctx,
-			    struct net_bridge_mdb_entry *mp, bool notify)
+			    struct net_bridge_mdb_entry *mp,
+			    enum br_mcast_sr_op sr_op, bool notify)
 {
-	if (!(mp->flags & BRIDGE_MDBE_F_HOST_JOINED)) {
-		mp->flags |= BRIDGE_MDBE_F_HOST_JOINED;
+	u8 old_flags = mp->flags;
+
+	mp->flags |= BRIDGE_MDBE_F_HOST_JOINED;
+	if (sr_op == BR_MCAST_SR_SET)
+		mp->flags |= BRIDGE_MDBE_F_HOST_STREAM_RESERVED;
+	else if (sr_op == BR_MCAST_SR_CLEAR)
+		mp->flags &= ~BRIDGE_MDBE_F_HOST_STREAM_RESERVED;
+
+	if ((mp->flags ^ old_flags) & BRIDGE_MDBE_F_HOST_MASK) {
 		if (br_multicast_is_star_g(&mp->addr))
 			br_multicast_star_g_host_state(mp);
 		if (notify)
@@ -1483,6 +1491,14 @@ void br_multicast_host_join(const struct net_bridge_mcast *brmctx,
 	if (br_group_is_l2(&mp->addr))
 		return;
 
+	/* Host stream-reserved entries are permanent and have no timer; drop
+	 * any timer left from an earlier non-reserved host join.
+	 */
+	if (mp->flags & BRIDGE_MDBE_F_HOST_STREAM_RESERVED) {
+		timer_delete(&mp->timer);
+		return;
+	}
+
 	mod_timer(&mp->timer, jiffies + brmctx->multicast_membership_interval);
 }
 
@@ -1491,7 +1507,8 @@ void br_multicast_host_leave(struct net_bridge_mdb_entry *mp, bool notify)
 	if (!(mp->flags & BRIDGE_MDBE_F_HOST_JOINED))
 		return;
 
-	mp->flags &= ~BRIDGE_MDBE_F_HOST_JOINED;
+	mp->flags &= ~(BRIDGE_MDBE_F_HOST_JOINED |
+		       BRIDGE_MDBE_F_HOST_STREAM_RESERVED);
 	if (br_multicast_is_star_g(&mp->addr))
 		br_multicast_star_g_host_state(mp);
 	if (notify)
@@ -1520,7 +1537,7 @@ __br_multicast_add_group(struct net_bridge_mcast *brmctx,
 		return ERR_CAST(mp);
 
 	if (!pmctx) {
-		br_multicast_host_join(brmctx, mp, true);
+		br_multicast_host_join(brmctx, mp, BR_MCAST_SR_KEEP, true);
 		goto out;
 	}
 
diff --git a/net/bridge/br_private.h b/net/bridge/br_private.h
index 4ae050ae4826e..fbb7a8156f347 100644
--- a/net/bridge/br_private.h
+++ b/net/bridge/br_private.h
@@ -375,6 +375,18 @@ struct net_bridge_port_group {
 
 #define BRIDGE_MDBE_F_HOST_JOINED		BIT(0)
 #define BRIDGE_MDBE_F_HOST_STREAM_RESERVED	BIT(1)
+#define BRIDGE_MDBE_F_HOST_MASK \
+	(BRIDGE_MDBE_F_HOST_JOINED | BRIDGE_MDBE_F_HOST_STREAM_RESERVED)
+
+/* How a host join treats BRIDGE_MDBE_F_HOST_STREAM_RESERVED. Only the MDB
+ * netlink path administers the flag (SET/CLEAR); data-path joins must leave an
+ * existing reservation intact (KEEP).
+ */
+enum br_mcast_sr_op {
+	BR_MCAST_SR_KEEP,
+	BR_MCAST_SR_CLEAR,
+	BR_MCAST_SR_SET,
+};
 
 struct net_bridge_mdb_entry {
 	struct rhash_head		rhnode;
@@ -1049,7 +1061,8 @@ int br_mdb_dump(struct net_device *dev, struct sk_buff *skb,
 int br_mdb_get(struct net_device *dev, struct nlattr *tb[], u32 portid, u32 seq,
 	       struct netlink_ext_ack *extack);
 void br_multicast_host_join(const struct net_bridge_mcast *brmctx,
-			    struct net_bridge_mdb_entry *mp, bool notify);
+			    struct net_bridge_mdb_entry *mp,
+			    enum br_mcast_sr_op sr_op, bool notify);
 void br_multicast_host_leave(struct net_bridge_mdb_entry *mp, bool notify);
 void br_multicast_star_g_handle_mode(struct net_bridge_port_group *pg,
 				     u8 filter_mode);
diff --git a/tools/testing/selftests/net/forwarding/bridge_mdb_stream_reserved.sh b/tools/testing/selftests/net/forwarding/bridge_mdb_stream_reserved.sh
index a21dc2ec3e95c..4c5933455037a 100755
--- a/tools/testing/selftests/net/forwarding/bridge_mdb_stream_reserved.sh
+++ b/tools/testing/selftests/net/forwarding/bridge_mdb_stream_reserved.sh
@@ -30,6 +30,8 @@
 ALL_TESTS="
 	cfg_test
 	fwd_sr_member_test
+	fwd_sr_host_member_test
+	fwd_sr_host_persistence_test
 	fwd_foreign_blocked_test
 	fwd_unicast_blocked_test
 	fwd_flag_gates_test
@@ -217,11 +219,43 @@ cfg_test()
 	bridge mdb add dev br0 port $swp2 grp $GRP vid $VID \
 		stream_reserved 2>/dev/null
 	check_fail $? "non-permanent stream_reserved port entry accepted"
-
-	# The flag must be rejected on host groups.
-	bridge mdb add dev br0 port br0 grp $GRP permanent vid $VID \
+	bridge mdb add dev br0 port br0 grp $GRP vid $VID \
 		stream_reserved 2>/dev/null
-	check_fail $? "stream_reserved accepted on a host group"
+	check_fail $? "non-permanent stream_reserved host group accepted"
+
+	# A plain (non-SR) host join is still accepted, must not be permanent,
+	# and toggles cleanly with stream_reserved on replace.
+	bridge mdb add dev br0 port br0 grp $GRP vid $VID
+	check_err $? "plain host join rejected"
+	bridge mdb add dev br0 port br0 grp $GRP permanent vid $VID 2>/dev/null
+	check_fail $? "permanent flag accepted on a plain host group"
+	bridge -d mdb show dev br0 | grep "port br0" | grep "$GRP" | \
+		grep -q "stream_reserved"
+	check_fail $? "stream_reserved unexpectedly set on a plain host join"
+	bridge mdb replace dev br0 port br0 grp $GRP permanent vid $VID \
+		stream_reserved
+	check_err $? "Failed to replace plain host join with stream_reserved"
+	bridge mdb replace dev br0 port br0 grp $GRP vid $VID
+	check_err $? "Failed to replace stream_reserved host group with plain"
+	bridge -d mdb show dev br0 | grep "port br0" | grep "$GRP" | \
+		grep -q "stream_reserved"
+	check_fail $? "stream_reserved not cleared on host group replace"
+	bridge mdb del dev br0 port br0 grp $GRP vid $VID
+
+	# permanent + stream_reserved is accepted on host groups and the
+	# entry is dumped as both permanent and stream_reserved.
+	bridge mdb add dev br0 port br0 grp $GRP permanent vid $VID \
+		stream_reserved
+	check_err $? "stream_reserved rejected on a host group"
+	bridge -d mdb show dev br0 | grep "port br0" | grep "$GRP" | \
+		grep -q "stream_reserved"
+	check_err $? "stream_reserved flag not shown on host group"
+	bridge -d mdb get dev br0 grp $GRP vid $VID | grep -q permanent
+	check_err $? "host stream_reserved entry not reported as permanent"
+	bridge -d -s mdb get dev br0 grp $GRP vid $VID | grep "port br0" | \
+		grep -q " 0.00"
+	check_err $? "host stream_reserved entry has a pending group timer"
+	bridge mdb del dev br0 port br0 grp $GRP permanent vid $VID
 
 	# Add a port group with the flag and confirm it is reflected in dump.
 	bridge mdb add dev br0 port $swp2 grp $GRP permanent vid $VID \
@@ -328,6 +362,89 @@ fwd_sr_member_test()
 	log_test "MDB stream_reserved member delivery"
 }
 
+# An SR-class frame for a group the local bridge host has joined with
+# stream_reserved is delivered to the host (passed up via br0); without the
+# flag set on the host join, the same frame is denied at ingress.
+fwd_sr_host_member_test()
+{
+	RET=0
+
+	tc qdisc add dev br0 clsact
+	sr_filter $swp1 on
+
+	# Host join WITHOUT stream_reserved: SR-class frame must be dropped.
+	# A plain host-joined IP group cannot be permanent.
+	bridge mdb add dev br0 port br0 grp $GRP vid $VID
+	rx_filter_install br0 6 $GRP
+
+	send_mc $GRP $GRP_DMAC $SR_PCP
+	tc_check_packets "dev br0 ingress" 6 0
+	check_err $? "SR-class frame delivered to host without stream_reserved"
+
+	send_mc $GRP $GRP_DMAC $BE_PCP
+	tc_check_packets "dev br0 ingress" 6 1
+	check_err $? "best-effort frame not delivered to host"
+
+	# Replace host join WITH stream_reserved: SR-class frame admitted.
+	bridge mdb replace dev br0 port br0 grp $GRP permanent vid $VID \
+		stream_reserved
+	check_err $? "Failed to replace host group with stream_reserved"
+
+	send_mc $GRP $GRP_DMAC $SR_PCP
+	tc_check_packets "dev br0 ingress" 6 2
+	check_err $? "reserved-stream SR-class frame not delivered to host"
+
+	rx_filter_uninstall br0 6
+	bridge mdb del dev br0 port br0 grp $GRP permanent vid $VID
+	sr_filter $swp1 off
+	tc qdisc del dev br0 clsact
+
+	log_test "MDB stream_reserved host listener delivery"
+}
+
+# A permanent + stream_reserved host group has no group timer and must
+# outlive the membership interval, even when promoted from a plain (timer
+# armed) host join, which must not leave a stale group timer behind.
+fwd_sr_host_persistence_test()
+{
+	RET=0
+
+	ip link set dev br0 type bridge mcast_membership_interval 200
+
+	bridge mdb add dev br0 port br0 grp $GRP permanent vid $VID \
+		stream_reserved
+	check_err $? "Failed to add permanent stream_reserved host group"
+
+	sleep 3
+
+	bridge mdb get dev br0 grp $GRP vid $VID &>/dev/null
+	check_err $? "host stream_reserved entry expired"
+
+	bridge mdb del dev br0 port br0 grp $GRP permanent vid $VID
+
+	# A plain host join arms the group timer; promoting it to
+	# stream_reserved must cancel that timer, otherwise the reservation is
+	# torn down when the stale timer expires.
+	bridge mdb add dev br0 port br0 grp $GRP vid $VID
+	check_err $? "plain host join rejected"
+	bridge mdb replace dev br0 port br0 grp $GRP permanent vid $VID \
+		stream_reserved
+	check_err $? "Failed to promote plain host join to stream_reserved"
+	bridge -d -s mdb get dev br0 grp $GRP vid $VID | grep "port br0" | \
+		grep -q " 0.00"
+	check_err $? "stale group timer left after promotion to stream_reserved"
+
+	sleep 3
+
+	bridge mdb get dev br0 grp $GRP vid $VID &>/dev/null
+	check_err $? "promoted stream_reserved entry expired"
+
+	bridge mdb del dev br0 port br0 grp $GRP permanent vid $VID
+	ip link set dev br0 type bridge mcast_membership_interval 26000
+
+	log_test "MDB stream_reserved host entry persistence"
+}
+
 # swp1 filters SR-class ingress. A foreign (non-reserved) group GRP2 at SR class
 # is dropped at ingress, reaching neither listener, while a best-effort (TC 0)
 # frame is admitted and delivered to both.
-- 
2.43.0
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help