diff --git a/docs/design/ubridge-l2-anchor-spec.md b/docs/design/ubridge-l2-anchor-spec.md index 279e5ebba..168386d4c 100644 --- a/docs/design/ubridge-l2-anchor-spec.md +++ b/docs/design/ubridge-l2-anchor-spec.md @@ -224,8 +224,16 @@ is hardened like the rest — best-effort, before the NIO is handed back. 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; + (`assert_idle_silence`, §E.2) — measured with the guests silenced (the + IOS consoles' interfaces shut, the Docker guests' links down, all four + cascade routers shut) over the anchors, the per-link bridge and the + switch bridges, in every kernel scenario: Dynamips, Docker, QEMU, IOU, + the IOL container, the switch fast path and the switch cascade. That + covers each creation + path — `tap create`, `docker create_veth` (plus the server-side + harden-after-create on the cascade's race-loser end), `bridge + add_nio_tap` and the `brctl` switch bridges (whose multicast snooping + is off precisely so the bridge role can reach silence); - a silent-failure guard found while validating this spec: after attach, every bridge port reports `brport/state == 3` (forwarding) (`assert_forwarding`). A bridge device left DOWN keeps its ports `DISABLED` (`state == 0`) and diff --git a/docs/features/ethernet-switch-kernel-datapath.md b/docs/features/ethernet-switch-kernel-datapath.md index 410f82758..c57e406c2 100644 --- a/docs/features/ethernet-switch-kernel-datapath.md +++ b/docs/features/ethernet-switch-kernel-datapath.md @@ -152,12 +152,14 @@ The cascade has its own in-repo e2e (`tests/e2e/test_ethernet_switch_cascade.py`): two switches joined by the link-owned veth pair, two c7200 routers per switch, real ICMP crossing both bridges — the pair is born UP with exactly one end per bridge, a dot1q -trunk cascade carries two VLANs at once, the symmetric access/trunk mode -flip on the cascade ends isolates and revives a VLAN in place, a `delay` -filter lands as netem on both ends (one per direction, RTT ≈ 2×delay), -suspend admin-downs both ends, and a link delete/re-create retires the whole -pair and mints a new one under the new link id — with zero host residue -after the project goes. +trunk cascade carries two VLANs at once, the §E.2 idle-silence window with +all four router interfaces shut (the absorbed anchors, both cascade ends +and both switch bridges), the symmetric access/trunk mode flip on the +cascade ends isolates and revives a VLAN in place, a `delay` filter lands +as netem on both ends (one per direction, RTT ≈ 2×delay), suspend +admin-downs both ends, and a link delete/re-create retires the whole pair +and mints a new one under the new link id — with zero host residue after +the project goes. The host-side checks throughout are the kernel's own view: bridge membership under `/sys/class/net//brif/`, the `master` link diff --git a/docs/features/iol-docker-kernel-datapath.md b/docs/features/iol-docker-kernel-datapath.md index f80f1423c..cbbbcf415 100644 --- a/docs/features/iol-docker-kernel-datapath.md +++ b/docs/features/iol-docker-kernel-datapath.md @@ -138,6 +138,9 @@ isolated instance, real `iol-xe/iol-xe:17-18-02` images, real uBridge with * kernel link reports `kernel_datapath`, anchors exist from node start, the per-link bridge enslaves exactly the two of them; * real ICMP crosses the [unix ↔ tap] port-bridge relay (100 % ping); +* the L2-anchor spec §E.2 silence window with the guests shut — the + behavioral half of the hardening on anchors created by uBridge's bridge + TAP module (`bridge add_nio_tap`); * `delay 100` lands as netem on both anchors, measured RTT grows ≥ 150 ms and returns < 50 ms when cleared; * the classifier spot check on a TAP anchor (the Docker suite runs the full diff --git a/docs/features/iou-kernel-datapath.md b/docs/features/iou-kernel-datapath.md index 774a0b39d..8f1ad117a 100644 --- a/docs/features/iou-kernel-datapath.md +++ b/docs/features/iou-kernel-datapath.md @@ -145,8 +145,9 @@ net for the port-coordinate capture restore this work touched. The repository's live e2e suite carries the same scenario with **real IOU routers** (`tests/e2e/test_iou_kernel_datapath.py`, pytest marker `e2e`): a real L3 IOU image provides the fabric and the real IOS CLI on -the server's telnet console, driving anchors-born-with-the-node, real ICMP -through the port bridge, netem delay, the TAP-anchor classifier spot check +the server's telnet console, driving anchors-born-with-the-node, the §E.2 +idle-silence window with the guests shut, real ICMP through the port +bridge, netem delay, the TAP-anchor classifier spot check (bpf match-drop; the eBPF every-nth mode at 56 % measured round-trip loss, 55.6 % expected), suspend, capture, link delete/re-create, node stop/start rewiring with a delay filter restored on the fresh anchor diff --git a/docs/features/qemu-kernel-datapath.md b/docs/features/qemu-kernel-datapath.md index e55963a55..18a35843e 100644 --- a/docs/features/qemu-kernel-datapath.md +++ b/docs/features/qemu-kernel-datapath.md @@ -141,9 +141,10 @@ expected), no kernel bridge holding the tap. The repo suite `tests/e2e/test_qemu_kernel_datapath.py` covers the same ground with a *real* guest: two IOSv routers (a real qcow2 image, linked clone, the real IOS CLI on the telnet console) — the full kernel lifecycle -(anchors at start, per-link bridge membership, pure-L2 anchors, real ICMP, -netem delay, suspend, capture, link delete/re-create, node stop/start -rewiring, zero residue on project delete) plus the relay negative control, +(anchors at start, per-link bridge membership, pure-L2 anchors, the §E.2 +idle-silence window with the guests shut, real ICMP, netem delay, suspend, +capture, link delete/re-create, node stop/start rewiring, zero residue on +project delete) plus the relay negative control, where the anchor is up and carrying as the relay's AF_PACKET endpoint (QEMU has no other leg — the TAP is the netdev on the relay too). It needs an L3 IOSv image in the images directory's `QEMU` subfolder; a raw node must also diff --git a/tests/e2e/README.md b/tests/e2e/README.md index 30dcf0e77..3f87fc827 100644 --- a/tests/e2e/README.md +++ b/tests/e2e/README.md @@ -121,9 +121,11 @@ daemon socket the server itself uses. Every kernel scenario also asserts the L2-anchor spec's server-side guarantees (`docs/design/ubridge-l2-anchor-spec.md` §E): anchors and bridges carry no L3 identity (`assert_pure_l2`), joined ports are FORWARDING -(`assert_forwarding`), and — in the Dynamips suite — an idle link with the -guests silenced stays silent for 5 s (`assert_idle_silence`). These skip on -a uBridge without `link l2only` (probed once by `harness.l2only_supported()`). +(`assert_forwarding`), and an idle link with the guests silenced stays +silent for 5 s (`assert_idle_silence`) — the guests are the IOS consoles' +interfaces shut, the Docker guests' links down, or all four cascade routers +silenced, whichever the scenario runs. These skip on a uBridge without +`link l2only` (probed once by `harness.l2only_supported()`). Set `GNS3_E2E_DEBUG=1` to run the isolated instance with debug logging (the uBridge command stream lands in its `server.log`). diff --git a/tests/e2e/test_ethernet_switch_cascade.py b/tests/e2e/test_ethernet_switch_cascade.py index 69d3653b4..6e082d175 100644 --- a/tests/e2e/test_ethernet_switch_cascade.py +++ b/tests/e2e/test_ethernet_switch_cascade.py @@ -34,6 +34,9 @@ off each switch. Real ICMP crossing both switches proves the datapath: Asymmetric flips are deliberately not tested: an access end egresses untagged, and the far trunk end would park those frames in its native VLAN — symmetric modes are the only sane inter-switch configuration; +* the L2-anchor spec §E.2 silence window with all four router interfaces + shut: the absorbed anchors, both cascade ends and both switch bridges + stay silent for 5 s; * a ``delay 100`` filter lands as netem on BOTH veth ends (each direction impaired exactly once, like any two-sided kernel link) and the measured RTT grows by ~2 x 100 ms; @@ -121,6 +124,19 @@ def _boot(server, project_id, node_id, timeout=360): return console +def _eth_shutdown(console, shut): + """Shut/unshut both router interfaces on their switch links — the only + legitimate speakers on these segments (IOS's own CDP/keepalives) — for + §E.2's silence window; the addresses stay configured and answer once the + interfaces are back.""" + console.run("conf t") + console.run("interface f1/0") + console.run("shutdown" if shut else "no shutdown") + console.run("interface f1/1") + console.run("shutdown" if shut else "no shutdown") + console.run("end") + + def test_ethernet_switch_cascade_kernel_datapath(): server = harness.live_server(kernel=True) compute = server.compute @@ -194,6 +210,22 @@ def test_ethernet_switch_cascade_kernel_datapath(): baseline20 = harness.wait_ping(c1, IP20 % 2) assert baseline20["success"] == 100, baseline20["raw"] + # L2-anchor spec §E.2: with all four router interfaces shut, every + # host-side device of this fabric stays silent for 5 s — the four + # absorbed router anchors, both cascade ends and both switch bridges + # (the bridges are created with multicast snooping off precisely so + # the bridge role can reach silence, see uBridge's brctl create). + if harness.l2only_supported(): + for console in (c1, c2): + _eth_shutdown(console, shut=True) + harness.assert_idle_silence(a1_10, a1_20, a2_10, a2_20, e0, e1, br1, br2) + for console in (c1, c2): + _eth_shutdown(console, shut=False) + assert harness.wait_ping(c1, IP10 % 2, attempts=5)["success"] == 100 + assert harness.wait_ping(c1, IP20 % 2, attempts=5)["success"] == 100 + else: + print(".. uBridge without link l2only: skipping the §E.2 assertion") + # L2-anchor spec §E.1: the cascade ends and both bridges carry no L3 # identity, and the bridged ends are FORWARDING. if harness.l2only_supported(): diff --git a/tests/e2e/test_ethernet_switch_kernel_datapath.py b/tests/e2e/test_ethernet_switch_kernel_datapath.py index a20a92fbf..bbc0e00fa 100644 --- a/tests/e2e/test_ethernet_switch_kernel_datapath.py +++ b/tests/e2e/test_ethernet_switch_kernel_datapath.py @@ -24,6 +24,8 @@ Real ICMP crossing the switch proves the datapath: and defer the join; once the routers boot, the controller's re-push makes the switch join their anchors — the switch bridge's members become exactly the two router anchors (no relay TAPs anywhere); +* the L2-anchor spec §E.2 silence window with both routers shut: the + absorbed anchors and the switch bridge stay silent for 5 s; * VLAN programming applies to absorbed anchors: different access VLANs isolate the routers (ping dies), the same VLAN bridges them again; * a ``delay 100`` filter lands as netem on the absorbed anchor (single @@ -94,6 +96,16 @@ def _boot(server, project_id, node_id, timeout=360): return console +def _eth_shutdown(console, shut): + """Shut/unshut the router's f1/0 — the only legitimate speaker on the + segment (IOS's own CDP/keepalives) — for §E.2's silence window; the + address stays configured and answers once it is back.""" + console.run("conf t") + console.run("interface f1/0") + console.run("shutdown" if shut else "no shutdown") + console.run("end") + + def test_ethernet_switch_kernel_fast_path(): server = harness.live_server(kernel=True) compute = server.compute @@ -140,6 +152,19 @@ def test_ethernet_switch_kernel_fast_path(): baseline = harness.wait_ping(c1, "10.1.1.2") assert baseline["success"] == 100, baseline["raw"] + # L2-anchor spec §E.2: with both routers shut, the absorbed anchors + # and the switch bridge stay silent for 5 s (the bridge is created + # with multicast snooping off precisely so the bridge role can). + if harness.l2only_supported(): + for console in (c1, c2): + _eth_shutdown(console, shut=True) + harness.assert_idle_silence(a1, a2, sw_bridge) + for console in (c1, c2): + _eth_shutdown(console, shut=False) + assert harness.wait_ping(c1, "10.1.1.2", attempts=5)["success"] == 100 + else: + print(".. uBridge without link l2only: skipping the §E.2 assertion") + # L2-anchor spec §E.1: the absorbed anchors and the switch bridge # carry no L3 identity (skipped on a uBridge without `link l2only`), # and the joined ports are FORWARDING — the silent-failure guard. diff --git a/tests/e2e/test_iol_docker_kernel_datapath.py b/tests/e2e/test_iol_docker_kernel_datapath.py index 735c2f00c..addc125e6 100644 --- a/tests/e2e/test_iol_docker_kernel_datapath.py +++ b/tests/e2e/test_iol_docker_kernel_datapath.py @@ -18,6 +18,9 @@ bridge, kernel filters, suspend and capture. (``ubridge_bridge_tap``); the anchors (``gx`` TAPs) exist from node start, before any link — and the per-link bridge enslaves exactly the two of them; * real ICMP crosses the [unix ↔ TAP] port bridge relay; +* the L2-anchor spec §E.2 silence window with the guests shut — the + behavioral half of the hardening on an anchor created by uBridge's + bridge TAP module; * a ``delay 100`` filter lands as netem on both anchors, RTT grows ~200 ms; * the classifier spot check on a TAP anchor: ``bpf`` match-drop and the eBPF ``frequency_drop`` every-nth mode (the Docker suite runs the full @@ -66,6 +69,16 @@ def _boot(server, project_id, node_id, timeout=BOOT_TIMEOUT): return console +def _eth_shutdown(console, shut): + """Shut/unshut the guest's Ethernet0/0 — the only legitimate speaker on + the link segment (the IOL instance's own chatter) — for §E.2's silence + window; the address stays configured and answers once it is back.""" + console.run("conf t") + console.run("interface Ethernet0/0") + console.run("shutdown" if shut else "no shutdown") + console.run("end") + + def _topology(compute, pid, image, name): n1 = compute.create_iol_router(pid, f"{name}-1", image) n2 = compute.create_iol_router(pid, f"{name}-2", image) @@ -133,6 +146,21 @@ def test_iol_docker_kernel_datapath(): baseline = harness.wait_ping(c1, "10.1.1.2") assert baseline["success"] == 100, baseline["raw"] + # L2-anchor spec §E.2: with the guests shut the anchors and the + # per-link bridge stay silent for 5 s. These anchors come from + # uBridge's bridge TAP module (`bridge add_nio_tap`) — the creator + # whose missing hardening the spec caught live; this is the + # behavioral half of that guarantee. + if harness.l2only_supported(): + for console in (c1, c2): + _eth_shutdown(console, shut=True) + harness.assert_idle_silence(a1, a2, bridge) + for console in (c1, c2): + _eth_shutdown(console, shut=False) + assert harness.wait_ping(c1, "10.1.1.2", attempts=5)["success"] == 100 + else: + print(".. uBridge without link l2only: skipping the §E.2 assertion") + # delay 100: netem on both anchors (each impairs one direction), # one-way ~100 ms => RTT grows by ~200 ms compute.call("PUT", f"/projects/{pid}/links/{lid}", {"filters": {"delay": [100]}}) diff --git a/tests/e2e/test_iou_kernel_datapath.py b/tests/e2e/test_iou_kernel_datapath.py index aff55b417..b6d3c7d88 100644 --- a/tests/e2e/test_iou_kernel_datapath.py +++ b/tests/e2e/test_iou_kernel_datapath.py @@ -20,6 +20,7 @@ Linux bridge, kernel filters, suspend and capture. bay/unit) exist from node start, before any link — and the per-link bridge enslaves exactly the two of them; * real ICMP crosses the [IOL fabric ↔ TAP] port bridge; +* the L2-anchor spec §E.2 silence window with the guests shut; * a ``delay 100`` filter lands as netem on both anchors, RTT grows ~200 ms; * the classifier spot check on a TAP anchor: ``bpf`` match-drop and the eBPF ``frequency_drop`` every-nth mode (the Docker suite runs the full @@ -81,6 +82,16 @@ def _boot(server, project_id, node_id, timeout=BOOT_TIMEOUT): return console +def _eth_shutdown(console, shut): + """Shut/unshut the guest's Ethernet0/0 — the only legitimate speaker on + the link segment (the IOL fabric's own chatter) — for §E.2's silence + window; the address stays configured and answers once it is back.""" + console.run("conf t") + console.run("interface Ethernet0/0") + console.run("shutdown" if shut else "no shutdown") + console.run("end") + + def test_iou_kernel_datapath(): server = harness.live_server(kernel=True) compute = server.compute @@ -147,6 +158,19 @@ def test_iou_kernel_datapath(): baseline = harness.wait_ping(c1, "10.1.1.2") assert baseline["success"] == 100, baseline["raw"] + # L2-anchor spec §E.2: with the guests shut the anchors and the + # per-link bridge stay silent for 5 s (these anchors are the `tap + # create` persistent TAPs, one per Ethernet bay/unit). + if harness.l2only_supported(): + for console in (c1, c2): + _eth_shutdown(console, shut=True) + harness.assert_idle_silence(a1, a2, bridge) + for console in (c1, c2): + _eth_shutdown(console, shut=False) + assert harness.wait_ping(c1, "10.1.1.2", attempts=5)["success"] == 100 + else: + print(".. uBridge without link l2only: skipping the §E.2 assertion") + # delay 100: netem on both anchors (each impairs one direction), # one-way ~100 ms => RTT grows by ~200 ms compute.call("PUT", f"/projects/{pid}/links/{lid}", {"filters": {"delay": [100]}}) diff --git a/tests/e2e/test_qemu_kernel_datapath.py b/tests/e2e/test_qemu_kernel_datapath.py index 795cce5c6..721895c53 100644 --- a/tests/e2e/test_qemu_kernel_datapath.py +++ b/tests/e2e/test_qemu_kernel_datapath.py @@ -19,6 +19,8 @@ port bridge, the anchor is the NIC's host side. anchors (``gq`` TAPs, one per adapter) exist from node start, before any link — and the per-link bridge enslaves exactly the two of them; * real ICMP crosses the [QEMU netdev ↔ TAP] anchor; +* the L2-anchor spec §E.2 silence window with the guests shut (the anchors + here are QEMU's own netdevs); * a ``delay 100`` filter lands as netem on both anchors, RTT grows ~200 ms; * suspend admin-downs the anchor (the e1000 loses carrier) and kills the traffic; resume restores; @@ -80,6 +82,16 @@ def _boot(server, project_id, node_id, timeout=BOOT_TIMEOUT): return console +def _eth_shutdown(console, shut): + """Shut/unshut the guest's GigabitEthernet0/0 — the only legitimate + speaker on the link segment (IOSv's own chatter) — for §E.2's silence + window; the address stays configured and answers once it is back.""" + console.run("conf t") + console.run(f"interface {GUEST_ETH_IF}") + console.run("shutdown" if shut else "no shutdown") + console.run("end") + + def test_qemu_kernel_datapath(): server = harness.live_server(kernel=True) compute = server.compute @@ -144,6 +156,18 @@ def test_qemu_kernel_datapath(): baseline = harness.wait_ping(c1, "10.1.1.2") assert baseline["success"] == 100, baseline["raw"] + # L2-anchor spec §E.2: with the guests shut the anchors (QEMU's own + # netdevs) and the per-link bridge stay silent for 5 s. + if harness.l2only_supported(): + for console in (c1, c2): + _eth_shutdown(console, shut=True) + harness.assert_idle_silence(a1, a2, bridge) + for console in (c1, c2): + _eth_shutdown(console, shut=False) + assert harness.wait_ping(c1, "10.1.1.2", attempts=5)["success"] == 100 + else: + print(".. uBridge without link l2only: skipping the §E.2 assertion") + # delay 100: netem on both anchors (each impairs one direction), # one-way ~100 ms => RTT grows by ~200 ms compute.call("PUT", f"/projects/{pid}/links/{lid}", {"filters": {"delay": [100]}})