Gate the TAP/kernel datapath on uBridge >= 1.3.1

uBridge 1.2.3 already has the tap module, so the persistent-TAP probes
(_probe_persistent_taps on the node, probe_tap_support and siblings for
/capabilities) pass on it. QEMU, Dynamips and VPCS nodes then anchor on
TAPs and the controller builds kernel links that need 1.3.x commands:
kernel markers and the eBPF tc modes (1.3.0) and per-interface kernel
captures (`capture stop_kernel <ifc>`, 1.3.1). On 1.2.3 a MikroTik CHR
node sent frames but never got DHCP past DISCOVER/OFFER, and stopping
a capture failed with "Bad number of parameters".

Add Hypervisor.supports_kernel_datapath (version >= 1.3.1) and refuse
older builds in the node-side probe and the four anchoring capability
probes (tap, iol tap, bridge tap, vxlan). The existing fallback then
applies: nodes keep the legacy relay datapath with the usual warning,
which now names the installed version, and links stay on the relay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
tikD1337 2026-10-23 15:00:27 +02:00
parent 4d55cb1b9c
commit 52fcc83494
6 changed files with 118 additions and 1 deletions

View File

@ -53,6 +53,7 @@ import socket
import psutil
from gns3server.compute.error import NodeError
from gns3server.compute.ubridge.hypervisor import KERNEL_DATAPATH_MINIMUM_VERSION
from gns3server.compute.ubridge.ubridge_error import UbridgeError
from gns3server.utils.tc_capabilities import FILTER_EBPF_MODES, parse_tc_capabilities, usable_ebpf_modes
@ -158,8 +159,18 @@ class KernelDatapathMixin:
"Unknown command"/"Unknown module", a build without CAP_NET_ADMIN
fails outright. The caller composes its own warning and fallback:
the legacy relay datapath keeps working either way.
A build older than the kernel dataplane is refused without asking:
1.2.3 already has the tap module, so its `tap create` succeeds,
but the anchors it creates would then carry kernel links that need
1.3.x commands (per-interface kernel captures, kernel markers).
"""
if not self.ubridge.supports_kernel_datapath:
return UbridgeError(
f"uBridge {self.ubridge.version} is too old for the kernel datapath "
f"(>= {KERNEL_DATAPATH_MINIMUM_VERSION} required)"
)
try:
await self._ubridge_send(f'tap create "{probe_name}"')
except UbridgeError as e:

View File

@ -34,6 +34,13 @@ from .ubridge_hypervisor import UBridgeHypervisor
log = logging.getLogger(__name__)
# The kernel dataplane the persistent-TAP anchors feed (kernel links, kernel
# markers and captures) needs uBridge >= 1.3.1: 1.3.0 introduced it, 1.3.1
# made kernel captures per interface (`capture stop_kernel <ifc>`, which the
# server always sends). The tap module itself is older (1.2.3), so a
# successful `tap create` alone does not mean the dataplane is there.
KERNEL_DATAPATH_MINIMUM_VERSION = "1.3.1"
class Hypervisor(UBridgeHypervisor):
"""
@ -149,6 +156,20 @@ class Hypervisor(UBridgeHypervisor):
return self._version
@property
def supports_kernel_datapath(self):
"""
Whether this uBridge is recent enough for the kernel dataplane
(persistent-TAP anchors, kernel links). False until the version is
known, i.e. before start().
:returns: boolean
"""
if not self._version:
return False
return parse_version(self._version) >= parse_version(KERNEL_DATAPATH_MINIMUM_VERSION)
async def _check_ubridge_version(self, env=None):
"""
Checks if the ubridge executable version meets the minimum required.

View File

@ -45,7 +45,7 @@ from gns3server.config import Config
from gns3server.utils.tc_capabilities import parse_tc_capabilities
from gns3server.utils.vxlan import VXLAN_VNI_MAX, VXLAN_VNI_MIN
from .hypervisor import Hypervisor
from .hypervisor import KERNEL_DATAPATH_MINIMUM_VERSION, Hypervisor
from .ubridge_error import UbridgeError
log = logging.getLogger(__name__)
@ -65,6 +65,22 @@ _vxlan_cache: dict[tuple[str, int, int, int], Optional[bool]] = {}
_lock = asyncio.Lock()
def _require_kernel_datapath(hypervisor):
"""
Refuse a uBridge older than the kernel dataplane before any anchoring
probe asks it anything. Its own commands can still succeed — 1.2.3 has
the tap module, so `tap create` answers OK — while the kernel links
built on that answer need 1.3.x commands it lacks. Raised as a
UbridgeError so the caller's usual failure path reports unknown.
"""
if not hypervisor.supports_kernel_datapath:
raise UbridgeError(
f"uBridge {hypervisor.version} is too old for the kernel datapath "
f"(>= {KERNEL_DATAPATH_MINIMUM_VERSION} required)"
)
async def probe_tc_capabilities(timeout: float = 15.0): # noqa: ASYNC109 - operation-specific timeout is intentional
"""
Probe the configured uBridge's tc module once per binary identity.
@ -176,6 +192,7 @@ async def _probe_tap(path, config, timeout): # noqa: ASYNC109 - operation-speci
async def ask():
await hypervisor.start()
_require_kernel_datapath(hypervisor)
await hypervisor.connect()
await hypervisor.send(f"tap create {name}")
try:
@ -255,6 +272,7 @@ async def _probe_iol_tap(path, config, timeout): # noqa: ASYNC109 - operation-s
async def ask():
await hypervisor.start()
_require_kernel_datapath(hypervisor)
await hypervisor.connect()
await hypervisor.send(f"iol_bridge create {bridge} {_IOL_PROBE_BRIDGE_ID}")
try:
@ -336,6 +354,7 @@ async def _probe_bridge_tap(path, config, timeout): # noqa: ASYNC109 - operatio
async def ask():
await hypervisor.start()
_require_kernel_datapath(hypervisor)
await hypervisor.connect()
try:
await hypervisor.send(f"bridge delete_nio_tap gns3brtapprobe{uuid.uuid4().hex[:4]} any")
@ -432,6 +451,7 @@ async def _probe_vxlan(path, config, timeout): # noqa: ASYNC109 - operation-spe
async def ask():
await hypervisor.start()
_require_kernel_datapath(hypervisor)
await hypervisor.connect()
await hypervisor.send(f"vxlan create {name} {vni} remote=127.0.0.1 dstport={dstport}")
try:

