mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-03 05:51: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
@@ -135,6 +135,21 @@ class KernelDatapathMixin:
|
||||
# Link attach / detach
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@staticmethod
|
||||
def _owns_anchor_impairments(nio):
|
||||
"""
|
||||
Whether this end owns the tc state (netem, cls_bpf, eBPF) of the
|
||||
anchor it was handed. A NIOBridge with no bridge of its own
|
||||
(``bridge is None``) means the anchor is absorbed by an Ethernet
|
||||
switch: the switch owns the anchor's bridge membership and its
|
||||
impairment state — the controller routes the link's filters to the
|
||||
switch end, so this NIO always carries an empty filter set, whose
|
||||
reconcile is a plain ``tc reset``. Applying it here would silently
|
||||
wipe the netem/bpf/eBPF the switch just put on the shared anchor.
|
||||
"""
|
||||
|
||||
return not (isinstance(nio, NIOBridge) and nio.bridge is None)
|
||||
|
||||
async def _kernel_attach(self, anchor, nio):
|
||||
"""
|
||||
Attach an anchor to the per-link kernel bridge carried by *nio*
|
||||
@@ -150,9 +165,11 @@ class KernelDatapathMixin:
|
||||
A NIO whose ``bridge`` is None means the anchor is bridged
|
||||
elsewhere — an Ethernet switch absorbed it into the switch's own
|
||||
bridge — so the membership work is skipped here and only the link
|
||||
state the anchor carries (capture, markers, impairment filters) is
|
||||
applied. Exactly one end of such a link owns the anchor's bridge
|
||||
state; the other end is a passive carrier.
|
||||
state this end owns is applied: capture and markers always, the
|
||||
impairment filters only when this end owns the anchor's tc state
|
||||
(see _owns_anchor_impairments). Exactly one end of such a link
|
||||
owns the anchor's bridge and impairment state; the other end is a
|
||||
passive carrier.
|
||||
"""
|
||||
|
||||
if nio.bridge is not None:
|
||||
@@ -174,10 +191,13 @@ class KernelDatapathMixin:
|
||||
# (restored here on node restart, like the capture above); bpf
|
||||
# expressions become cls_bpf match-drop classifiers and
|
||||
# frequency_drop/quota/window_drop become the eBPF stateful
|
||||
# classifier.
|
||||
await self._ubridge_apply_netem(anchor, nio.filters)
|
||||
await self._ubridge_apply_bpf_drops(anchor, nio.filters)
|
||||
await self._ubridge_apply_ebpf_drops(anchor, nio.filters)
|
||||
# classifier. A passive end owns none of this: its empty filter
|
||||
# set is a tc reset, which would wipe the switch's qdisc off the
|
||||
# shared anchor.
|
||||
if self._owns_anchor_impairments(nio):
|
||||
await self._ubridge_apply_netem(anchor, nio.filters)
|
||||
await self._ubridge_apply_bpf_drops(anchor, nio.filters)
|
||||
await self._ubridge_apply_ebpf_drops(anchor, nio.filters)
|
||||
|
||||
async def _relay_attach(self, anchor, bridge_name, nio):
|
||||
"""
|
||||
@@ -226,16 +246,19 @@ class KernelDatapathMixin:
|
||||
"""
|
||||
Re-apply everything a kernel link carries on an already-attached
|
||||
anchor (filters and markers changed): no re-enslaving, no bridge
|
||||
create.
|
||||
create. A passive end (bridge is None — an anchor absorbed by an
|
||||
Ethernet switch) leaves the anchor's impairment state to the
|
||||
switch that owns it.
|
||||
"""
|
||||
|
||||
await self._ubridge_apply_markers(anchor, nio)
|
||||
# The netem apply resets the interface first (the kernel merges
|
||||
# optional netem attrs on replace), so everything anchored on clsact
|
||||
# must re-apply after it: bpf drops flush + re-add, eBPF modes re-set.
|
||||
await self._ubridge_apply_netem(anchor, nio.filters)
|
||||
await self._ubridge_apply_bpf_drops(anchor, nio.filters)
|
||||
await self._ubridge_apply_ebpf_drops(anchor, nio.filters)
|
||||
if self._owns_anchor_impairments(nio):
|
||||
await self._ubridge_apply_netem(anchor, nio.filters)
|
||||
await self._ubridge_apply_bpf_drops(anchor, nio.filters)
|
||||
await self._ubridge_apply_ebpf_drops(anchor, nio.filters)
|
||||
|
||||
async def _remove_kernel_nio(self, nio, adapter_number, port_number=0):
|
||||
"""
|
||||
|
||||
Reference in new issue
Block a user