feat(controller): let IOL runner containers take the kernel datapath

_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).
This commit is contained in:
YueGuobin 2026-10-02 13:30:42 +08:00
parent 4b4a0fc35f
commit 13c1ffb951
No known key found for this signature in database
2 changed files with 80 additions and 34 deletions

View File

@ -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"],

View File

@ -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