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