mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-10-13 05:55:35 +03:00
test(e2e): classifier spot check and filter persistence on TAP anchors
The filter matrix (cls_bpf, the eBPF classifier) only ran on Docker veth host ends; the IOU and IOL-container scenarios now spot-check it on their TAP anchors and prove a filter survives a node restart: * bpf "icmp" drops everything with clsact on the anchor, both ends, and clears back to an empty qdisc; * frequency_drop 3 (the eBPF every-nth mode) measured 56 % round-trip loss (1 - (2/3)^2 = 55.6 % expected) in both scenarios; * the IOU test applies delay 100 before a router stop/start: the fresh anchor gets netem back (tc level) and the restarted router's pings measure RTT >= 150 ms until the filter is cleared (a re-attach that dropped the filter would show ~100 ms). IOU file 492 s, IOL file 182 s, both green.
This commit is contained in:
parent
9d937802d5
commit
1f544c96a5
@ -140,6 +140,10 @@ isolated instance, real `iol-xe/iol-xe:17-18-02` images, real uBridge with
|
||||
* real ICMP crosses the [unix ↔ tap] port-bridge relay (100 % ping);
|
||||
* `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
|
||||
matrix on veth host ends): `bpf "icmp"` drops everything (clsact on the
|
||||
anchor), `frequency_drop 3` as the eBPF every-nth mode measured 56 %
|
||||
round-trip loss (theory 55.6 %), both restoring on clear;
|
||||
* suspend admin-downs the anchor and kills the traffic; resume restores;
|
||||
* AF_PACKET capture writes a real pcap of the ICMP exchange;
|
||||
* link delete/re-create swaps the port bridge's TAP leg out and back in
|
||||
|
||||
@ -146,10 +146,13 @@ 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, suspend, capture, link
|
||||
delete/re-create, node stop/start rewiring, the serial link staying on the
|
||||
relay, and a relay negative control whose real `delay`/`frequency_drop`
|
||||
filters ride the port's `iol_bridge add_packet_filter` list.
|
||||
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
|
||||
(tc and traffic both), the serial link staying on the relay, and a relay
|
||||
negative control whose real `delay`/`frequency_drop` filters ride the
|
||||
port's `iol_bridge add_packet_filter` list.
|
||||
|
||||
The uBridge side passed its own 30/30 (`tests/iol/test_tap_anchor.py` in the
|
||||
fork), including the DOWN-anchor resilience and kernel-bridge interop this
|
||||
|
||||
@ -72,16 +72,18 @@ Current scenarios:
|
||||
names), zero residue.
|
||||
* `test_iol_docker_kernel_datapath.py` — two real iol-xe containers (Cisco
|
||||
CML iol-runner images): kernel link through the port bridge's swappable
|
||||
TAP leg (anchors at start, netem delay, suspend, capture, link
|
||||
delete/re-create swap, node stop/start rewiring) and a relay negative
|
||||
control.
|
||||
TAP leg (anchors at start, netem delay, the TAP-anchor classifier spot
|
||||
check for bpf match-drop and the eBPF every-nth mode, suspend, capture,
|
||||
link delete/re-create swap, node stop/start rewiring) and a relay
|
||||
negative control.
|
||||
* `test_iou_kernel_datapath.py` — two real IOU routers (a real IOU image
|
||||
from the images directory, the real IOS CLI on the server's telnet
|
||||
console): kernel link through the [IOL fabric ↔ TAP] port bridge (same
|
||||
lifecycle as the iol-xe containers), a serial link staying on the relay
|
||||
(serial bays never anchor), and a relay negative control whose filters
|
||||
ride the IOU-specific `iol_bridge add_packet_filter` engine (delay and
|
||||
frequency_drop measured through real traffic).
|
||||
lifecycle as the iol-xe containers) including the TAP-anchor classifier
|
||||
spot check and a filter surviving a node restart, a serial link staying
|
||||
on the relay (serial bays never anchor), and a relay negative control
|
||||
whose filters ride the IOU-specific `iol_bridge add_packet_filter` engine
|
||||
(delay and frequency_drop measured through real traffic).
|
||||
* `test_qemu_kernel_datapath.py` — two real IOSv routers (a real qcow2
|
||||
image from the images directory's `QEMU` subfolder, linked clone, the
|
||||
real IOS CLI on the server's telnet console): the anchor TAP is QEMU's
|
||||
|
||||
@ -19,6 +19,9 @@ bridge, kernel filters, suspend and capture.
|
||||
before any link — and the per-link bridge enslaves exactly the two of them;
|
||||
* real ICMP crosses the [unix ↔ TAP] port bridge relay;
|
||||
* 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
|
||||
matrix on veth host ends);
|
||||
* suspend admin-downs the anchor and kills the traffic; resume restores;
|
||||
* link delete releases the port bridge's TAP leg (stop → delete_nio_tap) and
|
||||
the anchor survives — re-creating the link swaps the leg back in (the
|
||||
@ -145,6 +148,40 @@ def test_iol_docker_kernel_datapath():
|
||||
fast = harness.wait_ping(c1, "10.1.1.2")
|
||||
assert fast["success"] == 100 and fast["avg"] < 50, fast
|
||||
|
||||
# Classifier spot check on the container's TAP anchor (the Docker
|
||||
# suite runs the full matrix on veth host ends): cls_bpf match-drop
|
||||
# and the eBPF stateful classifier attach to a tun/tap anchor the
|
||||
# same way.
|
||||
tc_caps = (caps or {}).get("ubridge_tc") or {}
|
||||
if tc_caps.get("cbpf"):
|
||||
print(".. bpf 'icmp' drops everything on the TAP anchor")
|
||||
compute.call("PUT", f"/projects/{pid}/links/{lid}", {"filters": {"bpf": ["icmp"]}})
|
||||
assert "clsact" in harness.qdiscs(a1) and "clsact" in harness.qdiscs(a2), (
|
||||
harness.qdiscs(a1),
|
||||
harness.qdiscs(a2),
|
||||
)
|
||||
blocked = harness.ping(c1, "10.1.1.2", repeat=4)
|
||||
assert blocked["success"] == 0, blocked["raw"]
|
||||
compute.call("PUT", f"/projects/{pid}/links/{lid}", {"filters": {}})
|
||||
assert "clsact" not in harness.qdiscs(a1), harness.qdiscs(a1)
|
||||
assert harness.wait_ping(c1, "10.1.1.2")["success"] == 100
|
||||
else:
|
||||
print(".. uBridge reports no cbpf: skipping the bpf check")
|
||||
|
||||
modes = tc_caps.get("ebpf_modes") or []
|
||||
if "nth" in modes:
|
||||
print(".. frequency_drop 3 (eBPF nth) on the TAP anchor")
|
||||
compute.call("PUT", f"/projects/{pid}/links/{lid}", {"filters": {"frequency_drop": [3]}})
|
||||
nth = harness.ping(c1, "10.1.1.2", repeat=9)
|
||||
loss = 100 - nth["success"]
|
||||
print(f".. {loss} % round-trip loss")
|
||||
# the kernel counts per direction: 1 - (2/3)^2 = 55.6 % round trip
|
||||
assert 20 <= loss <= 90, nth["raw"]
|
||||
compute.call("PUT", f"/projects/{pid}/links/{lid}", {"filters": {}})
|
||||
assert harness.wait_ping(c1, "10.1.1.2")["success"] == 100
|
||||
else:
|
||||
print(".. uBridge reports no eBPF nth: skipping the frequency_drop check")
|
||||
|
||||
# suspend: anchor admin-down (the port bridge's TAP writes fail EIO,
|
||||
# nothing comes back); resume restores
|
||||
compute.call("PUT", f"/projects/{pid}/links/{lid}", {"suspend": True})
|
||||
|
||||
@ -21,6 +21,9 @@ Linux bridge, kernel filters, suspend and capture.
|
||||
bridge enslaves exactly the two of them;
|
||||
* real ICMP crosses the [IOL fabric ↔ TAP] port bridge;
|
||||
* 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
|
||||
matrix on veth host ends);
|
||||
* suspend admin-downs the anchor and kills the traffic; resume restores;
|
||||
* a serial link between the same nodes never anchors (per-port exclusion):
|
||||
it stays on the relay, ``kernel_datapath`` is False, and real traffic
|
||||
@ -29,7 +32,7 @@ Linux bridge, kernel filters, suspend and capture.
|
||||
re-creating the link swaps the leg back in and the traffic returns;
|
||||
* node stop tears the anchors down (uBridge holds their fds — the IOL
|
||||
bridge goes first, then ``tap delete``); a restart recreates the whole
|
||||
wiring from the NIO;
|
||||
wiring from the NIO, a delay filter applied before the stop included;
|
||||
* deleting the project leaves no anchors and no bridges behind.
|
||||
|
||||
``test_iou_relay_control`` is the negative control on an isolated
|
||||
@ -159,6 +162,39 @@ def test_iou_kernel_datapath():
|
||||
fast = harness.wait_ping(c1, "10.1.1.2")
|
||||
assert fast["success"] == 100 and fast["avg"] < 50, fast
|
||||
|
||||
# Classifier spot check on a TAP anchor (the Docker suite runs the
|
||||
# full matrix on veth host ends): cls_bpf match-drop and the eBPF
|
||||
# stateful classifier attach to a tun/tap anchor the same way.
|
||||
tc_caps = (caps or {}).get("ubridge_tc") or {}
|
||||
if tc_caps.get("cbpf"):
|
||||
print(".. bpf 'icmp' drops everything on the TAP anchor")
|
||||
compute.call("PUT", f"/projects/{pid}/links/{lid}", {"filters": {"bpf": ["icmp"]}})
|
||||
assert "clsact" in harness.qdiscs(a1) and "clsact" in harness.qdiscs(a2), (
|
||||
harness.qdiscs(a1),
|
||||
harness.qdiscs(a2),
|
||||
)
|
||||
blocked = harness.ping(c1, "10.1.1.2", repeat=4)
|
||||
assert blocked["success"] == 0, blocked["raw"]
|
||||
compute.call("PUT", f"/projects/{pid}/links/{lid}", {"filters": {}})
|
||||
assert "clsact" not in harness.qdiscs(a1), harness.qdiscs(a1)
|
||||
assert harness.wait_ping(c1, "10.1.1.2")["success"] == 100
|
||||
else:
|
||||
print(".. uBridge reports no cbpf: skipping the bpf check")
|
||||
|
||||
modes = tc_caps.get("ebpf_modes") or []
|
||||
if "nth" in modes:
|
||||
print(".. frequency_drop 3 (eBPF nth) on the TAP anchor")
|
||||
compute.call("PUT", f"/projects/{pid}/links/{lid}", {"filters": {"frequency_drop": [3]}})
|
||||
nth = harness.ping(c1, "10.1.1.2", repeat=9)
|
||||
loss = 100 - nth["success"]
|
||||
print(f".. {loss} % round-trip loss")
|
||||
# the kernel counts per direction: 1 - (2/3)^2 = 55.6 % round trip
|
||||
assert 20 <= loss <= 90, nth["raw"]
|
||||
compute.call("PUT", f"/projects/{pid}/links/{lid}", {"filters": {}})
|
||||
assert harness.wait_ping(c1, "10.1.1.2")["success"] == 100
|
||||
else:
|
||||
print(".. uBridge reports no eBPF nth: skipping the frequency_drop check")
|
||||
|
||||
# suspend: anchor admin-down (the port bridge's TAP writes fail EIO,
|
||||
# nothing comes back); resume restores
|
||||
compute.call("PUT", f"/projects/{pid}/links/{lid}", {"suspend": True})
|
||||
@ -206,8 +242,11 @@ def test_iou_kernel_datapath():
|
||||
# node stop: uBridge holds the anchor fds, so the IOL bridge goes
|
||||
# first and the taps with it (a stopped node must not litter the
|
||||
# host with gi devices); a restart recreates the whole wiring from
|
||||
# the NIO that outlived it.
|
||||
print(".. stopping and restarting router 1")
|
||||
# the NIO that outlived it — *including the NIO's filters*, which
|
||||
# land on the freshly created anchor. The delay filter goes on
|
||||
# before the stop so the restart has something to restore.
|
||||
print(".. stopping and restarting router 1 with a delay filter applied")
|
||||
compute.call("PUT", f"/projects/{pid}/links/{lid}", {"filters": {"delay": [100]}})
|
||||
compute.call("POST", f"/projects/{pid}/nodes/{n1_id}/stop")
|
||||
assert harness.wait_until(lambda: not harness.tap_exists(a1), timeout=15), a1
|
||||
# the per-link bridge lost this end's port; it may be gone entirely
|
||||
@ -218,8 +257,14 @@ def test_iou_kernel_datapath():
|
||||
c1 = _boot(server, pid, n1_id)
|
||||
assert harness.tap_exists(a1)
|
||||
assert harness.bridge_members(bridge) == sorted([a1, a2]), harness.bridge_members(bridge)
|
||||
assert harness.wait_until(lambda: "netem" in harness.qdiscs(a1), timeout=15), harness.qdiscs(a1)
|
||||
harness.configure_ios(c1, "R1", eth_ip="10.1.1.1", eth_if="Ethernet0/0")
|
||||
assert harness.wait_ping(c1, "10.1.1.2", attempts=5)["success"] == 100
|
||||
survivor = harness.wait_ping(c1, "10.1.1.2", attempts=5)
|
||||
assert survivor["success"] == 100, survivor["raw"]
|
||||
assert survivor["avg"] >= 150, (baseline, survivor)
|
||||
compute.call("PUT", f"/projects/{pid}/links/{lid}", {"filters": {}})
|
||||
cleared = harness.ping(c1, "10.1.1.2", repeat=3)
|
||||
assert cleared["success"] == 100 and cleared["avg"] < 50, (baseline, cleared)
|
||||
|
||||
c1.close()
|
||||
c2.close()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user