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 <[email protected]> --- 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

