From 27b55b72f9dbd3117d5593df6db540f3a835f1d6 Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Sat, 5 Sep 2026 22:19:22 +0800 Subject: [PATCH] fix: keep upstream link-carrier commands off unix-socket NIO bridges Upstream #2870 added TAP carrier control (set_nio_tap_carrier) and a busybox interface-status monitor to the Docker base class. Both assume TAP wiring: unix-socket NIO bridges carry no TAP NIO, so uBridge rejects the carrier command ('bridge has no TAP NIO') and every link create, update or delete on a running iol-runner node would fail; the monitor polls eth{adapter} interfaces that do not exist in the container's network namespace (or misreports the Docker default eth0 as adapter 0). VendorDockerVM now short-circuits _set_adapter_carrier and the interface monitor under GNS3_UNIX_SOCKET_NIO; TAP-wired vendor nodes keep the base behavior. --- gns3server/compute/docker/vendor_docker_vm.py | 32 ++++++++++ tests/compute/docker/test_iol_docker_vm.py | 61 +++++++++++++++++++ 2 files changed, 93 insertions(+) diff --git a/gns3server/compute/docker/vendor_docker_vm.py b/gns3server/compute/docker/vendor_docker_vm.py index d6157eea4..b156209ae 100644 --- a/gns3server/compute/docker/vendor_docker_vm.py +++ b/gns3server/compute/docker/vendor_docker_vm.py @@ -454,6 +454,38 @@ class VendorDockerVM(DockerVM): if nio: await self._connect_nio(adapter_number, nio, port_number) + async def _set_adapter_carrier(self, adapter_number, connected, port_number=0): + """ + Override: with GNS3_UNIX_SOCKET_NIO the bridges carry only unix and + UDP NIOs — there is no TAP, and uBridge rejects the carrier command + ("bridge has no TAP NIO"), which would fail every link create, + update or delete on a running node. The link state is the socket + pair itself. + """ + + if not self._unix_socket_nio: + await super()._set_adapter_carrier(adapter_number, connected, port_number) + + async def _start_interface_monitor(self): + """ + Override: with GNS3_UNIX_SOCKET_NIO no GNS3-managed eth interface + exists in the container's network namespace — the busybox poll would + idle forever (or misreport the Docker default eth0 as adapter 0 + status). Vendor images with TAP wiring keep the base monitor. + """ + + if not self._unix_socket_nio: + await super()._start_interface_monitor() + + async def _stop_interface_monitor(self): + """ + Override: mirror _start_interface_monitor — there is nothing to stop + when the monitor never started under unix-socket NIO. + """ + + if not self._unix_socket_nio: + await super()._stop_interface_monitor() + def _cleanup_console_resources(self): """ Override: close the docker-exec pty socket, if any, so the next diff --git a/tests/compute/docker/test_iol_docker_vm.py b/tests/compute/docker/test_iol_docker_vm.py index 2bfc4f4ec..4826b00a7 100644 --- a/tests/compute/docker/test_iol_docker_vm.py +++ b/tests/compute/docker/test_iol_docker_vm.py @@ -610,6 +610,67 @@ async def test_generic_unix_socket_dir_honored_in_wiring(compute_project, manage assert "add_nio_tap" not in flat +# --------------------------------------------------------------------------- +# Upstream link-carrier / interface-monitor vs unix-socket NIO +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +async def test_link_operations_never_send_tap_carrier(compute_project, manager): + """ + uBridge rejects set_nio_tap_carrier on a bridge without a TAP NIO + ("bridge has no TAP NIO"), and unix-socket NIO bridges never have one: + every link create/update/delete on a running node would fail. The + vendor override must keep the carrier command out of all three paths. + """ + + vm = _make_vm(compute_project, manager) + vm._ubridge_hypervisor = MagicMock() # truthy ubridge, like a started node + vm._ubridge_send = AsyncioMagicMock() + vm.status = "started" + nio = manager.create_nio({"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"}) + + await vm.adapter_add_nio_binding(0, nio, 0) + await vm.adapter_update_nio_binding(0, nio, 0) + await vm.adapter_remove_nio_binding(0, 0) + + commands = [str(c.args[0]) for c in vm._ubridge_send.call_args_list] + assert any(c.startswith("bridge add_nio_udp") for c in commands) + assert not any("set_nio_tap_carrier" in c for c in commands) + + +@pytest.mark.asyncio +async def test_interface_monitor_disabled_under_unix_nio(compute_project, manager): + + vm = _make_vm(compute_project, manager) + vm.manager.query = AsyncioMagicMock() + + await vm._start_interface_monitor() # must be a no-op: no eth NICs to poll + await vm._stop_interface_monitor() + + vm.manager.query.assert_not_called() + assert vm._interface_monitor_task is None + + +@pytest.mark.asyncio +async def test_tap_wired_vendor_keeps_carrier_and_monitor(compute_project, manager): + """ + Vendor nodes with TAP wiring (XRd, SR Linux, ...) keep the base + behavior: the guards only apply to unix-socket NIO. + """ + + vm = VendorDockerVM("vendor-1", str(uuid.uuid4()), compute_project, manager, "vendor:latest", + console_type="docker_exec", environment="GNS3_SKIP_INIT=1") + with patch.object(DockerVM, "_set_adapter_carrier", new=AsyncioMagicMock()) as carrier, \ + patch.object(DockerVM, "_start_interface_monitor", new=AsyncioMagicMock()) as monitor: + await vm._set_adapter_carrier(0, True) + await vm._start_interface_monitor() + # the base-class attribute is replaced by a plain mock (no descriptor + # binding), so super() calls arrive without self + carrier.assert_called_once_with(0, True, 0) + monitor.assert_called_once_with() + + # --------------------------------------------------------------------------- # Startup configuration # ---------------------------------------------------------------------------