From f13da59f235610ae5952c8e7e1c0f3b822eeac67 Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Fri, 2 Oct 2026 14:36:58 +0800 Subject: [PATCH] docs(ubridge): the fifth L2-only creator, and the call site that used it The spec's B table grows its fifth row: bridge add_nio_tap mints a transient TAP when the name is free and hardens it at creation, while an attach to an existing device (possibly a user-owned TAP with addresses) is left untouched. The rule paragraph spells out attach-is-not-a-creation. Delivery: uBridge fb72758, closing the gap the new e2e assertions found (live: the switch's gns3*-0 relay TAP carried fe80::). The server issues no link l2only of its own -- the switch call site now documents that the TAP is minted-and-hardened by add_nio_tap, and the ESW feature doc describes the uBridge-side close instead of a server workaround. --- docs/design/ubridge-l2-anchor-spec.md | 55 ++++++++++++++----- .../ethernet-switch-kernel-datapath.md | 9 +++ .../compute/builtin/nodes/ethernet_switch.py | 5 +- 3 files changed, 53 insertions(+), 16 deletions(-) diff --git a/docs/design/ubridge-l2-anchor-spec.md b/docs/design/ubridge-l2-anchor-spec.md index 8c6afad71..279e5ebba 100644 --- a/docs/design/ubridge-l2-anchor-spec.md +++ b/docs/design/ubridge-l2-anchor-spec.md @@ -4,10 +4,11 @@ See LICENSE file for licensing information. --> > Frozen requirements spec for the **uBridge** project. Status: **delivered** -> (uBridge `e2e3155` — `link l2only` plus the creators that apply it) and -> verified from the server side: anchors and per-link bridges report -> `addrgenmode none`, `ip -4/-6 addr` are empty, and an idle anchor is silent -> once the one-shot membership burst of §0 has settled. +> (uBridge `e2e3155` — `link l2only` plus the creators that apply it; +> `fb72758` — the fifth creator: the TAP `bridge add_nio_tap` creates for a +> free name) and verified from the server side: anchors and per-link bridges +> report `addrgenmode none`, `ip -4/-6 addr` are empty, and an idle anchor is +> silent once the one-shot membership burst of §0 has settled. > > **Residual, accepted (measured after delivery):** enslaving a port still > makes the *bridge device* announce its multicast memberships — one IGMPv3 @@ -141,10 +142,16 @@ directly, and a caller cannot forget it: | `docker create_veth ` | the **host** end only | the guest end moves into a container netns and is deliberately untouched | | `link veth ` | both ends | both ends are host-side | | `brctl create ` | the bridge device itself | the fabric the anchors are enslaved to; the bridge's own link-local floods to every port | +| `bridge add_nio_tap
` | the TAP it **creates** (name free) | by-name `TUNSETIFF` creates a transient device when the name is free — cloud's bridge interfaces, and the relay swap's create-if-missing path. Hardened at creation; an attach to an existing device is left untouched (the caller may name a user-owned TAP that has addresses). | Rule for future creators: any command that creates a host-side device for the data plane hardens it, and the acceptance checks in §E apply to it. +An **attach** is not a creation: `bridge add_nio_tap` (and every other open +of a TAP that already exists) must leave that device's addresses alone. The +transient TAP `bridge add_nio_tap` creates for a free name is a creation and +is hardened like the rest — best-effort, before the NIO is handed back. + ## C. Implementation notes (netlink, not `/proc`) - Use `RTM_SETLINK` with `IFLA_AF_SPEC{ AF_INET6, IFLA_INET6_ADDR_GEN_MODE = @@ -198,17 +205,35 @@ data plane hardens it, and the acceptance checks in §E apply to it. ## gns3-server alignment (informational — not uBridge scope) -- **Call sites unchanged**: the four creators harden the devices; the server does - not issue `link l2only`, and no NIO/JSON schema, filter or MCP tool description - changes. This is host-side hygiene only. -- **e2e assertions to add** (kernel-datapath suites, alongside the existing - capture/marker/filter checks): - - anchors and per-link bridges have no IPv4/IPv6 addresses (§E.1); - - an idle anchor captures zero frames in 5 s (§E.2); +- **Call sites**: the creators harden the devices; the server issues no + `link l2only` itself, and no NIO/JSON schema, filter or MCP tool + description changes. This is host-side hygiene only. +- **The fifth creator was found by these servers' e2e assertions**: on a + build predating `fb72758`, the Ethernet switch's relay port TAPs + (`gns3{id}-N`, created implicitly by `bridge add_nio_tap`) carried a live + `fe80::` — the §E.1 assertion failed on exactly that device, live. The + close is uBridge-side (§B table row 5): the transient TAP is hardened at + creation, attaches stay untouched. The server side needed no change for + it — the assertion alone caught it, and it now guards the fixed build. +- **e2e assertions** (landed in `tests/e2e/harness.py`, asserted by the + kernel-datapath suites alongside the existing capture/marker/filter checks): + - anchors and per-link bridges have no IPv4/IPv6 addresses, addrgenmode + `none` (`assert_pure_l2`, §E.1) — on the kernel scenarios' anchors and + bridges, and on relay-mode anchors/switch TAPs alike (the hardening is a + creation-time property, not a datapath one). The switch relay-TAP + assertion is what exercises the fifth creator end-to-end (the same + uBridge creation path cloud's transient TAPs take); + - an idle link stays silent for 5 s after the settle window + (`assert_idle_silence`, §E.2) — measured with the guest interfaces shut + (silencing IOS's own CDP/keepalives) over the anchors and the bridge; - a silent-failure guard found while validating this spec: after attach, every - bridge port reports `brport/state == 3` (forwarding). A bridge device left - DOWN keeps its ports `DISABLED` (`state == 0`) and forwards nothing — with - no error anywhere, so the assertion is the only signal. + bridge port reports `brport/state == 3` (forwarding) (`assert_forwarding`). + A bridge device left DOWN keeps its ports `DISABLED` (`state == 0`) and + forwards nothing — with no error anywhere, so the assertion is the only + signal. - **Rollout**: with a uBridge that lacks the command the anchors keep today's behaviour (noise present). The e2e assertions skip when `link l2only` answers - `202-Unknown command`, mirroring the existing tc-capability degradation. + `202-Unknown command` (`harness.l2only_supported()` probes a throwaway + uBridge once), mirroring the existing tc-capability degradation. A build + that has the command but predates `fb72758` fails the switch relay-TAP + assertion instead of skipping: the gap is exactly what it exists to catch. diff --git a/docs/features/ethernet-switch-kernel-datapath.md b/docs/features/ethernet-switch-kernel-datapath.md index 7808129ee..406088b2e 100644 --- a/docs/features/ethernet-switch-kernel-datapath.md +++ b/docs/features/ethernet-switch-kernel-datapath.md @@ -41,6 +41,15 @@ on the relay: `GNS3_UNIX_SOCKET_NIO` containers); * cross-compute links, non-Ethernet ports: the common rules. +**Relay port TAPs.** The switch's own relay-path port TAPs (the +`gns3{id}-N` devices) are created *implicitly* by uBridge's +`bridge add_nio_tap` — a name that is free is minted as a transient TAP, +and uBridge hardens it L2-only at creation (the fifth creator of +`docs/design/ubridge-l2-anchor-spec.md` §B, uBridge `fb72758`). The server +issues nothing extra: without that hardening the device would take a +kernel-default IPv6 identity whose DAD/MLD noise floods the segment, which +is exactly what the e2e's relay-control §E.1 assertion guards against. + ## The wire format: one NIO names the other end's interface ``` diff --git a/gns3server/compute/builtin/nodes/ethernet_switch.py b/gns3server/compute/builtin/nodes/ethernet_switch.py index 48188bde1..d0fd0a478 100644 --- a/gns3server/compute/builtin/nodes/ethernet_switch.py +++ b/gns3server/compute/builtin/nodes/ethernet_switch.py @@ -386,7 +386,10 @@ class EthernetSwitch(KernelDatapathMixin, BaseNode): ubridge_bridge = self._ubridge_bridge_name(port_number) tap = self._tap_name(port_number) - # per-port uBridge relay -- uBridge holds the TAP fd + # per-port uBridge relay -- uBridge holds the TAP fd. Nothing created + # this TAP before: add_nio_tap mints it (transient, a free name) and + # hardens it L2-only at creation (ubridge-l2-anchor-spec §B, the fifth + # creator — the server issues no link l2only of its own). await self._ubridge_send(f"bridge create {ubridge_bridge}") await self._ubridge_send(f'bridge add_nio_tap {ubridge_bridge} "{tap}"') # enslave the same TAP to the kernel bridge (the cloud.py::_add_linux_ethernet move)