diff --git a/gns3server/compute/docker/iol_docker_vm.py b/gns3server/compute/docker/iol_docker_vm.py index e6d2bf70a..5687caca7 100644 --- a/gns3server/compute/docker/iol_docker_vm.py +++ b/gns3server/compute/docker/iol_docker_vm.py @@ -489,25 +489,13 @@ class IOLDockerVM(VendorDockerVM): return self._tap_datapath = True - for adapter_number in range(0, len(self._ethernet_adapters)): - for port_number in range(0, self._ethernet_adapters[adapter_number].interfaces): - tap = self._tap_name(adapter_number, port_number) - # A persistent TAP outlives a crash: sweep any leftover of a - # previous run before recreating it (a surviving device would - # also keep its stale tc qdisc). - with contextlib.suppress(UbridgeError): - await self._ubridge_send(f'tap delete "{tap}"') - await self._ubridge_send(f'tap create "{tap}"') - # Carrier off until a link attaches (the anchor contract). - await self._ubridge_send(f'link set "{tap}" down') - self._kernel_taps[(adapter_number, port_number)] = tap - log.debug( - "IOL container '%s': anchor TAP %s created for bay %d unit %d", - self._name, - tap, - adapter_number, - port_number, - ) + # Every bay/unit anchors. No set_owner: the container port bridges + # hold these fds, and only while a kernel link is attached. + await self._create_anchor_taps( + (adapter_number, port_number) + for adapter_number in range(0, len(self._ethernet_adapters)) + for port_number in range(0, self._ethernet_adapters[adapter_number].interfaces) + ) async def _ensure_anchor(self, adapter_number, port_number): """ @@ -650,11 +638,7 @@ class IOLDockerVM(VendorDockerVM): for port_number in range(0, adapter.interfaces): with contextlib.suppress(UbridgeError): await self._ubridge_send(f"bridge delete {self._bridge_name(adapter_number, port_number)}") - for tap in self._kernel_taps.values(): - with contextlib.suppress(UbridgeError): - await self._ubridge_send(f'tap delete "{tap}"') - await self._remove_kernel_bridges() - self._kernel_taps.clear() + await self._delete_anchor_taps() self._tap_datapath = False self._ubridge_tc_caps = None await super()._stop_ubridge() diff --git a/gns3server/compute/dynamips/nodes/router.py b/gns3server/compute/dynamips/nodes/router.py index de0063d43..439d9c10f 100644 --- a/gns3server/compute/dynamips/nodes/router.py +++ b/gns3server/compute/dynamips/nodes/router.py @@ -42,7 +42,6 @@ from ...base_node import BaseNode from ...error import NodeError from ...kernel_datapath import KernelDatapathMixin from ...nios.nio_bridge import NIOBridge -from ...ubridge.ubridge_error import UbridgeError from ..adapters.adapter import ETHERNET_ADAPTERS, ETHERNET_WICS from ..dynamips_error import DynamipsError from ..nios.nio_tap import NIOTAP @@ -1648,19 +1647,15 @@ class Router(KernelDatapathMixin, BaseNode): # uBridge (re)started: the tc capabilities must be probed again. self._ubridge_tc_caps = None if not self._tap_datapath: - probe = f"gd{self._id.replace('-', '')[:8]}prob" - try: - await self._ubridge_send(f'tap create "{probe}"') - except UbridgeError as e: + probe_error = await self._probe_persistent_taps(f"gd{self._id.replace('-', '')[:8]}prob") + if probe_error is not None: message = ( - f"Router '{self._name}': uBridge cannot create persistent TAPs ({e}); " + f"Router '{self._name}': uBridge cannot create persistent TAPs ({probe_error}); " "this node runs on the relay datapath and cannot carry kernel links" ) log.warning(message) self.project.emit("log.warning", {"message": message}) return - with contextlib.suppress(UbridgeError): - await self._ubridge_send(f'tap delete "{probe}"') self._tap_datapath = True await self._create_taps() @@ -1675,31 +1670,15 @@ class Router(KernelDatapathMixin, BaseNode): async def _create_taps(self): """ - Create the persistent TAP every Ethernet slot/port owns. Each TAP - starts DOWN (carrier off until a kernel link attaches); uBridge - creates the device and hands ownership to this user, so the - unprivileged Dynamips hypervisor can open it. A port whose anchor - already exists (a restart, or an adapter hot-added to a running - router) keeps it — an anchor is never recreated under a live link. - A persistent TAP outlives its creator, so a leftover from a previous - run (crash, kill) is swept first. + Create the persistent TAP every Ethernet slot/port owns (WIC ports + included), handed to this user so the unprivileged Dynamips + hypervisor can open the device. Unlike QEMU's, these anchors outlive + a node stop — the hypervisor keeps its NIO bindings — so a port that + already holds one keeps it: an anchor is never recreated under a + live link (a restart, or an adapter hot-added to a running router). """ - for slot_number, port_number in self._ethernet_slot_ports(): - if (slot_number, port_number) in self._kernel_taps: - continue - tap = self._tap_name(slot_number, port_number) - with contextlib.suppress(UbridgeError): - await self._ubridge_send(f'tap delete "{tap}"') - await self._ubridge_send(f'tap create "{tap}"') - try: - await self._ubridge_send(f"tap set_owner {tap} {os.getuid()}") - except UbridgeError as e: - # Only root could open the TAP now: the hypervisor would fail - # to bind its NIO, which is worth surfacing at that point. - log.warning("Router '%s': could not hand TAP %s to uid %s: %s", self._name, tap, os.getuid(), e) - await self._ubridge_send(f'link set "{tap}" down') - self._kernel_taps[(slot_number, port_number)] = tap + await self._create_anchor_taps(self._ethernet_slot_ports(), set_owner=True, keep_existing=True) async def _attach_kernel_nio(self, slot_number, port_number, nio): """ @@ -1792,11 +1771,7 @@ class Router(KernelDatapathMixin, BaseNode): with contextlib.suppress(DynamipsError, OSError): await tap_nio.delete() self._tap_nios.clear() - for tap in self._kernel_taps.values(): - with contextlib.suppress(UbridgeError): - await self._ubridge_send(f'tap delete "{tap}"') - await self._remove_kernel_bridges() - self._kernel_taps.clear() + await self._delete_anchor_taps() self._ubridge_tc_caps = None await super()._stop_ubridge() diff --git a/gns3server/compute/iou/iou_vm.py b/gns3server/compute/iou/iou_vm.py index aad00f0bc..f11cac8af 100644 --- a/gns3server/compute/iou/iou_vm.py +++ b/gns3server/compute/iou/iou_vm.py @@ -929,27 +929,15 @@ class IOUVM(KernelDatapathMixin, BaseNode): return self._tap_datapath = True - await self._create_taps() - - async def _create_taps(self): - """ - Create the persistent TAP every Ethernet bay/unit owns (four units - per bay, the IOU adapter model). uBridge holds a device's fd only - while a kernel link binds the port to it (``iol_bridge - add_nio_tap``); until then the TAP sits DOWN with no holder, and it - survives link churn untouched — links attach to it, never recreate - it. A persistent TAP outlives its creator, so a leftover from a - previous run (crash, kill) is swept first. - """ - - for adapter_number, adapter in enumerate(self._ethernet_adapters): - for port_number in adapter.ports.keys(): - tap = self._tap_name(adapter_number, port_number) - with contextlib.suppress(UbridgeError): - await self._ubridge_send(f'tap delete "{tap}"') - await self._ubridge_send(f'tap create "{tap}"') - await self._ubridge_send(f'link set "{tap}" down') - self._kernel_taps[(adapter_number, port_number)] = tap + # Every Ethernet bay/unit anchors (four units per bay, the IOU + # adapter model; serial bays never do). No set_owner: uBridge holds + # these fds itself, and only while a kernel link binds the port — + # until then, and after, the TAP survives link churn untouched. + await self._create_anchor_taps( + (adapter_number, port_number) + for adapter_number, adapter in enumerate(self._ethernet_adapters) + for port_number in adapter.ports.keys() + ) async def _attach_kernel_nio(self, adapter_number, port_number, nio): """ @@ -988,11 +976,7 @@ class IOUVM(KernelDatapathMixin, BaseNode): if self.ubridge: with contextlib.suppress(UbridgeError): await self._ubridge_send(f"iol_bridge delete {self._iol_bridge_name()}") - for tap in self._kernel_taps.values(): - with contextlib.suppress(UbridgeError): - await self._ubridge_send(f'tap delete "{tap}"') - await self._remove_kernel_bridges() - self._kernel_taps.clear() + await self._delete_anchor_taps() self._ubridge_tc_caps = None await super()._stop_ubridge() diff --git a/gns3server/compute/kernel_datapath.py b/gns3server/compute/kernel_datapath.py index fd5fced43..967d813bc 100644 --- a/gns3server/compute/kernel_datapath.py +++ b/gns3server/compute/kernel_datapath.py @@ -34,6 +34,9 @@ A node type using this mixin provides: or None when the adapter has none (a vendor container without a veth); * ``_kernel_anchors()`` — every anchor it currently owns, used to recognise the polymorphic marker anchor; +* ``_tap_name(adapter_number, port_number)`` — the deterministic anchor TAP + name of a port (the shared utils.kernel_anchor contract), for the node + types whose anchor is a persistent TAP; * optionally ``_relay_carrier_bridge(adapter_number, port_number)`` — the uBridge bridge carrying the adapter's TAP relay, for node types whose relay datapath still rides a uBridge-owned TAP instead of an anchor; @@ -91,6 +94,15 @@ class KernelDatapathMixin: return None + def _tap_name(self, adapter_number, port_number=0): + """ + Returns the deterministic anchor TAP name of a port (the shared + utils.kernel_anchor naming contract — the controller names a peer's + anchor with the same function when an Ethernet switch absorbs it). + """ + + raise NotImplementedError + def _kernel_error(self, message): """ Returns the node module's error for a kernel-datapath failure @@ -130,6 +142,86 @@ class KernelDatapathMixin: state = "on" if connected else "off" await self._ubridge_send(f"bridge set_nio_tap_carrier {bridge_name} {state}") + # ------------------------------------------------------------------ + # Anchor TAP lifecycle + # ------------------------------------------------------------------ + + async def _probe_persistent_taps(self, probe_name): + """ + Probe the node's uBridge for the persistent-TAP module with a + scratch TAP that is deleted again. Returns None when the module + works, or the UbridgeError that refused it — an old build answers + "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. + """ + + try: + await self._ubridge_send(f'tap create "{probe_name}"') + except UbridgeError as e: + return e + with contextlib.suppress(UbridgeError): + await self._ubridge_send(f'tap delete "{probe_name}"') + return None + + async def _create_anchor_taps(self, ports, set_owner=False, keep_existing=False): + """ + Create the persistent anchor TAP of every port, named through + ``_tap_name`` and registered in ``_kernel_taps``. + + *ports* is an iterable of ``(adapter_number, port_number)`` keys — a + node type enumerates whichever of its ports anchor (QEMU's whole + adapters, IOU's bay/units, Dynamips' Ethernet slot ports, the IOL + container's bay/units). A persistent TAP outlives its creator, so a + leftover of a previous run (crash, kill) is swept first; each TAP is + created and starts DOWN (carrier off until a link attaches). + ``set_owner`` hands the device to this user for the node types whose + unprivileged emulator opens it itself (QEMU, Dynamips); + ``keep_existing`` leaves ports that already hold an anchor alone — + for a node whose anchors outlive its stop (Dynamips) or an adapter + hot-added to a running node, where an anchor must never be recreated + under a live link. + """ + + for key in ports: + if keep_existing and key in self._kernel_taps: + continue + tap = self._tap_name(*key) + with contextlib.suppress(UbridgeError): + await self._ubridge_send(f'tap delete "{tap}"') + await self._ubridge_send(f'tap create "{tap}"') + if set_owner: + try: + await self._ubridge_send(f"tap set_owner {tap} {os.getuid()}") + except UbridgeError as e: + # Only root could open the TAP then: the emulator would + # fail to attach its netdev, which is worth surfacing + # here rather than as a launch failure. + log.warning( + "Node '%s' [%s]: could not hand TAP %s to uid %s: %s", self._name, self._id, tap, os.getuid(), e + ) + await self._ubridge_send(f'link set "{tap}" down') + self._kernel_taps[key] = tap + log.debug("Node '%s' [%s]: anchor TAP %s created for port %s", self._name, self._id, tap, key) + + async def _delete_anchor_taps(self): + """ + Delete every registered anchor TAP and the per-link kernel bridges + this node still holds, then drop the registry. The caller releases + whatever holds each TAP's fd first (the QEMU process, the IOL + bridge, the hypervisor's NIO bindings, the container port bridges): + a held device answers EBADFD and the best-effort delete below would + leak it. The sweep is suppressed per TAP — an anchor already gone + is not an error — and the registry is cleared either way. + """ + + if self.ubridge: + for tap in self._kernel_taps.values(): + with contextlib.suppress(UbridgeError): + await self._ubridge_send(f'tap delete "{tap}"') + await self._remove_kernel_bridges() + self._kernel_taps.clear() + # ------------------------------------------------------------------ # Link attach / detach # ------------------------------------------------------------------ diff --git a/gns3server/compute/qemu/qemu_vm.py b/gns3server/compute/qemu/qemu_vm.py index 37485fc53..dac2c219f 100644 --- a/gns3server/compute/qemu/qemu_vm.py +++ b/gns3server/compute/qemu/qemu_vm.py @@ -20,7 +20,6 @@ order to run a QEMU VM. """ import asyncio -import contextlib import json import logging import math @@ -54,7 +53,6 @@ from ..kernel_datapath import KernelDatapathMixin from ..nios.nio_bridge import NIOBridge from ..nios.nio_tap import NIOTAP from ..nios.nio_udp import NIOUDP -from ..ubridge.ubridge_error import UbridgeError from .qemu_error import QemuError from .utils.qcow2 import Qcow2, Qcow2Error from .utils.ziputils import pack_zip, unpack_zip @@ -1565,65 +1563,26 @@ class QemuVM(KernelDatapathMixin, BaseNode): if not self.ubridge: return - probe = f"gq{self._id.replace('-', '')[:8]}prob" - try: - await self._ubridge_send(f'tap create "{probe}"') - except UbridgeError as e: - log.warning("QEMU VM '%s': uBridge cannot create persistent TAPs (%s)", self._name, e) + probe_error = await self._probe_persistent_taps(f"gq{self._id.replace('-', '')[:8]}prob") + if probe_error is not None: + log.warning("QEMU VM '%s': uBridge cannot create persistent TAPs (%s)", self._name, probe_error) self.project.emit( "log.warning", { - "message": f"QEMU VM '{self._name}': uBridge does not support persistent TAPs ({e}); " + "message": f"QEMU VM '{self._name}': uBridge does not support persistent TAPs ({probe_error}); " "this node runs on the legacy relay datapath and cannot carry kernel links" }, ) return - with contextlib.suppress(UbridgeError): - await self._ubridge_send(f'tap delete "{probe}"') self._tap_datapath = True - await self._create_taps() - - async def _create_taps(self): - """ - Create the persistent TAP every adapter owns. Each adapter is born as - a TAP whatever its NIO type — the anchor role Docker's veth host end - plays — so a link attaches to (or detaches from) a running VM without - touching its interfaces. uBridge creates the device and hands - ownership to this user, so the unprivileged QEMU process can open it; - the TAP starts DOWN (carrier off until a link attaches). A persistent - TAP outlives its creator, so a leftover from a previous run (crash, - kill) is swept first. - """ - - for adapter_number, adapter in enumerate(self._ethernet_adapters): - tap = self._tap_name(adapter_number) - with contextlib.suppress(UbridgeError): - await self._ubridge_send(f'tap delete "{tap}"') - await self._ubridge_send(f'tap create "{tap}"') - try: - await self._ubridge_send(f"tap set_owner {tap} {os.getuid()}") - except UbridgeError as e: - # Only root could open the TAP now: the VM would fail to - # attach its netdev, which is worth surfacing here rather - # than as a launch failure. - log.warning("QEMU VM '%s': could not hand TAP %s to uid %s: %s", self._name, tap, os.getuid(), e) - await self._ubridge_send(f'link set "{tap}" down') - self._kernel_taps[(adapter_number, 0)] = tap - - async def _remove_taps(self): - """ - Delete the adapter TAPs together with the per-link kernel bridges - this VM still holds: a stopped VM has no links attached, so those - bridges are orphans whose ports just disappeared. - """ - - if self.ubridge: - for tap in self._kernel_taps.values(): - with contextlib.suppress(UbridgeError): - await self._ubridge_send(f'tap delete "{tap}"') - await self._remove_kernel_bridges() - self._kernel_taps.clear() + # Every adapter anchors (port 0 only — QEMU adapters are + # single-port). The shared lifecycle sweeps leftovers first, creates + # each TAP and hands it to this user (unprivileged QEMU opens it + # itself), leaving it down until a link attaches. + await self._create_anchor_taps( + ((adapter_number, 0) for adapter_number, _adapter in enumerate(self._ethernet_adapters)), set_owner=True + ) async def _stop_ubridge(self): """ @@ -1635,8 +1594,7 @@ class QemuVM(KernelDatapathMixin, BaseNode): probed again. """ - if self.ubridge: - await self._remove_taps() + await self._delete_anchor_taps() self._ubridge_tc_caps = None await super()._stop_ubridge()