From 58b97b4ce5e4ec1af580ce39a049ee7d645c959f Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Mon, 28 Sep 2026 00:39:03 +0800 Subject: [PATCH] fix(controller): don't push the synthetic suspend filter to kernel links update_suspend() piggybacks on the filter system: get_active_filters() returns {"frequency_drop": [-1]} while a link is suspended so relay-based node types drop all traffic at the uBridge filter. Kernel-datapath links implement suspend natively via interface carrier, and pushing the synthetic filter tripped the compute-side guard, making suspend fail with 409 on every kernel link (and leaving controller/kernel state torn). _get_node_filters() now returns empty filters for kernel links. --- gns3server/controller/udp_link.py | 7 +++++ tests/controller/test_kernel_datapath_link.py | 29 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/gns3server/controller/udp_link.py b/gns3server/controller/udp_link.py index 7a479c1cd..d78bf6c17 100644 --- a/gns3server/controller/udp_link.py +++ b/gns3server/controller/udp_link.py @@ -88,6 +88,13 @@ class UDPLink(Link): :returns: Tuple of (node1_filters, node2_filters) """ + # Kernel-datapath links implement suspend natively (interface carrier + # driven by the NIO suspend flag). The synthetic frequency_drop filter + # that get_active_filters() injects for suspended links is a + # relay-datapath emulation mechanism — sending it would trip the + # compute-side kernel guard, so kernel links always push empty filters. + if self.kernel_datapath: + return {}, {} filter_node = self._get_filter_node() return ( self.get_active_filters() if filter_node == node1 else {}, diff --git a/tests/controller/test_kernel_datapath_link.py b/tests/controller/test_kernel_datapath_link.py index f5e9dc938..e3502cdca 100644 --- a/tests/controller/test_kernel_datapath_link.py +++ b/tests/controller/test_kernel_datapath_link.py @@ -225,3 +225,32 @@ async def test_kernel_datapath_property(project): assert link.kernel_datapath is False await link._prepare() assert link.kernel_datapath is True + + +@pytest.mark.asyncio +async def test_update_suspend_kernel_link_sends_no_synthetic_filter(project): + """ + Suspend is emulated on the relay datapath via a synthetic frequency_drop + filter (get_active_filters); kernel links implement it natively via + interface carrier and must push empty filters — the compute-side guard + rejects non-empty filters on kernel NIOs. + """ + + link, node1, node2 = await _kernel_link(project) + await link._prepare() + + bodies = [] + + async def capture_put(path, data=None, **kwargs): + bodies.append(data) + + node1.put = capture_put + node2.put = capture_put + + await link.update_suspend(True) + + assert link._suspended is True + assert len(bodies) == 2 + for body in bodies: + assert body["filters"] == {} + assert body["suspend"] is True