View File

@ -122,6 +122,29 @@ async def test_prepare_tap_datapath_falls_back_without_the_tap_module(vm):
assert emit.called
@pytest.mark.asyncio
async def test_prepare_tap_datapath_falls_back_below_the_kernel_dataplane_version(vm):
"""
uBridge 1.2.3 has the tap module, so a `tap create` probe alone passes
— and the node then runs on TAP anchors whose kernel links the build
cannot drive. Below the kernel-dataplane version the node keeps the
legacy socket-netdev datapath without creating any TAP, and the warning
names the installed version.
"""
vm._ubridge_hypervisor.supports_kernel_datapath = False
vm._ubridge_hypervisor.version = "1.2.3"
with patch.object(vm.project, "emit") as emit:
await vm._prepare_tap_datapath()
assert vm._tap_datapath is False
assert vm._kernel_taps == {}
assert not any(c.args[0].startswith("tap ") for c in vm._ubridge_send.call_args_list)
message = emit.call_args.args[1]["message"]
assert "1.2.3" in message
assert "1.3.1" in message
@pytest.mark.asyncio
async def test_tap_name_fits_ifnamsiz(vm):
"""

View File

@ -194,6 +194,24 @@ async def test_accepts_ubridge_1_2_3(tmp_path, monkeypatch):
assert hyp.version == "1.2.3"
@pytest.mark.parametrize(
"version, supported",
[
("", False), # not started yet: version unknown
("1.2.3", False), # has the tap module, but no kernel dataplane
("1.3.0", False), # no per-interface capture stop_kernel
("1.3.1", True),
("1.3.2", True),
("1.4.0", True),
],
)
def test_supports_kernel_datapath(tmp_path, monkeypatch, version, supported):
hyp = _make("unix", tmp_path, monkeypatch)
hyp._version = version
assert hyp.supports_kernel_datapath is supported
# ---------------------------------------------------------------------------
# start: fail-fast on an immediately-exiting uBridge
# ---------------------------------------------------------------------------

View File

@ -50,6 +50,9 @@ class FakeHypervisor:
# doesn't exist), None = answer 100 (an impossible reply on the probe's
# uuid name — the probe must treat it as unknown)
bridge_tap_error = "214-bridge 'nosuch' doesn't exist"
# False = a build older than the kernel dataplane (uBridge < 1.3.1)
supports_kernel_datapath = True
version = "1.3.1"
spawned = 0
def __init__(self, project, path, working_dir, transport, host, node_id):
@ -90,6 +93,8 @@ def probe_env(monkeypatch, tmp_path):
FakeHypervisor.error = None
FakeHypervisor.error_prefix = None
FakeHypervisor.bridge_tap_error = "214-bridge 'nosuch' doesn't exist"
FakeHypervisor.supports_kernel_datapath = True
FakeHypervisor.version = "1.3.1"
FakeHypervisor.reply = ["netem=delay,rate;ebpf=1;cbpf=1;ebpf_modes=nth,quota,window,flow"]
tc_probe._cache.clear()
tc_probe._tap_cache.clear()
@ -285,6 +290,25 @@ async def test_probe_bridge_tap_support_missing_binary(monkeypatch):
assert FakeHypervisor.spawned == 0
@pytest.mark.parametrize(
"probe", [probe_tap_support, probe_iol_tap_support, probe_bridge_tap_support, probe_vxlan_support]
)
async def test_anchor_probes_unknown_below_the_kernel_dataplane_version(probe):
"""
uBridge 1.2.3 already has the tap module, so its `tap create` succeeds —
but the kernel links built on that answer need commands that only 1.3.x
has (per-interface `capture stop_kernel`, kernel markers, ...). A build
below the kernel-dataplane version is unknown without being asked
anything, so every node stays on the relay datapath.
"""
FakeHypervisor.supports_kernel_datapath = False
FakeHypervisor.version = "1.2.3"
assert await probe() is None
assert FakeHypervisor.spawned == 1
assert FakeHypervisor.last.commands == []
class _FixedUuid:
"""A uuid4 stand-in whose .int/.hex pin the probe's random draws."""