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
@@ -29,7 +29,9 @@ Real ICMP crossing the switch proves the datapath:
|
||||
* VLAN programming applies to absorbed anchors: different access VLANs
|
||||
isolate the routers (ping dies), the same VLAN bridges them again;
|
||||
* a ``delay 100`` filter lands as netem on the absorbed anchor (single
|
||||
interface, switch-owned) and the measured RTT grows by ~100 ms;
|
||||
interface, switch-owned) and the measured RTT grows by ~100 ms — on both
|
||||
link endpoint orders (peer-first and switch-first; the order decides
|
||||
which side's NIO update runs last and must not decide the outcome);
|
||||
* suspend admin-downs the anchor and kills the traffic; resume restores;
|
||||
* capture (hosted by the switch side) writes a pcap of ICMP records;
|
||||
* deleting a link detaches the anchor (which survives — the router owns
|
||||
@@ -128,8 +130,12 @@ def test_ethernet_switch_kernel_fast_path():
|
||||
# The switch bridge exists from the moment the switch node does; a
|
||||
# link against a stopped router must not fail — the join defers.
|
||||
assert harness.bridge_members(sw_bridge) == []
|
||||
# link1 is created peer-first, link2 switch-first: the endpoint order
|
||||
# decides which side's NIO update runs last, and only the switch-first
|
||||
# order used to expose the passive end's tc-reset wipe (its empty-filter
|
||||
# reconcile ran after the switch's apply). Both orders must work.
|
||||
link1 = compute.create_link(pid, (r1_id, *ETH), (switch_id, 0, 0))
|
||||
link2 = compute.create_link(pid, (r2_id, *ETH), (switch_id, 0, 1))
|
||||
link2 = compute.create_link(pid, (switch_id, 0, 1), (r2_id, *ETH))
|
||||
assert link1["kernel_datapath"] is True, link1
|
||||
assert link2["kernel_datapath"] is True, link2
|
||||
assert harness.bridge_members(sw_bridge) == []
|
||||
@@ -223,6 +229,22 @@ def test_ethernet_switch_kernel_fast_path():
|
||||
fast = harness.wait_ping(c1, "10.1.1.2")
|
||||
assert fast["success"] == 100 and fast["avg"] < 50, fast
|
||||
|
||||
# The same single-owned impairment on link2 — created switch-first,
|
||||
# the order whose peer NIO update runs LAST. The passive end's empty
|
||||
# reconcile used to tc-reset the shared anchor right after the switch
|
||||
# applied the netem, wiping it (API green, data plane unimpaired);
|
||||
# the qdisc and the RTT must both hold under this order too.
|
||||
compute.call("PUT", f"/projects/{pid}/links/{link2['link_id']}", {"filters": {"delay": [100]}})
|
||||
assert "netem" in harness.qdiscs(a2), harness.qdiscs(a2)
|
||||
assert "netem" not in harness.qdiscs(a1), harness.qdiscs(a1)
|
||||
delayed2 = harness.wait_ping(c1, "10.1.1.2")
|
||||
assert delayed2["success"] == 100, delayed2["raw"]
|
||||
assert 60 <= delayed2["avg"] <= 250, (baseline, delayed2)
|
||||
compute.call("PUT", f"/projects/{pid}/links/{link2['link_id']}", {"filters": {}})
|
||||
assert "netem" not in harness.qdiscs(a2)
|
||||
fast2 = harness.wait_ping(c1, "10.1.1.2")
|
||||
assert fast2["success"] == 100 and fast2["avg"] < 50, fast2
|
||||
|
||||
# suspend: the anchor admin-downs, the link is dead; resume restores
|
||||
compute.call("PUT", f"/projects/{pid}/links/{link1['link_id']}", {"suspend": True})
|
||||
assert not harness.tap_up(a1)
|
||||
|
||||
Reference in new issue
Block a user