fix: derive per-node IOL app id so linked routers get distinct MACs

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.
This commit is contained in:
YueGuobin 2026-09-04 10:53:46 +08:00
parent 0ca9ccc637
commit b17d021bf6
No known key found for this signature in database
3 changed files with 26 additions and 8 deletions

View File

@ -137,7 +137,10 @@ diagnosing wiring issues).
`GNS3_IOL_MEMORY` (default 2048 MB). Keep container memory at IOL memory `GNS3_IOL_MEMORY` (default 2048 MB). Keep container memory at IOL memory
+ ~512 MB headroom or the OOM-killer will shoot the router. + ~512 MB headroom or the OOM-killer will shoot the router.
* **MAC addresses**: the `mac_address` template field and per-adapter custom * **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` * **Interface names are IOL-style `Ethernet0/0`**, not `GigabitEthernet0/0`
(4 ports per unit, matching the adapter-count granularity) — startup (4 ports per unit, matching the adapter-count granularity) — startup
configs addressing `GigabitEthernet…` are rejected by the parser. configs addressing `GigabitEthernet…` are rejected by the parser.

View File

@ -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), ``/tmp`` (``s%02d.sock`` receive, ``c%02d.sock`` send — raw Ethernet frames),
wired by the generic ``GNS3_UNIX_SOCKET_NIO`` capability of VendorDockerVM wired by the generic ``GNS3_UNIX_SOCKET_NIO`` capability of VendorDockerVM
(uBridge reaches them through a per-node runtime directory bound at /tmp — (uBridge reaches them through a per-node runtime directory bound at /tmp —
see ``VendorDockerVM._unix_socket_host_dir``). Because the netio bus see ``VendorDockerVM._unix_socket_host_dir``). The local application ID is
directory is private to the node, the application IDs are fixed constants derived from the node ID so that linked nodes get distinct MACs.
with no cross-node collisions.
This class is selected by the ``GNS3_IOL_RUNNER=1`` environment marker. This class is selected by the ``GNS3_IOL_RUNNER=1`` environment marker.
""" """
@ -194,8 +193,13 @@ class IOLDockerVM(VendorDockerVM):
"memory": self._iol_memory, "memory": self._iol_memory,
"num-eth": self.adapters * 4, # every adapter is a 4-port unit "num-eth": self.adapters * 4, # every adapter is a 4-port unit
"num-serial": 0, # GNS3 docker adapters are ethernet-only "num-serial": 0, # GNS3 docker adapters are ethernet-only
"local-app": 1, # IOL derives interface MACs from the local application ID
"remote-app": 2, # (aabb.cc00.0<app><iface>0); 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(), "user-id": os.getuid(),
"group-id": os.getgid(), "group-id": os.getgid(),
} }

View File

@ -261,14 +261,25 @@ async def test_start_writes_iol_config(compute_project, manager):
assert config["binary"] == "/binary.iol" assert config["binary"] == "/binary.iol"
assert config["num-eth"] == 16 # 4 adapters, each a 4-port unit assert config["num-eth"] == 16 # 4 adapters, each a 4-port unit
assert config["num-serial"] == 0 assert config["num-serial"] == 0
assert config["local-app"] == 1 assert config["local-app"] == int(vm.id.replace("-", ""), 16) % 1022 + 1
assert config["remote-app"] == 2 assert config["remote-app"] == 1023
assert config["memory"] == 2048 assert config["memory"] == 2048
assert config["user-id"] == os.getuid() assert config["user-id"] == os.getuid()
assert config["group-id"] == os.getgid() assert config["group-id"] == os.getgid()
assert vm.status == "started" 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 @pytest.mark.asyncio
async def test_start_rewrites_config_on_adapter_change(compute_project, manager): async def test_start_rewrites_config_on_adapter_change(compute_project, manager):