mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-10-13 05:55:35 +03:00
test(e2e): assert the §E.2 idle-silence window in every kernel scenario
The L2-anchor spec's behavioral guarantee — an idle link stays silent for 5 s once the enslavement burst settles — only ran in the Dynamips and Docker suites. It now also runs in QEMU, IOU, the IOL container, the switch fast path and the switch cascade, which covers every host-device creation path: * tap create (Dynamips/QEMU/IOU anchors), docker create_veth (Docker host ends; the cascade's race-loser end is hardened after create by the server), bridge add_nio_tap (the IOL container anchor, the fifth creator), and the brctl switch bridges, whose multicast snooping is off precisely so the bridge role can reach silence; * each scenario silences its guests first (IOS interfaces shut through the console; Docker guests' links down) and re-verifies the ping after; * the cascade measures 8 devices at once (four absorbed router anchors, both cascade ends, both switch bridges). IOL 158 s, cascade 293 s, ESW fast path 378 s, QEMU 428 s, IOU 308 s — all green.
This commit is contained in:
parent
1f544c96a5
commit
ff41a54364
@ -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
|
||||
|
||||
@ -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/<bridge>/brif/`, the `master` link
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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`).
|
||||
|
||||
@ -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():
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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]}})
|
||||
|
||||
@ -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]}})
|
||||
|
||||
@ -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]}})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user