fix: alias long unix-socket NIO paths through the runtime dir

AF_UNIX sun_path caps at 107 bytes and node volume directories
(projects/<uuid>/project-files/docker/<uuid>/tmp) exceed it — uBridge
rejects the NIO with 'invalid file path size'. When the wiring path is
too long, create <XDG_RUNTIME_DIR>/gns3/unixio-<node-id> as a symlink
to the real volume directory (same trick as the uBridge control
socket) and reference the alias in the uBridge commands; the symlink is
removed when uBridge stops. Found during E2E with iol-xe:17-18-02.
This commit is contained in:
YueGuobin 2026-08-30 12:21:17 +08:00
parent e272ad915b
commit 42e26717e6
No known key found for this signature in database
2 changed files with 86 additions and 6 deletions

View File

@ -32,6 +32,7 @@ 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
@ -103,6 +104,7 @@ 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(",")
@ -326,6 +328,50 @@ class VendorDockerVM(DockerVM):
"""
return os.path.join(self.working_dir, os.path.relpath(self._unix_socket_dir, "/"))
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.
"""
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()
async def _add_ubridge_connection(self, nio, adapter_number):
"""
Override: with GNS3_UNIX_SOCKET_NIO, bridge the adapter through the
@ -361,8 +407,9 @@ class VendorDockerVM(DockerVM):
self._bridges.add(bridge_name)
host_dir = self._unix_socket_host_dir
local_sock = os.path.join(host_dir, f"c{adapter_number:02d}.sock")
remote_sock = os.path.join(host_dir, f"s{adapter_number:02d}.sock")
wiring_dir = 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")
# A c-socket left over from a previous ubridge run would fail its bind.
with contextlib.suppress(OSError):

View File

@ -355,16 +355,17 @@ async def test_add_ubridge_connection_unix_wiring(compute_project, manager):
sent = [c for c in vm._ubridge_hypervisor.method_calls if "send" in str(c)]
flat = "\n".join(str(c) for c in sent)
wiring_dir = vm._unix_socket_wiring_dir() # may alias host_dir when long
assert call.send("bridge create bridge0") in sent
assert call.send(f'bridge add_nio_unix bridge0 "{os.path.join(host_dir, "c00.sock")}" '
f'"{os.path.join(host_dir, "s00.sock")}"') in sent
assert call.send(f'bridge add_nio_unix bridge0 "{os.path.join(wiring_dir, "c00.sock")}" '
f'"{os.path.join(wiring_dir, "s00.sock")}"') in sent
assert "add_nio_udp bridge0 4242 127.0.0.1 4343" in flat
assert "bridge start bridge0" in flat
# the TAP/namespace path must not be used at all
assert "add_nio_tap" not in flat
assert "move_to_ns" not in flat
assert "set_mac_addr" not in flat
assert vm._ethernet_adapters[0].host_ifc == os.path.join(host_dir, "c00.sock")
assert vm._ethernet_adapters[0].host_ifc == os.path.join(wiring_dir, "c00.sock")
@pytest.mark.asyncio
@ -483,5 +484,37 @@ async def test_generic_unix_socket_dir_honored_in_wiring(compute_project, manage
await vm._add_ubridge_connection(None, 0)
flat = "\n".join(str(c) for c in vm._ubridge_hypervisor.method_calls)
assert f'"{os.path.join(host_dir, "s00.sock")}"' in flat
assert f'"{os.path.join(vm._unix_socket_wiring_dir(), "s00.sock")}"' in flat
assert "add_nio_tap" not in flat
@pytest.mark.asyncio
async def test_long_socket_path_aliased_through_runtime_dir(compute_project, manager, monkeypatch, tmp_path):
"""
Node volume paths exceed sun_path's 107 bytes; the wiring must alias them
through a short symlink so uBridge accepts the NIO (and remove the alias
when uBridge stops).
"""
from unittest.mock import PropertyMock
long_dir = str(tmp_path / "a-very-long-project-directory-name" / ("x" * 40) / "tmp")
os.makedirs(long_dir, exist_ok=True)
open(os.path.join(long_dir, "s00.sock"), "w").close()
runtime_dir = tmp_path / "run"
monkeypatch.setenv("XDG_RUNTIME_DIR", str(runtime_dir))
vm = _make_vm(compute_project, manager)
vm._ubridge_hypervisor = MagicMock()
with patch.object(type(vm), "_unix_socket_host_dir", new_callable=PropertyMock,
return_value=long_dir):
await vm._add_ubridge_connection(None, 0)
alias = os.path.join(str(runtime_dir), "gns3", f"unixio-{vm.id}")
assert os.path.islink(alias) and os.readlink(alias) == long_dir
flat = "\n".join(str(c) for c in vm._ubridge_hypervisor.method_calls)
assert f'"{os.path.join(alias, "s00.sock")}"' in flat
assert long_dir not in flat # uBridge only ever sees the short path
vm._ubridge_hypervisor.stop = AsyncioMagicMock()
await vm._stop_ubridge()
assert not os.path.lexists(alias)