test(e2e): take the switch anchors out of NetworkManager's reach

The esw kernel fast-path scenario deletes and re-creates a link, which
re-enslaves the peer anchor — and on NetworkManager hosts NM's netdev
state machine can release an NM-managed TAP from its bridge some 60-70 ms
after enslavement, ~50% of re-enslavements, while the tap is in NM's
not-yet-assumed state. The freed port stays administratively up and no
GNS3 process logs anything: the membership assert sees the join, then the
ping measures a silent blackhole (this was the line-277 e2e failure).

Root-caused with a server-free harness (one uBridge, one fresh tap, one
bridge, an addif/delif loop): the same tap frees 20/40 while NM-managed,
0/40 after `nmcli device set <tap> managed no`, 20/40 again when
re-managed. Docker's veth host ends are not NM-managed, which is why only
TAP anchors ever showed it — including the "anchor lost its bridge
membership after a ports_mapping update (TAP peers only)" incident from
the switch's development, whose conclusion is corrected in the docs.

Unmanage the anchors once the routers have booted, before the scenario
re-enslaves them; document the failure signature and the host-side remedy
(per-device nmcli call, or NM unmanaged-devices configuration) in the
L2-anchor spec and the switch feature doc.
This commit is contained in:
YueGuobin committed 2026-10-06 00:21:11 +08:00
1 parent dc5e2d90ba
commit 1c85befbab
4 files changed
+51 -4

No files matched your search

+26
View File
@@ -1053,6 +1053,32 @@ def bridge_members(bridge):
return sorted(os.listdir(f"/sys/class/net/{bridge}/brif"))
def unmanage_from_networkmanager(*names):
"""Best-effort ``nmcli device set <name> managed no`` for the given
interfaces.
NetworkManager (desktop hosts) manages the TAP devices uBridge creates.
While a tap is in NM's not-yet-assumed state, NM's netdev-event state
machine can *release* it from its bridge some 60-70 ms after
enslavement (measured: ~50% of re-enslavements on an NM host): the port
is left administratively up, out of the bridge, with no server-side log
or error — a silent blackhole. Scenarios that detach and re-enslave an
anchor (link delete + recreate, node restart) must call this once their
nodes have booted, or they are nondeterministically flaky on such
hosts. Silently a no-op where nmcli is unavailable.
"""
nmcli = shutil.which("nmcli")
if not nmcli:
return
for name in names:
# S603: resolved binary, fixed argv, no shell
subprocess.run( # noqa: S603
[nmcli, "device", "set", name, "managed", "no"],
capture_output=True,
check=False,
)
# ---------------------------------------------------------------------------
# L2-anchor spec assertions (docs/design/ubridge-l2-anchor-spec.md §E)
# ---------------------------------------------------------------------------
@@ -146,6 +146,15 @@ def test_ethernet_switch_kernel_fast_path():
c1 = _boot(server, pid, r1_id)
c2 = _boot(server, pid, r2_id)
# Keep NetworkManager's hands off the anchors: on NM hosts its
# netdev state machine can release a freshly re-enslaved tap from
# the bridge ~60 ms after the join (silently — the port stays up,
# the server keeps believing it is attached), which the link
# delete/recreate below triggers nondeterministically. The first
# join at node start is unaffected; this covers everything after
# it.
harness.unmanage_from_networkmanager(a1, a2)
# Deferred joins completed by the node-start re-push: the anchors
# ARE the switch ports now.
assert harness.wait_until(lambda: harness.bridge_members(sw_bridge) == sorted([a1, a2]), timeout=15), (