Ports that do not participate in STP or RSTP while they are enabled on a bridge can easily become not floodable, because the bundle update checks the STP/RSTP config on the bridge and not on the port and these ports have their default forwarding disabled state. So, every time a bundle update runs, all bundles that do not participate in STP/RSTP are marked as not floodable, this means all the internal ports, including the bridge port, become non-floodable, as they do not participate by design.
The issue was introduced quite some time ago, but became much easier to hit since the fix that made bundle update run on every reconfiguration to make sure the floodable flags are updated. Reported-at: https://mail.openvswitch.org/pipermail/ovs-discuss/2026-September/054022.html Reported-by: Svenne Krap <[email protected]> Fixes: 4b5f19962adc ("stp,rstp: disable learning and forwarding in STP/RSTP disabled state.") Fixes: 332ca1dad732 ("ofproto-dpif: Fix bundle floodable flag when disabling STP/RSTP.") Signed-off-by: Ilya Maximets <[email protected]> --- ofproto/ofproto-dpif.c | 8 ++-- tests/ofproto-dpif.at | 92 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 4 deletions(-) diff --git a/ofproto/ofproto-dpif.c b/ofproto/ofproto-dpif.c index 0f0f71d14..443de8708 100644 --- a/ofproto/ofproto-dpif.c +++ b/ofproto/ofproto-dpif.c @@ -3376,8 +3376,8 @@ bundle_update(struct ofbundle *bundle) LIST_FOR_EACH (port, bundle_node, &bundle->ports) { if (port->up.pp.config & OFPUTIL_PC_NO_FLOOD || netdev_get_pt_mode(port->up.netdev) == NETDEV_PT_LEGACY_L3 - || (bundle->ofproto->stp && !stp_forward_in_state(port->stp_state)) - || (bundle->ofproto->rstp && !rstp_forward_in_state(port->rstp_state))) { + || (port->stp_port && !stp_forward_in_state(port->stp_state)) + || (port->rstp_port && !rstp_forward_in_state(port->rstp_state))) { bundle->floodable = false; break; } @@ -3425,8 +3425,8 @@ bundle_add_port(struct ofbundle *bundle, ofp_port_t ofp_port, ovs_list_push_back(&bundle->ports, &port->bundle_node); if (port->up.pp.config & OFPUTIL_PC_NO_FLOOD || netdev_get_pt_mode(port->up.netdev) == NETDEV_PT_LEGACY_L3 - || (bundle->ofproto->stp && !stp_forward_in_state(port->stp_state)) - || (bundle->ofproto->rstp && !rstp_forward_in_state(port->rstp_state))) { + || (port->stp_port && !stp_forward_in_state(port->stp_state)) + || (port->rstp_port && !rstp_forward_in_state(port->rstp_state))) { bundle->floodable = false; } } diff --git a/tests/ofproto-dpif.at b/tests/ofproto-dpif.at index efb36e058..58f395ab3 100644 --- a/tests/ofproto-dpif.at +++ b/tests/ofproto-dpif.at @@ -14037,3 +14037,95 @@ Datapath actions: 100,1 OVS_VSWITCHD_STOP AT_CLEANUP + +AT_SETUP([ofproto-dpif - bundle floodable flag on internal port - STP]) +AT_KEYWORDS([stp bundle floodable]) +OVS_VSWITCHD_START + +add_of_ports br0 1 2 3 +AT_CHECK([ovs-vsctl set interface p1 type=internal]) + +AT_CHECK([ovs-vsctl set bridge br0 stp_enable=true]) +AT_CHECK([ovs-ofctl add-flow br0 action=NORMAL]) + +AT_CHECK([ovs-appctl time/stop]) + +# Give time for STP to synchronize. +AT_CHECK([ovs-appctl time/warp 30000 3000], [0], [ignore]) + +dnl Verify we are in forwarding state. +AT_CHECK([ovs-appctl stp/show br0 | grep "p2.*forwarding"], [0], [ignore]) +AT_CHECK([ovs-appctl stp/show br0 | grep "p3.*forwarding"], [0], [ignore]) + +dnl Send a broadcast packet on p2, verify it floods to the bridge port, the +dnl internal p1 and the dummy p3. +AT_CHECK([ovs-appctl ofproto/trace br0 \ + 'in_port=p2,eth_src=00:00:00:00:00:01,eth_dst=ff:ff:ff:ff:ff:ff' \ + -generate --names], [0], [stdout]) +AT_CHECK([tail -1 stdout | sed 's/Datapath actions: //' \ + | tr "," "\n" | sort | tr "\n" ","], [0], [br0,p1,p3,]) + +dnl Delete p3 to trigger reconfiguration. +AT_CHECK([ovs-vsctl del-port p3]) + +dnl Verify we are still in forwarding state. +AT_CHECK([ovs-appctl time/warp 30000 3000], [0], [ignore]) +AT_CHECK([ovs-appctl stp/show br0 | grep "p2.*forwarding"], [0], [ignore]) + +dnl Send a broadcast packet on p2, verify it still floods to both the bridge +dnl port and the p1. +AT_CHECK([ovs-appctl ofproto/trace br0 \ + 'in_port=p2,eth_src=00:00:00:00:00:01,eth_dst=ff:ff:ff:ff:ff:ff' \ + -generate --names], [0], [stdout]) +AT_CHECK([tail -1 stdout], [0], [dnl +Datapath actions: br0,p1 +]) + +OVS_VSWITCHD_STOP +AT_CLEANUP + +AT_SETUP([ofproto-dpif - bundle floodable flag on internal port - RSTP]) +AT_KEYWORDS([rstp bundle floodable]) +OVS_VSWITCHD_START + +add_of_ports br0 1 2 3 +AT_CHECK([ovs-vsctl set interface p1 type=internal]) + +AT_CHECK([ovs-vsctl set bridge br0 rstp_enable=true]) +AT_CHECK([ovs-ofctl add-flow br0 action=NORMAL]) + +AT_CHECK([ovs-appctl time/stop]) + +# Give time for RSTP to synchronize. +AT_CHECK([ovs-appctl time/warp 5000 500], [0], [ignore]) + +dnl Verify we are in forwarding state. +AT_CHECK([ovs-appctl rstp/show br0 | grep "p2.*Designated"], [0], [ignore]) +AT_CHECK([ovs-appctl rstp/show br0 | grep "p3.*Designated"], [0], [ignore]) + +dnl Send a broadcast packet on p2, verify it floods to the bridge port, the +dnl internal p1 and the dummy p3. +AT_CHECK([ovs-appctl ofproto/trace br0 \ + 'in_port=p2,eth_src=00:00:00:00:00:01,eth_dst=ff:ff:ff:ff:ff:ff' \ + -generate --names], [0], [stdout]) +AT_CHECK([tail -1 stdout | sed 's/Datapath actions: //' \ + | tr "," "\n" | sort | tr "\n" ","], [0], [br0,p1,p3,]) + +dnl Delete p3 to trigger reconfiguration. +AT_CHECK([ovs-vsctl del-port p3]) + +dnl Verify we are still in forwarding state. +AT_CHECK([ovs-appctl time/warp 5000 500], [0], [ignore]) +AT_CHECK([ovs-appctl rstp/show br0 | grep "p2.*Designated"], [0], [ignore]) + +dnl Send a broadcast packet on p2, verify it still floods to both the bridge +dnl port and the p1. +AT_CHECK([ovs-appctl ofproto/trace br0 \ + 'in_port=p2,eth_src=00:00:00:00:00:01,eth_dst=ff:ff:ff:ff:ff:ff' \ + -generate --names], [0], [stdout]) +AT_CHECK([tail -1 stdout], [0], [dnl +Datapath actions: br0,p1 +]) + +OVS_VSWITCHD_STOP +AT_CLEANUP -- 2.55.0 _______________________________________________ dev mailing list [email protected] https://mail.openvswitch.org/mailman/listinfo/ovs-dev
