diff --git a/gns3server/controller/udp_link.py b/gns3server/controller/udp_link.py index 5b37513d3..4ed74b3e8 100644 --- a/gns3server/controller/udp_link.py +++ b/gns3server/controller/udp_link.py @@ -20,6 +20,7 @@ import asyncio 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 from gns3server.utils.packet_filter_validation import ( KERNEL_UNSUPPORTED_FILTERS, @@ -56,14 +57,18 @@ log = logging.getLogger(__name__) def _is_unix_socket_docker(node): """ Best-effort detection of vendor containers bridged through AF_UNIX - socket pairs (GNS3_UNIX_SOCKET_NIO in their environment): they have no - host-side interface to enslave into a kernel bridge. The compute side + 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 here only surfaces as a clearer-late error. + 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 + return "GNS3_UNIX_SOCKET_NIO" in environment or is_iol_runner_environment(environment) class UDPLink(Link): diff --git a/tests/controller/test_kernel_datapath_link.py b/tests/controller/test_kernel_datapath_link.py index 1caff1f7e..9087fe720 100644 --- a/tests/controller/test_kernel_datapath_link.py +++ b/tests/controller/test_kernel_datapath_link.py @@ -419,6 +419,47 @@ async def test_kernel_datapath_not_eligible_for_unix_socket_containers(project): assert link._kernel_datapath_eligible(node1, node2) is False +@pytest.mark.asyncio +async def test_kernel_datapath_not_eligible_for_iol_runner_containers(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. + """ + + link, node1, node2 = await _kernel_link(project) + node2._properties = {"environment": "GNS3_IOL_RUNNER=1"} + assert link._kernel_datapath_eligible(node1, node2) is False + + # 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 + + +@pytest.mark.asyncio +async def test_kernel_datapath_not_eligible_for_an_iol_runner_container_on_a_switch(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. + """ + + 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 + + @pytest.mark.asyncio async def test_kernel_datapath_disabled_by_config(project): @@ -915,9 +956,7 @@ async def test_switch_link_repushes_the_switch_nio_on_node_start(project): switch.put = AsyncioMagicMock() await link.node_started(peer) - switch.put.assert_called_once_with( - "/adapters/0/ports/0/nio", data=entries[0][3], timeout=120 - ) + switch.put.assert_called_once_with("/adapters/0/ports/0/nio", data=entries[0][3], timeout=120) # a relay link never re-pushes anything switch.put.reset_mock()