mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-03 05:51:15 +02:00
refactor: reach unix-socket NIOs through the container root in /proc
The unix-socket NIO no longer requires the socket directory to be a persisted volume: uBridge references the sockets through /proc/<container-pid>/root<dir>/..., which is always well under the 107-byte sun_path cap (a project directory path alone exceeds it) and leaves the sockets ephemeral in the container's own filesystem. This replaces the runtime-dir symlink alias and its cleanup, and the mount-time volume enforcement. IOLDockerVM now persists only /tmp/run (the IOL working directory: startup-config + NVRAM) as a nested bind instead of the whole /tmp; stale socket cleanup is gone with it, as containers are recreated on every start.
This commit is contained in:
4 files changed
+135
-252
No files matched your search
@@ -25,22 +25,20 @@ and muxes the IOS console onto PID 1 stdio (works with the plain ``telnet``
|
||||
console type; requires a TTY, which GNS3 always allocates).
|
||||
|
||||
Networking does not use the container's network namespace at all: the runner's
|
||||
netiomux exposes per-interface AF_UNIX datagram sockets in ``/tmp``
|
||||
(``s%02d.sock`` receive, ``c%02d.sock`` send — raw Ethernet frames), wired by
|
||||
the generic ``GNS3_UNIX_SOCKET_NIO`` capability of VendorDockerVM. Because the
|
||||
netio bus directory is private to the node's ``/tmp`` volume, the application
|
||||
IDs are fixed constants with no cross-node collisions.
|
||||
netiomux exposes per-interface AF_UNIX datagram sockets in the container's
|
||||
``/tmp`` (``s%02d.sock`` receive, ``c%02d.sock`` send — raw Ethernet frames),
|
||||
wired by the generic ``GNS3_UNIX_SOCKET_NIO`` capability of VendorDockerVM
|
||||
(uBridge reaches them through the container's root in /proc). Because the
|
||||
netio bus directory is private to the container's /tmp, the application IDs
|
||||
are fixed constants with no cross-node collisions.
|
||||
|
||||
This class is selected by the ``GNS3_IOL_RUNNER=1`` environment marker.
|
||||
"""
|
||||
|
||||
import glob
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from gns3server.compute.docker.docker_error import DockerHttp404Error
|
||||
from gns3server.compute.docker.vendor_docker_vm import VendorDockerVM
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -58,7 +56,7 @@ class IOLDockerVM(VendorDockerVM):
|
||||
OOM-killer will fire.
|
||||
|
||||
The marker itself forces ``GNS3_SKIP_INIT`` and the unix-socket NIO wiring,
|
||||
and auto-adds the ``/config`` and ``/tmp`` persistent volumes, so a
|
||||
and auto-adds the ``/config`` and ``/tmp/run`` persistent volumes, so a
|
||||
template containing only ``GNS3_IOL_RUNNER=1`` is fully configured.
|
||||
"""
|
||||
|
||||
@@ -90,13 +88,14 @@ class IOLDockerVM(VendorDockerVM):
|
||||
def _persistent_volume_list(self, image_info, include_network_config=True):
|
||||
"""
|
||||
Override: the runner requires ``/config`` (its config file, generated
|
||||
below) and ``/tmp`` (netiomux sockets, NETMAP, NVRAM) as persisted
|
||||
volumes — /tmp because uBridge must reach the sockets on the host.
|
||||
Auto-add both so a minimal template cannot be misconfigured.
|
||||
below) and ``/tmp/run`` (its working directory: startup-config and
|
||||
NVRAM live there — NETMAP and the netiomux sockets are ephemeral and
|
||||
stay in the container's own /tmp). Auto-add both so a minimal
|
||||
template cannot be misconfigured.
|
||||
"""
|
||||
|
||||
volumes = super()._persistent_volume_list(image_info, include_network_config)
|
||||
for needed in (self._IOL_CONFIG_DIR, "/tmp"):
|
||||
for needed in (self._IOL_CONFIG_DIR, self._IOL_RUN_DIR):
|
||||
if not any(needed == v or needed.startswith(v.rstrip("/") + "/") for v in volumes):
|
||||
volumes.append(needed)
|
||||
return volumes
|
||||
@@ -119,47 +118,28 @@ class IOLDockerVM(VendorDockerVM):
|
||||
|
||||
async def _prepare_iol_runtime(self):
|
||||
"""
|
||||
Regenerate the node's runtime files before the container starts.
|
||||
Regenerate the node's runtime files before the container starts:
|
||||
|
||||
* ``<working_dir>/tmp/run/`` must exist or the IOL process dies at
|
||||
boot (the runner writes NETMAP there but does not create it).
|
||||
* ``<working_dir>/config/iol-config.json`` is rewritten on every
|
||||
start so adapter-count and memory changes take effect.
|
||||
* Ephemeral sockets left by a previous (possibly SIGKILLed) run are
|
||||
removed — the runner rebinds them on boot and would fail on a
|
||||
stale file. ``tmp/run`` is never touched: startup-config and NVRAM
|
||||
persist there.
|
||||
"""
|
||||
|
||||
try:
|
||||
state = await self._get_container_state()
|
||||
except DockerHttp404Error:
|
||||
state = "stopped"
|
||||
The netiomux sockets and the netio bus directory need no cleanup:
|
||||
they live in the container's own /tmp, which is fresh in every
|
||||
container GNS3 creates (containers are recreated on each start).
|
||||
"""
|
||||
|
||||
os.makedirs(os.path.join(self.working_dir, "tmp", "run"), exist_ok=True)
|
||||
self._write_iol_config()
|
||||
|
||||
if state == "running":
|
||||
# Idempotent start of a live node: the sockets belong to the
|
||||
# running runner; base start() will return early.
|
||||
return
|
||||
|
||||
tmp_dir = os.path.join(self.working_dir, "tmp")
|
||||
for pattern in ("s??.sock", "c??.sock"):
|
||||
for stale in glob.glob(os.path.join(tmp_dir, pattern)):
|
||||
try:
|
||||
os.unlink(stale)
|
||||
except OSError:
|
||||
pass
|
||||
for netio_dir in glob.glob(os.path.join(tmp_dir, "netio*")):
|
||||
shutil.rmtree(netio_dir, ignore_errors=True)
|
||||
|
||||
def _write_iol_config(self):
|
||||
"""
|
||||
Write the runner's config file on the host side of the /config volume.
|
||||
The runner drops to user-id/group-id after its setup, so everything it
|
||||
creates inside the volumes is owned by the server user (which is also
|
||||
what makes the /tmp sockets reachable by uBridge).
|
||||
creates is owned by the server user — which is also what lets an
|
||||
unprivileged uBridge traverse /proc/<pid>/root to reach the
|
||||
container's sockets.
|
||||
"""
|
||||
|
||||
config = {
|
||||
|
||||
@@ -32,7 +32,6 @@ import json
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
from gns3server.utils.asyncio import wait_for_file_creation
|
||||
from gns3server.utils.asyncio.telnet_server import AsyncioTelnetServer
|
||||
@@ -70,16 +69,16 @@ class VendorDockerVM(DockerVM):
|
||||
socket files instead of a TAP interface moved into the container's
|
||||
network namespace. For images whose network agent exposes, per adapter
|
||||
``N``, a receive socket ``s%02d.sock`` and a send-to path ``c%02d.sock``
|
||||
(raw Ethernet frames, one datagram per frame) inside a persisted volume
|
||||
directory — e.g. Cisco CML's iol-runner (see IOLDockerVM). uBridge
|
||||
binds ``c{N:02d}.sock`` (its receive side) and sends to
|
||||
``s{N:02d}.sock``. No TAP is created, the container's network namespace
|
||||
is untouched and the ``mac_address`` template field is ignored (the
|
||||
image's agent owns the MAC scheme).
|
||||
(raw Ethernet frames, one datagram per frame) inside the container —
|
||||
e.g. Cisco CML's iol-runner (see IOLDockerVM). uBridge reaches the
|
||||
sockets through the container's root in ``/proc`` (see
|
||||
``_unix_socket_wiring_dir``), so no volume mount is required; it binds
|
||||
``c{N:02d}.sock`` (its receive side) and sends to ``s{N:02d}.sock``.
|
||||
No TAP is created, the container's network namespace is untouched and
|
||||
the ``mac_address`` template field is ignored (the image's agent owns
|
||||
the MAC scheme).
|
||||
* ``GNS3_UNIX_SOCKET_DIR=<dir>`` — in-container directory holding the
|
||||
socket files (default ``/tmp``). Must be a persisted volume
|
||||
(extra_volumes) so uBridge can reach the sockets on the host; node
|
||||
creation fails with an actionable error otherwise.
|
||||
socket files (default ``/tmp``).
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -104,7 +103,6 @@ class VendorDockerVM(DockerVM):
|
||||
self._stop_timeout = 60
|
||||
self._unix_socket_nio = False
|
||||
self._unix_socket_dir = "/tmp"
|
||||
self._unix_socket_aliases = set()
|
||||
if self._environment:
|
||||
for _line in self._environment.splitlines():
|
||||
_line = _line.strip().rstrip(",")
|
||||
@@ -167,17 +165,6 @@ class VendorDockerVM(DockerVM):
|
||||
are never shadowed by an empty mount.
|
||||
"""
|
||||
binds = super()._mount_binds(image_info)
|
||||
if self._unix_socket_nio:
|
||||
socket_dir = self._unix_socket_dir.rstrip("/")
|
||||
if not any(
|
||||
v.rstrip("/") == socket_dir or socket_dir.startswith(v.rstrip("/") + "/")
|
||||
for v in self._volumes
|
||||
):
|
||||
raise DockerError(
|
||||
f"GNS3_UNIX_SOCKET_NIO requires socket directory '{self._unix_socket_dir}' of "
|
||||
f"container '{self._name}' to be a persisted volume (add it to extra_volumes) "
|
||||
f"so uBridge can reach the sockets on the host"
|
||||
)
|
||||
if self._gns3_init:
|
||||
return binds
|
||||
binds = [b for b in binds if b.get("Target") != "/gns3volumes/etc/network"]
|
||||
@@ -320,57 +307,20 @@ class VendorDockerVM(DockerVM):
|
||||
return self._interface_names[adapter_number]
|
||||
return f"eth{adapter_number}"
|
||||
|
||||
@property
|
||||
def _unix_socket_host_dir(self):
|
||||
async def _unix_socket_wiring_dir(self):
|
||||
"""
|
||||
Host-side path of the in-container unix-socket directory: the bind
|
||||
source of the persisted volume it lives in.
|
||||
"""
|
||||
return os.path.join(self.working_dir, os.path.relpath(self._unix_socket_dir, "/"))
|
||||
Host-side directory to reference in the uBridge unix-NIO commands:
|
||||
the container's socket directory reached through its root in /proc.
|
||||
|
||||
def _unix_socket_wiring_dir(self):
|
||||
"""
|
||||
Directory to reference in the uBridge unix-NIO commands.
|
||||
|
||||
AF_UNIX paths are capped at 107 bytes (sun_path minus the NUL), and a
|
||||
node volume directory (projects/<uuid>/project-files/docker/<uuid>/tmp)
|
||||
alone is ~120 — uBridge would reject the NIO with "invalid file path
|
||||
size". Long directories are therefore aliased through a symlink in the
|
||||
runtime directory (same trick as the uBridge control socket). uBridge
|
||||
resolves the symlink; the socket files themselves always live in the
|
||||
persisted volume, and the container-side /tmp paths are unaffected.
|
||||
The sockets live in the container's own filesystem (no volume mount
|
||||
needed); /proc/<pid>/root/<dir> resolves to the same files uBridge
|
||||
sees, from outside any namespace. This also keeps the path well under
|
||||
the 107-byte sun_path cap — a node directory (projects/<uuid>/…)
|
||||
alone exceeds it.
|
||||
"""
|
||||
|
||||
host_dir = self._unix_socket_host_dir
|
||||
if len(host_dir) + len("/c00.sock") <= 107:
|
||||
return host_dir
|
||||
|
||||
runtime_dir = os.environ.get("XDG_RUNTIME_DIR") or tempfile.gettempdir()
|
||||
alias = os.path.join(runtime_dir, "gns3", f"unixio-{self.id}")
|
||||
try:
|
||||
os.makedirs(os.path.dirname(alias), mode=0o700, exist_ok=True)
|
||||
if os.path.lexists(alias):
|
||||
os.unlink(alias)
|
||||
os.symlink(host_dir, alias)
|
||||
except OSError as e:
|
||||
raise DockerError(
|
||||
f"Could not create unix-socket alias '{alias}' for container '{self._name}': {e}"
|
||||
)
|
||||
self._unix_socket_aliases.add(alias)
|
||||
return alias
|
||||
|
||||
async def _stop_ubridge(self):
|
||||
"""
|
||||
Override: also drop the short-path aliases created for the unix-socket
|
||||
NIO (the sockets themselves are removed by the image's agent / the
|
||||
next start's cleanup).
|
||||
"""
|
||||
|
||||
await super()._stop_ubridge()
|
||||
for alias in self._unix_socket_aliases:
|
||||
with contextlib.suppress(OSError):
|
||||
os.unlink(alias)
|
||||
self._unix_socket_aliases.clear()
|
||||
pid = await self._get_namespace()
|
||||
return f"/proc/{pid}/root{self._unix_socket_dir}"
|
||||
|
||||
async def _add_ubridge_connection(self, nio, adapter_number):
|
||||
"""
|
||||
@@ -379,15 +329,16 @@ class VendorDockerVM(DockerVM):
|
||||
a TAP interface moved into the container's network namespace.
|
||||
|
||||
Per adapter N the image's network agent is expected to create, inside
|
||||
GNS3_UNIX_SOCKET_DIR (a persisted volume, enforced by _mount_binds):
|
||||
GNS3_UNIX_SOCKET_DIR:
|
||||
|
||||
* ``s{N:02d}.sock`` — its receive socket; frames sent there are
|
||||
injected into guest interface N;
|
||||
* ``c{N:02d}.sock`` — the path it sends guest-egress frames to.
|
||||
|
||||
uBridge binds the c-socket as its receive side and sends to the
|
||||
s-socket. No TAP is allocated, the namespace is untouched and guest
|
||||
MAC addresses are whatever the image's agent uses.
|
||||
s-socket (both reached through /proc — see _unix_socket_wiring_dir).
|
||||
No TAP is allocated, the namespace is untouched and guest MAC
|
||||
addresses are whatever the image's agent uses.
|
||||
"""
|
||||
|
||||
if not self._unix_socket_nio:
|
||||
@@ -406,8 +357,7 @@ class VendorDockerVM(DockerVM):
|
||||
await self._ubridge_send(f"bridge create {bridge_name}")
|
||||
self._bridges.add(bridge_name)
|
||||
|
||||
host_dir = self._unix_socket_host_dir
|
||||
wiring_dir = self._unix_socket_wiring_dir()
|
||||
wiring_dir = await self._unix_socket_wiring_dir()
|
||||
local_sock = os.path.join(wiring_dir, f"c{adapter_number:02d}.sock")
|
||||
remote_sock = os.path.join(wiring_dir, f"s{adapter_number:02d}.sock")
|
||||
|
||||
@@ -424,7 +374,8 @@ class VendorDockerVM(DockerVM):
|
||||
f"Socket '{remote_sock}' for adapter {adapter_number} of container "
|
||||
f"'{self._name}' did not appear within 30 seconds. Check that the "
|
||||
f"container's port count covers adapter {adapter_number} and that "
|
||||
f"'{self._unix_socket_dir}' is bind-mounted from the node directory."
|
||||
f"its network agent creates the per-adapter socket pair in "
|
||||
f"'{self._unix_socket_dir}'."
|
||||
)
|
||||
|
||||
await self._ubridge_send(f'bridge add_nio_unix {bridge_name} "{local_sock}" "{remote_sock}"')
|
||||
|
||||
Reference in new issue
Block a user