fix(docker): delete the per-link kernel bridge when a node stops

The bridge teardown only hung off the link-removal path, but deleting a
node or closing a project never issues a per-link delete — the veths
disappeared with their containers and left the bridge behind as an empty
orphan (reproduced: create a kernel link, DELETE the project, the
gns3{link_id} bridge survives). `_remove_kernel_veths` now deletes the
node's kernel-NIO bridges alongside the veth host ends: both endpoints do
it, the peer's still-enslaved port answers EBUSY (suppressed, last one
wins) exactly like link deletion, a relay NIO's uBridge bridge is left
alone (it dies with the process), and a node restart rebuilds the bridge
from the NIO in _connect_nio.

Found while cleaning up the window_drop e2e: seven such bridges had
accumulated on this host. Re-verified live: the bridge is gone after the
project delete, and the window_drop e2e (which includes a node restart
with the kernel link attached) still passes 15/15.
This commit is contained in:
YueGuobin committed 2026-10-06 00:21:08 +08:00
1 parent 316e7002c1
commit 48dbdbe4a9
3 files changed
+48 -1

No files matched your search

@@ -1006,6 +1006,34 @@ async def test_remove_kernel_veths(vm):
assert vm._kernel_veths == {}
@pytest.mark.asyncio
async def test_remove_kernel_veths_deletes_per_link_bridge(vm):
"""
The per-link kernel bridge dies with the node too: deleting a project
never runs the link-teardown path, so without this the bridge stays
behind as an empty orphan (EBUSY from the peer's port is suppressed —
the last endpoint wins).
"""
vm._ubridge_hypervisor = MagicMock()
vm._ubridge_send = AsyncioMagicMock()
host_ifc, _ = vm._veth_names(0, 0)
vm._kernel_veths[(0, 0)] = host_ifc
nio = vm.manager.create_nio({"type": "nio_bridge", "bridge": BRIDGE})
vm._ethernet_adapters[0].add_nio(0, nio)
# a relay NIO's bridge is not this node's to delete (uBridge dies with it)
relay = vm.manager.create_nio({"type": "nio_udp", "lport": 10000, "rhost": "127.0.0.1", "rport": 10001})
vm._ethernet_adapters[0].add_nio(1, relay)
await vm._remove_kernel_veths()
vm._ubridge_send.assert_any_call(f'docker delete_veth "{host_ifc}"')
# exactly the kernel bridge — the relay NIO's uBridge bridge dies with
# the uBridge process and must not be touched here
deletes = [call.args[0] for call in vm._ubridge_send.call_args_list if "brctl delete" in call.args[0]]
assert deletes == [f'brctl delete "{BRIDGE}"']
def test_veth_names_deterministic_and_bounded(vm):
host_ifc, guest_ifc = vm._veth_names(3, 2)