From 9d937802d511ab8c98406d7b109a6428228c6105 Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Fri, 2 Oct 2026 22:13:30 +0800 Subject: [PATCH] test(e2e): relay filters shape real traffic on both relay engines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extend the docker and IOU relay negative controls with the filters the relay itself supports (delay, frequency_drop, bpf), asserted through real ICMP — the first live coverage of the relay filter path: * bridge engine (docker): delay 100 grows the RTT by ~200 ms and comes back on clear; frequency_drop 2 kills the round trips (the filter counts every packet crossing the bridge, both directions sharing one counter, so on an alternating ping stream one whole direction dies; verified against uBridge directly), frequency_drop -1 blackholes, and bpf drops by libpcap expression ('icmp', then 'greater 150' discriminating by frame size). * iol_bridge engine (IOU): the same delay and frequency_drop effects through the port's iol_bridge filter list, plus the available_filters spot check. Docker 33 s, IOU 168 s, both green. --- docs/features/docker-kernel-datapath.md | 7 ++-- docs/features/iou-kernel-datapath.md | 3 +- tests/e2e/README.md | 11 ++++-- tests/e2e/test_docker_kernel_datapath.py | 48 +++++++++++++++++++++++- tests/e2e/test_iou_kernel_datapath.py | 36 +++++++++++++++++- 5 files changed, 94 insertions(+), 11 deletions(-) diff --git a/docs/features/docker-kernel-datapath.md b/docs/features/docker-kernel-datapath.md index 2d400adea..ed1212226 100644 --- a/docs/features/docker-kernel-datapath.md +++ b/docs/features/docker-kernel-datapath.md @@ -465,9 +465,10 @@ L2-anchor spec §E assertions (no L3 identity, forwarding ports, idle silence with the guests' links down), the filter matrix one type at a time (netem core and extensions, cls_bpf match-drop, the eBPF classifier, each asserted through real traffic), suspend, capture, link delete/re-create, -container stop/start, a relay negative control, and the relay→kernel -upgrade on project reopen (the datapath choice is flipped across a server -restart). The container image is digest-pinned and pulled through the +container stop/start, a relay negative control — whose own filters +(delay/frequency_drop/bpf) are asserted to really shape the relay wire as +uBridge userspace bridge filters — and the relay→kernel upgrade on project +reopen (the datapath choice is flipped across a server restart). The container image is digest-pinned and pulled through the server's own pull route on a cache miss, so collaborators test the same bytes. diff --git a/docs/features/iou-kernel-datapath.md b/docs/features/iou-kernel-datapath.md index a0fc2df66..e6858d152 100644 --- a/docs/features/iou-kernel-datapath.md +++ b/docs/features/iou-kernel-datapath.md @@ -148,7 +148,8 @@ IOU routers** (`tests/e2e/test_iou_kernel_datapath.py`, pytest marker 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. +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 d29b18394..bd538c2c8 100644 --- a/tests/e2e/README.md +++ b/tests/e2e/README.md @@ -79,7 +79,9 @@ Current scenarios: 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. + (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 @@ -91,9 +93,10 @@ Current scenarios: veth datapath): the per-link kernel bridge enslaves exactly the two veth host ends, real ICMP crosses, the filter matrix runs one type at a time (netem core and extensions, cls_bpf match-drop, the eBPF classifier — - each measured through real traffic), plus a relay negative control and a - relay→kernel reopen upgrade (the server's datapath choice is flipped - across a restart). + each measured through real traffic), plus a relay negative control whose + own filters (delay, frequency_drop, bpf) really shape the wire as uBridge + userspace bridge filters, and a relay→kernel reopen upgrade (the server's + datapath choice is flipped across a restart). * `test_marker_kernel_datapath.py` — two real Alpine containers on a kernel-only link with traffic-insight markers attached to a veth anchor (`marker add_kernel`): `marker.match` signals collected off the dedicated diff --git a/tests/e2e/test_docker_kernel_datapath.py b/tests/e2e/test_docker_kernel_datapath.py index a56101a91..ec5276460 100644 --- a/tests/e2e/test_docker_kernel_datapath.py +++ b/tests/e2e/test_docker_kernel_datapath.py @@ -36,7 +36,10 @@ server itself uses. The scenarios cover: offered by ``available_filters`` and everything restores cleanly. * ``test_docker_relay_control`` — the negative control on an isolated relay-configured instance: no per-link kernel bridge, the wire is the - uBridge relay, and the kernel-only filter types are hidden. + uBridge relay, the kernel-only filter types are hidden — and the relay's + own filter set (delay, frequency_drop, bpf) really shapes the wire, as + uBridge userspace filters on the filter node's bridge + (``bridge add_packet_filter``). * ``test_docker_relay_to_kernel_reopen_upgrade`` — the server's datapath choice is flipped (relay → kernel) across a restart and the project is reopened: the link is rebuilt on the kernel datapath and forwards. @@ -481,6 +484,49 @@ def test_docker_relay_control(): assert not (offered & {"rate", "reorder", "gemodel", "duplicate", "seed", "limit", "quota", "window_drop"}), ( offered ) + + # The relay's filters are real uBridge userspace filters walked by + # the bridge's two listener threads: both directions of the wire + # cross them. delay 100 creates one delay line per direction, so the + # RTT grows by ~200 ms. + print(".. relay filters: delay 100 ms (one delay line per direction, RTT +~200 ms)") + _put_filters(compute, pid, link["link_id"], {"delay": [100]}) + harness.docker_wait_ping(daemon, n1["container_id"], R2_IP) + delayed = harness.docker_ping(daemon, n1["container_id"], R2_IP, count=5) + assert delayed["loss"] == 0, delayed["raw"] + assert delayed["avg"] >= 150, (relay_ping, delayed) + _put_filters(compute, pid, link["link_id"], {}) + fast = harness.docker_wait_ping(daemon, n1["container_id"], R2_IP) + assert fast["loss"] == 0 and fast["avg"] < 50, (relay_ping, fast) + + # frequency_drop counts every packet crossing the bridge, both + # directions sharing one counter, so on an alternating ping stream + # the same direction dies on every crossing (probed against uBridge + # directly: replies drop, round trips die wholesale). -1 is the + # drop-everything encoding the suspend emulation uses. + print(".. relay filters: frequency_drop 2 (shared counter, one direction dies)") + _put_filters(compute, pid, link["link_id"], {"frequency_drop": [2]}) + dropped = harness.docker_ping(daemon, n1["container_id"], R2_IP, count=10) + print(f".. {dropped['loss']} % round-trip loss") + assert dropped["loss"] >= 50, dropped["raw"] + _put_filters(compute, pid, link["link_id"], {"frequency_drop": [-1]}) + blackholed = harness.docker_ping(daemon, n1["container_id"], R2_IP, count=4) + assert blackholed["loss"] == 100, blackholed["raw"] + + # bpf on the relay is uBridge's libpcap userspace match-drop — a + # different engine from the kernel datapath's cls_bpf + print(".. relay filters: bpf 'icmp' drops every ICMP frame") + _put_filters(compute, pid, link["link_id"], {"bpf": ["icmp"]}) + icmp_dropped = harness.docker_ping(daemon, n1["container_id"], R2_IP, count=4) + assert icmp_dropped["loss"] == 100, icmp_dropped["raw"] + print(".. relay filters: bpf 'greater 150' discriminates by frame size") + _put_filters(compute, pid, link["link_id"], {"bpf": ["greater 150"]}) + small = harness.docker_ping(daemon, n1["container_id"], R2_IP, count=4) + assert small["loss"] == 0, small["raw"] + big = harness.docker_ping(daemon, n1["container_id"], R2_IP, count=4, size=300) + assert big["loss"] == 100, big["raw"] + _put_filters(compute, pid, link["link_id"], {}) + assert harness.docker_ping(daemon, n1["container_id"], R2_IP, count=5)["loss"] == 0 except BaseException: harness.release(server, pid, failed=True) raise diff --git a/tests/e2e/test_iou_kernel_datapath.py b/tests/e2e/test_iou_kernel_datapath.py index 55b80f82c..a706b2dc9 100644 --- a/tests/e2e/test_iou_kernel_datapath.py +++ b/tests/e2e/test_iou_kernel_datapath.py @@ -34,7 +34,9 @@ Linux bridge, kernel filters, suspend and capture. ``test_iou_relay_control`` is the negative control on an isolated relay-configured instance: the same topology rides the fabric socket ↔ UDP -relay (no per-link bridge, nothing enslaved) and still pings. +relay (no per-link bridge, nothing enslaved) and still pings — with its +filters applied by the IOU-specific engine +(``iol_bridge add_packet_filter`` on the IOL port) really shaping the wire. """ import re @@ -238,7 +240,9 @@ def test_iou_relay_control(): Negative control: the same topology with ``enable_kernel_datapath = false`` (an isolated instance) wires nothing into the kernel — no per-link bridge, no enslavement — and still pings over the fabric - socket ↔ UDP relay. + socket ↔ UDP relay. The relay's filters ride the IOU-specific engine: + the IOL port's userspace filter list (``iol_bridge add_packet_filter``), + walked by both the fabric → wire and wire → fabric paths. Anchors DO exist here (an IOU node creates its port TAPs at start whenever the uBridge can terminate an IOL port on one); the server @@ -281,6 +285,34 @@ def test_iou_relay_control(): relay_ping = harness.wait_ping(c1, "10.1.1.2") assert relay_ping["success"] == 100, relay_ping["raw"] + # The relay link's filters really shape the wire on the IOU engine + # too. delay 100: one delay line per direction (the port's IOL and + # NIO sides are separate lines), so the RTT grows by ~200 ms. + lid = link["link_id"] + offered = {entry["type"] for entry in compute.call("GET", f"/projects/{pid}/links/{lid}/available_filters")} + assert "delay" in offered and "frequency_drop" in offered, offered + + print(".. iol_bridge relay filter: delay 100 ms (per-direction delay lines, RTT +~200 ms)") + compute.call("PUT", f"/projects/{pid}/links/{lid}", {"filters": {"delay": [100]}}) + delayed = harness.ping(c1, "10.1.1.2", repeat=5) + assert delayed["success"] == 100, delayed["raw"] + assert delayed["avg"] >= 150, (relay_ping, delayed) + compute.call("PUT", f"/projects/{pid}/links/{lid}", {"filters": {}}) + fast = harness.ping(c1, "10.1.1.2", repeat=3) + assert fast["success"] == 100 and fast["avg"] < 50, (relay_ping, fast) + + # frequency_drop shares one counter between the port's two + # directions: on an alternating ping stream one whole direction dies + # (same semantics as the plain relay's bridge filters). + print(".. iol_bridge relay filter: frequency_drop 2 (shared counter, one direction dies)") + compute.call("PUT", f"/projects/{pid}/links/{lid}", {"filters": {"frequency_drop": [2]}}) + dropped = harness.ping(c1, "10.1.1.2", repeat=6) + print(f".. success {dropped['success']} %") + assert dropped["success"] <= 50, dropped["raw"] + compute.call("PUT", f"/projects/{pid}/links/{lid}", {"filters": {}}) + recovered = harness.wait_ping(c1, "10.1.1.2") + assert recovered["success"] == 100, recovered["raw"] + c1.close() c2.close() except BaseException: