diff --git a/gns3server/compute/docker/vendor_docker_vm.py b/gns3server/compute/docker/vendor_docker_vm.py index 8b8f5fb52..4eba1a705 100644 --- a/gns3server/compute/docker/vendor_docker_vm.py +++ b/gns3server/compute/docker/vendor_docker_vm.py @@ -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//project-files/docker//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): diff --git a/tests/compute/docker/test_iol_docker_vm.py b/tests/compute/docker/test_iol_docker_vm.py index 8d12307ef..5986e4dd0 100644 --- a/tests/compute/docker/test_iol_docker_vm.py +++ b/tests/compute/docker/test_iol_docker_vm.py @@ -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)