feat(ubridge): probe iol_bridge's TAP-port support

Whether an IOU port can terminate on a persistent TAP (iol_bridge
add_nio_tap) is a capability of its own: the tap module and that command
land independently, so a build may have one without the other. Probed on
a scratch IOL bridge with a scratch TAP — one add/delete cycle, no
iol_bridge start needed (attaching a NIO works on a stopped bridge) —
cached by binary identity, with the scratch bridge id above the per-node
id space the fabric locks (513..1024) so a probe cannot collide with a
running node. Any failure, including an old build's "Unknown command",
means unknown: IOU stays on the relay datapath.

Reported through /capabilities as ubridge_iol_tap, next to ubridge_tap.
This commit is contained in:
YueGuobin committed 2026-10-06 00:21:09 +08:00
1 parent 9a14a91293
commit 200c946eef
7 files changed
+178 -4

No files matched your search

@@ -25,7 +25,7 @@ from fastapi import APIRouter, Request
from gns3server import schemas
from gns3server.compute import MODULES
from gns3server.compute.ubridge.tc_probe import probe_tc_capabilities, probe_tap_support
from gns3server.compute.ubridge.tc_probe import probe_iol_tap_support, probe_tc_capabilities, probe_tap_support
from gns3server.utils.path import get_default_project_directory
from gns3server.utils.tc_capabilities import usable_ebpf_modes
from gns3server.version import __version__
@@ -56,6 +56,10 @@ async def get_capabilities(request: Request) -> dict:
# datapath. None = unknown (probe failed or uBridge too old).
ubridge_tap = await probe_tap_support()
# IOL-port TAP termination (iol_bridge add_nio_tap): the anchor IOU's
# Ethernet ports need for the kernel datapath. None = unknown.
ubridge_iol_tap = await probe_iol_tap_support()
# record the controller hostname or IP address
if request.client:
request.app.state.controller_host = request.client.host
@@ -69,4 +73,5 @@ async def get_capabilities(request: Request) -> dict:
"node_types": node_types,
"ubridge_tc": ubridge_tc,
"ubridge_tap": ubridge_tap,
"ubridge_iol_tap": ubridge_iol_tap,
}
+91
View File
@@ -51,6 +51,8 @@ log = logging.getLogger(__name__)
_cache = {}
# binary identity -> whether the tap module works (None = unknown)
_tap_cache = {}
# binary identity -> whether iol_bridge can bind a port to a TAP (None = unknown)
_iol_tap_cache = {}
_lock = asyncio.Lock()
@@ -183,3 +185,92 @@ async def _probe_tap(path, config, timeout):
with contextlib.suppress(Exception):
await hypervisor.stop()
shutil.rmtree(working_dir, ignore_errors=True)
# Above the per-node IOL bridge ids: a real node's application id is 1..512,
# so its bridge locks /tmp/netio<uid>/513..1024. 1050 cannot collide with a
# node — only with another probe process, whose failure then answers "unknown"
# (the safe answer: IOU stays on the relay datapath).
_IOL_PROBE_BRIDGE_ID = 1050
async def probe_iol_tap_support(timeout: float = 15.0):
"""
Whether this host's uBridge can terminate an IOL port on a persistent
TAP (``iol_bridge add_nio_tap``) — the anchor IOU's Ethernet ports need
for the kernel datapath. Probed on a scratch IOL bridge with a scratch
TAP (both cleaned up), cached by binary identity like the tap probe;
any failure (missing binary, old build answering "Unknown command",
lock contention with another server process) means unknown, reported as
None and cached as such.
"""
config = Config.instance()
path = shutil.which(config.settings.Server.ubridge_path)
if not path:
return None
try:
stat = os.stat(path)
key = (path, stat.st_mtime_ns, stat.st_size)
except OSError:
return None
async with _lock:
if key in _iol_tap_cache:
return _iol_tap_cache[key]
supported = await _probe_iol_tap(path, config, timeout)
_iol_tap_cache[key] = supported
return supported
async def _probe_iol_tap(path, config, timeout):
"""
Spawn a throwaway uBridge and run one add_nio_tap/delete_nio_tap cycle on
a scratch bridge and scratch TAP — attaching works on a stopped bridge,
so no iol_bridge start is needed. The scratch resources are cleaned up
best-effort even when the probe itself fails.
"""
working_dir = tempfile.mkdtemp(prefix="gns3-iol-probe-")
hypervisor = Hypervisor(
None,
path,
working_dir,
config.settings.Server.ubridge_control_transport,
config.settings.Server.host,
str(uuid.uuid4()),
)
bridge = f"gns3iolprobe{uuid.uuid4().hex[:4]}"
tap = f"gns3ita{uuid.uuid4().hex[:6]}"
try:
async def ask():
await hypervisor.start()
await hypervisor.connect()
await hypervisor.send(f"iol_bridge create {bridge} {_IOL_PROBE_BRIDGE_ID}")
try:
await hypervisor.send(f"tap create {tap}")
await hypervisor.send(
f"iol_bridge add_nio_tap {bridge} {_IOL_PROBE_BRIDGE_ID - 1} 0 0 {tap}"
)
await hypervisor.send(f"iol_bridge delete_nio_tap {bridge} 0 0")
finally:
# iol_bridge delete releases every port NIO (the TAP fd),
# so the subsequent tap delete cannot hit EBADFD.
with contextlib.suppress(UbridgeError):
await hypervisor.send(f"iol_bridge delete {bridge}")
with contextlib.suppress(UbridgeError):
await hypervisor.send(f"tap delete {tap}")
return True
try:
return await asyncio.wait_for(ask(), timeout=timeout)
except (UbridgeError, OSError, asyncio.TimeoutError, ValueError) as e:
# Old build ("Unknown command"), missing module, or no
# CAP_NET_ADMIN: unknown.
log.debug("uBridge iol-tap probe failed: %s", e)
return None
finally:
with contextlib.suppress(Exception):
await hypervisor.stop()
shutil.rmtree(working_dir, ignore_errors=True)
+1
View File
@@ -101,6 +101,7 @@ class Compute:
"node_types": [],
"ubridge_tc": None,
"ubridge_tap": None,
"ubridge_iol_tap": None,
}
self.name = name
# Cache of interfaces on remote host
@@ -55,3 +55,9 @@ class Capabilities(BaseModel):
description="Whether uBridge can create the persistent TAPs a QEMU adapter anchors on "
"(needed to wire QEMU links on the kernel datapath); absent when the probe failed",
)
ubridge_iol_tap: Optional[bool] = Field(
None,
description="Whether uBridge can bind an IOL port to a persistent TAP (iol_bridge add_nio_tap, "
"the anchor IOU Ethernet ports need to wire IOU links on the kernel datapath); absent when the "
"probe failed",
)