mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-03 14:01:15 +02:00
fix(compute): a passive absorbed anchor no longer resets the switch's tc state
The peer end of an Ethernet switch fast-path link is a NIOBridge with no bridge of its own (the switch absorbed its anchor into the switch bridge). Its update reconcile still ran the impairment pass with the empty filter set the controller routes to that end — and an empty set is a plain `tc reset` on the shared anchor, silently wiping the netem/cls_bpf/eBPF the switch had just applied. Any link update did it (filters, markers, suspend, capture start/stop), and the outcome depended on the link's endpoint order: UdpLink.update() PUTs node1 then node2, so with the switch first its apply ran first and the peer's reset ran last (API green, data plane unimpaired), while with the peer first the reset ran first and the filter survived. That order dependence is also why the shipped e2e never caught it — it only created peer-first links. Ownership is now explicit: only the end that owns the anchor's tc state applies the impairment reconcile (_owns_anchor_impairments — a NIOBridge with `bridge is None` is a passive carrier, the same idiom the bridge-delete path already used). Markers, capture and the suspend carrier still apply on both ends: markers reconcile per process, so neither end can strip the other's, and the carrier converges by agreement. Red/green: two new docker unit tests fail on the unfixed tree with `tc reset "<anchor>"` and pass after; the switch e2e also creates one link switch-first and asserts the netem qdisc and the RTT hold under that order (failed with the wipe, passes with the fix). Full unit suite 2110 passed; the switch e2e file passed end to end (378s).
This commit is contained in:
3 files changed
+111
-13
No files matched your search
@@ -322,6 +322,32 @@ async def test_connect_nio_kernel_eexist_still_reapplies_everything(vm):
|
||||
vm._ubridge_send.assert_any_call(f'tc netem set "{host_ifc}" delay 10')
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_connect_nio_kernel_passive_anchor_skips_impairments(vm):
|
||||
"""
|
||||
A NIOBridge with no bridge of its own (bridge is None) is the absorbed
|
||||
end of an Ethernet switch link: the anchor belongs to the switch, which
|
||||
owns its tc state and applies the link's filters there (the controller
|
||||
routes them to the switch end, so this NIO always carries {}). The
|
||||
passive end must not reconcile impairments — an empty filter set is a
|
||||
tc reset, which would silently wipe the switch's qdisc off the shared
|
||||
anchor at every node start.
|
||||
"""
|
||||
|
||||
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": None})
|
||||
|
||||
await vm._connect_nio(0, nio)
|
||||
|
||||
tc_calls = [c.args[0] for c in vm._ubridge_send.call_args_list if str(c.args[0]).startswith("tc ")]
|
||||
assert tc_calls == [], tc_calls
|
||||
# The membership is the switch's as well: no per-link bridge work here.
|
||||
assert not any("brctl" in c.args[0] for c in vm._ubridge_send.call_args_list)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_connect_nio_kernel_create_failure_propagates(vm):
|
||||
|
||||
@@ -533,6 +559,33 @@ async def test_update_nio_kernel_turns_ebpf_modes_off(vm):
|
||||
assert "nth_drop" not in c.args[0] or "off" in c.args[0]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_nio_kernel_passive_anchor_skips_impairments(vm):
|
||||
"""
|
||||
The link-update reconcile on the absorbed (passive) end must leave the
|
||||
anchor's tc state to the switch: no tc reset / netem / drop classifiers
|
||||
from this end, or the reset would wipe what the switch applied moments
|
||||
earlier in the same link update (switch-first links PUT this side last).
|
||||
The carrier (suspend) is still driven here, and markers still reconcile
|
||||
(this end may host some).
|
||||
"""
|
||||
|
||||
vm._ubridge_hypervisor = MagicMock()
|
||||
vm._ubridge_send = AsyncioMagicMock()
|
||||
vm.status = "started"
|
||||
host_ifc, _ = vm._veth_names(0, 0)
|
||||
vm._kernel_veths[(0, 0)] = host_ifc
|
||||
nio = vm.manager.create_nio({"type": "nio_bridge", "bridge": None})
|
||||
vm._ethernet_adapters[0].add_nio(0, nio)
|
||||
|
||||
await vm.adapter_update_nio_binding(0, nio)
|
||||
|
||||
tc_calls = [c.args[0] for c in vm._ubridge_send.call_args_list if str(c.args[0]).startswith("tc ")]
|
||||
assert tc_calls == [], tc_calls
|
||||
# The carrier still follows the suspend flag on the shared anchor.
|
||||
vm._ubridge_send.assert_any_call(f'link set "{host_ifc}" up')
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_apply_ebpf_drops_requires_ebpf_capability(vm):
|
||||
"""
|
||||
|
||||
Reference in new issue
Block a user