From b17d021bf644bb81438719d8ed19cb253ff80a2c Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Fri, 4 Sep 2026 10:53:46 +0800 Subject: [PATCH] fix: derive per-node IOL app id so linked routers get distinct MACs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit IOL derives interface MACs from its application ID. With a constant local-app every node shared the same MACs, and linked routers dropped each other's frames as MAC loops — ARP never resolved and pings were 100% lost even though the whole uBridge datapath was forwarding. Derive local-app from the node UUID (stable across restarts, kept in 1..1022) and align remote-app with the netiomux convention CML uses (1023). Verified end to end: two iol-xe nodes, Ethernet0/0 link, ARP resolves both ways, ping 4/5 (first loss = ARP), graceful stop/start keeps the NVRAM config. --- docs/features/iol-runner-docker.md | 5 ++++- gns3server/compute/docker/iol_docker_vm.py | 14 +++++++++----- tests/compute/docker/test_iol_docker_vm.py | 15 +++++++++++++-- 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/docs/features/iol-runner-docker.md b/docs/features/iol-runner-docker.md index 64aeb3a72..0c16a1540 100644 --- a/docs/features/iol-runner-docker.md +++ b/docs/features/iol-runner-docker.md @@ -137,7 +137,10 @@ diagnosing wiring issues). `GNS3_IOL_MEMORY` (default 2048 MB). Keep container memory at IOL memory + ~512 MB headroom or the OOM-killer will shoot the router. * **MAC addresses**: the `mac_address` template field and per-adapter custom - MACs are ignored — IOL derives its own scheme (`aabb.cc00.0XY0`). + MACs are ignored — IOL derives its own scheme from the node's application + ID (`aabb.cc{app}{iface}`), e.g. `aabb.cc03.0400`. The ID is derived from + the node UUID so linked routers always get distinct MACs (nodes sharing an + ID would silently drop each other's frames as MAC loops). * **Interface names are IOL-style `Ethernet0/0`**, not `GigabitEthernet0/0` (4 ports per unit, matching the adapter-count granularity) — startup configs addressing `GigabitEthernet…` are rejected by the parser. diff --git a/gns3server/compute/docker/iol_docker_vm.py b/gns3server/compute/docker/iol_docker_vm.py index df53f2a78..3dddf87b7 100644 --- a/gns3server/compute/docker/iol_docker_vm.py +++ b/gns3server/compute/docker/iol_docker_vm.py @@ -29,9 +29,8 @@ 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 a per-node runtime directory bound at /tmp — -see ``VendorDockerVM._unix_socket_host_dir``). Because the netio bus -directory is private to the node, the application IDs are fixed constants -with no cross-node collisions. +see ``VendorDockerVM._unix_socket_host_dir``). The local application ID is +derived from the node ID so that linked nodes get distinct MACs. This class is selected by the ``GNS3_IOL_RUNNER=1`` environment marker. """ @@ -194,8 +193,13 @@ class IOLDockerVM(VendorDockerVM): "memory": self._iol_memory, "num-eth": self.adapters * 4, # every adapter is a 4-port unit "num-serial": 0, # GNS3 docker adapters are ethernet-only - "local-app": 1, - "remote-app": 2, + # IOL derives interface MACs from the local application ID + # (aabb.cc00.00); every node needs a distinct one or + # linked routers share MACs and drop each other's frames as loops. + # Derived from the node ID: stable across restarts, unique enough + # (CML allocates per-lab sequential IDs for the same reason). + "local-app": int(self.id.replace("-", ""), 16) % 1022 + 1, + "remote-app": 1023, # netiomux's fake peer application ID "user-id": os.getuid(), "group-id": os.getgid(), } diff --git a/tests/compute/docker/test_iol_docker_vm.py b/tests/compute/docker/test_iol_docker_vm.py index 4f19cd8c8..28ea48afa 100644 --- a/tests/compute/docker/test_iol_docker_vm.py +++ b/tests/compute/docker/test_iol_docker_vm.py @@ -261,14 +261,25 @@ async def test_start_writes_iol_config(compute_project, manager): assert config["binary"] == "/binary.iol" assert config["num-eth"] == 16 # 4 adapters, each a 4-port unit assert config["num-serial"] == 0 - assert config["local-app"] == 1 - assert config["remote-app"] == 2 + assert config["local-app"] == int(vm.id.replace("-", ""), 16) % 1022 + 1 + assert config["remote-app"] == 1023 assert config["memory"] == 2048 assert config["user-id"] == os.getuid() assert config["group-id"] == os.getgid() assert vm.status == "started" +def test_local_app_is_distinct_per_node(compute_project, manager): + # IOL derives interface MACs from the app ID: two nodes sharing one would + # drop each other's frames as MAC loops, so IDs must differ per node. + vm1 = _make_vm(compute_project, manager) + vm2 = _make_vm(compute_project, manager) + id1 = int(vm1.id.replace("-", ""), 16) % 1022 + 1 + id2 = int(vm2.id.replace("-", ""), 16) % 1022 + 1 + assert id1 != id2 + assert 1 <= id1 <= 1022 and 1 <= id2 <= 1022 + + @pytest.mark.asyncio async def test_start_rewrites_config_on_adapter_change(compute_project, manager):