From 13c1ffb951b7c32b36459c412618ca359970dadf Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Fri, 2 Oct 2026 13:30:42 +0800 Subject: [PATCH] feat(controller): let IOL runner containers take the kernel datapath MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _is_unix_socket_docker no longer excludes the IOL runner marker: those containers anchor on persistent TAPs now (compute side), so they graduate through _kernel_endpoint_ready's gate instead — both ubridge_bridge_tap (the swappable leg) and ubridge_tap (the tap module) strict True, an unreported capability keeping the link on the relay where it always works. The generic GNS3_UNIX_SOCKET_NIO exclusion keeps its shape (an IOL runner environment wins the compute's class selection, so the exclusion must not fire when both markers are set). The switch fast path names an IOL runner peer's anchor through kernel_anchor_type — the iol_docker key, never the gv veth namespace. The 307d676cb regression tests move with the semantics: the capability matrix replaces the unconditional relay, and the switch-path test also asserts the gx naming. Its original concern stands — a compute that cannot serve the anchor lifecycle must never be handed a kernel link (the switch's deferred join would make it a silent dead cable). --- gns3server/controller/udp_link.py | 42 +++++++---- tests/controller/test_kernel_datapath_link.py | 72 +++++++++++++------ 2 files changed, 80 insertions(+), 34 deletions(-) diff --git a/gns3server/controller/udp_link.py b/gns3server/controller/udp_link.py index cc8e3b356..391bb04e3 100644 --- a/gns3server/controller/udp_link.py +++ b/gns3server/controller/udp_link.py @@ -21,7 +21,7 @@ import logging from gns3server.config import Config from gns3server.utils.application_id import is_iol_runner_environment -from gns3server.utils.kernel_anchor import kernel_anchor_name, kernel_cascade_names +from gns3server.utils.kernel_anchor import kernel_anchor_name, kernel_anchor_type, kernel_cascade_names from gns3server.utils.packet_filter_validation import ( KERNEL_UNSUPPORTED_FILTERS, split_kernel_only_features, @@ -57,18 +57,21 @@ log = logging.getLogger(__name__) def _is_unix_socket_docker(node): """ Best-effort detection of vendor containers bridged through AF_UNIX - socket pairs: they have no host-side interface to enslave into a kernel - bridge. Two ways in — the generic GNS3_UNIX_SOCKET_NIO knob, or the IOL - runner marker, whose class (IOLDockerVM, selected on GNS3_IOL_RUNNER) - forces the unix-socket wiring whatever the knob says. The compute side - independently rejects kernel-datapath NIOs on these containers, so a - missed detection surfaces as a clearer-late error — except on the switch - fast path, where an absent anchor is deferred forever by design (a link - to a stopped node must not fail), making the miss a silently dead cable. + socket pairs with no anchor of their own: the generic + GNS3_UNIX_SOCKET_NIO knob. IOL runner containers also bridge through + unix sockets (their class forces the wiring), but their anchors are + persistent TAPs — they graduate through _kernel_endpoint_ready's + capability gate instead of this exclusion. When both markers are present + the IOL runner one wins the compute's class selection, so the exclusion + must not fire for it. The compute side independently rejects + kernel-datapath NIOs on anchor-less containers, so a missed detection + surfaces as a clearer-late error — except on the switch fast path, where + an absent anchor is deferred forever by design (a link to a stopped node + must not fail), making the miss a silently dead cable. """ environment = (node.properties or {}).get("environment") or "" - return "GNS3_UNIX_SOCKET_NIO" in environment or is_iol_runner_environment(environment) + return "GNS3_UNIX_SOCKET_NIO" in environment and not is_iol_runner_environment(environment) class UDPLink(Link): @@ -196,8 +199,12 @@ class UDPLink(Link): anchor (brctl) or attaches the relay to it (add_nio_ethernet) without touching the interfaces the node itself uses. Both endpoints must be on the same compute (there is no kernel link across hosts) and both - must be able to anchor: Docker adapters are born as veth pairs, - QEMU adapters as persistent TAPs, which need uBridge's tap module — + must be able to anchor: Docker adapters are born as veth pairs, and + IOL runner containers — whose guest leg is unix sockets, not a veth — + anchor on persistent TAPs reached through the bridge module's + swappable TAP leg (its own capability pair, see + _kernel_endpoint_ready). QEMU adapters are persistent TAPs, which + need uBridge's tap module — asked of that compute, since an old uBridge leaves QEMU on the relay datapath and cannot carry a kernel link at all. IOU's Ethernet bays anchor on persistent TAPs too, bound to its IOL fabric @@ -253,6 +260,15 @@ class UDPLink(Link): """ if node.node_type == "docker": + environment = (node.properties or {}).get("environment") or "" + if is_iol_runner_environment(environment): + # IOL runner containers anchor on persistent TAPs like IOU, + # through the bridge module's swappable TAP leg — both + # capabilities strict True: an unreported one (old uBridge, + # failed probe) keeps the link on the relay, where it always + # works. + capabilities = node.compute.capabilities or {} + return capabilities.get("ubridge_bridge_tap") is True and capabilities.get("ubridge_tap") is True return True capabilities = node.compute.capabilities or {} if node.node_type == "qemu": @@ -354,7 +370,7 @@ class UDPLink(Link): peer_index = 1 - index peer = self._nodes[peer_index]["node"] peer_anchor = kernel_anchor_name( - peer.node_type, + kernel_anchor_type(peer.node_type, (peer.properties or {}).get("environment")), peer.id, self._nodes[peer_index]["adapter_number"], self._nodes[peer_index]["port_number"], diff --git a/tests/controller/test_kernel_datapath_link.py b/tests/controller/test_kernel_datapath_link.py index 834172dde..9c0ababfe 100644 --- a/tests/controller/test_kernel_datapath_link.py +++ b/tests/controller/test_kernel_datapath_link.py @@ -431,43 +431,73 @@ async def test_kernel_datapath_not_eligible_for_unix_socket_containers(project): @pytest.mark.asyncio -async def test_kernel_datapath_not_eligible_for_iol_runner_containers(project): +async def test_iol_runner_containers_gate_on_the_bridge_tap_capabilities(project): """ - The IOL runner marker is the other way into the unix-socket datapath: the - compute selects IOLDockerVM on GNS3_IOL_RUNNER and that class wires the - adapters through AF_UNIX socket pairs whatever the generic - GNS3_UNIX_SOCKET_NIO knob says — and the documented environment for - those nodes carries only the marker. Matching the knob alone let them - through to a kernel link their compute cannot serve. + The IOL runner marker selects a container whose guest leg is unix + sockets: it can still anchor kernel links (persistent TAPs through the + bridge module's swappable TAP leg), but only when its compute reports + both ubridge_bridge_tap and ubridge_tap — an unreported capability (old + uBridge, failed probe) keeps the link on the relay, where it always + works. This is also the regression guard for the compute that cannot + serve the anchor lifecycle being handed a kernel link anyway. """ - link, node1, node2 = await _kernel_link(project) - node2._properties = {"environment": "GNS3_IOL_RUNNER=1"} - assert link._kernel_datapath_eligible(node1, node2) is False + def iol_capable_compute(bridge_tap, tap): + compute = MagicMock() + compute.id = "compute-1" + compute.capabilities = {"ubridge_bridge_tap": bridge_tap, "ubridge_tap": tap} + return compute - # the marker travels in a multi-line environment like any other knob - node2._properties = {"environment": "GNS3_IOL_RUNNER=1\nGNS3_IOL_STARTUP_CONFIG=cfg.txt"} - assert link._kernel_datapath_eligible(node1, node2) is False + for environment in ("GNS3_IOL_RUNNER=1", "GNS3_IOL_RUNNER=1\nGNS3_IOL_STARTUP_CONFIG=cfg.txt"): + compute = iol_capable_compute(True, True) + node1 = _node(project, compute, "docker1") + node2 = _node(project, compute, "iol1", environment=environment) + link = await _link(project, node1, node2) + assert link._kernel_datapath_eligible(node1, node2) is True, environment + + for bridge_tap, tap in ((None, True), (True, None), (False, True), (True, False)): + compute = iol_capable_compute(bridge_tap, tap) + node1 = _node(project, compute, "docker1") + node2 = _node(project, compute, "iol1", environment=environment) + link = await _link(project, node1, node2) + assert link._kernel_datapath_eligible(node1, node2) is False, (environment, bridge_tap, tap) @pytest.mark.asyncio -async def test_kernel_datapath_not_eligible_for_an_iol_runner_container_on_a_switch(project): +async def test_iol_runner_container_on_a_switch_gates_and_names_the_iol_docker_anchor(project): """ - The switch fast path absorbs the peer's anchor, and a container bridged - through unix sockets has none — nor will it ever. The switch defers a - join against a stopped peer by design (it must not fail the link), so - such a cable would be silently dead instead of merely slow: the gate has - to run before the switch branch, keeping the link on the relay the - switch's own port TAP serves. + On the switch fast path an IOL runner peer gates on the same capability + pair — and when it passes, the switch absorbs an anchor named under the + ``iol_docker`` key (a persistent TAP, not the ``gv`` veth namespace). + Without the capabilities the link must stay on the relay: the switch + defers a join against a missing anchor by design (a link to a stopped + node must not fail), so a gate miss here is a silently dead cable, not + a late error. """ + from gns3server.utils.kernel_anchor import kernel_anchor_name + + compute = MagicMock() + compute.id = "compute-1" + compute.capabilities = {"ubridge_bridge_tap": True, "ubridge_tap": True} + switch = _node(project, compute, "sw1", node_type="ethernet_switch") + peer = _node(project, compute, "iol1", environment="GNS3_IOL_RUNNER=1") + link = await _link(project, switch, peer) + + assert link._kernel_datapath_eligible(switch, peer) is True + entries = await link._prepare() + switch_nio = entries[0][3] + assert switch_nio["type"] == "nio_anchor" + assert switch_nio["anchor"] == kernel_anchor_name("iol_docker", peer.id, 0, 0) + assert switch_nio["anchor"].startswith("gx") + + # a compute that cannot serve the swappable leg keeps the link relay compute = MagicMock() compute.id = "compute-1" compute.capabilities = {"ubridge_tap": True, "ubridge_iol_tap": True} switch = _node(project, compute, "sw1", node_type="ethernet_switch") peer = _node(project, compute, "iol1", environment="GNS3_IOL_RUNNER=1") link = await _link(project, switch, peer) - assert link._kernel_datapath_eligible(switch, peer) is False