From 1f544c96a532d6ce662f73977795e211581addb4 Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Fri, 2 Oct 2026 22:32:42 +0800 Subject: [PATCH] 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. --- docs/features/iol-docker-kernel-datapath.md | 4 ++ docs/features/iou-kernel-datapath.md | 11 ++-- tests/e2e/README.md | 16 +++--- tests/e2e/test_iol_docker_kernel_datapath.py | 37 ++++++++++++++ tests/e2e/test_iou_kernel_datapath.py | 53 ++++++++++++++++++-- 5 files changed, 106 insertions(+), 15 deletions(-) diff --git a/docs/features/iol-docker-kernel-datapath.md b/docs/features/iol-docker-kernel-datapath.md index 91d3a4b08..f80f1423c 100644 --- a/docs/features/iol-docker-kernel-datapath.md +++ b/docs/features/iol-docker-kernel-datapath.md @@ -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 diff --git a/docs/features/iou-kernel-datapath.md b/docs/features/iou-kernel-datapath.md index e6858d152..774a0b39d 100644 --- a/docs/features/iou-kernel-datapath.md +++ b/docs/features/iou-kernel-datapath.md @@ -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 diff --git a/tests/e2e/README.md b/tests/e2e/README.md index bd538c2c8..30dcf0e77 100644 --- a/tests/e2e/README.md +++ b/tests/e2e/README.md @@ -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 diff --git a/tests/e2e/test_iol_docker_kernel_datapath.py b/tests/e2e/test_iol_docker_kernel_datapath.py index b39484e6e..735c2f00c 100644 --- a/tests/e2e/test_iol_docker_kernel_datapath.py +++ b/tests/e2e/test_iol_docker_kernel_datapath.py @@ -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}) diff --git a/tests/e2e/test_iou_kernel_datapath.py b/tests/e2e/test_iou_kernel_datapath.py index a706b2dc9..aff55b417 100644 --- a/tests/e2e/test_iou_kernel_datapath.py +++ b/tests/e2e/test_iou_kernel_datapath.py @@ -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()