test(e2e): relay filters shape real traffic on both relay engines

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.
This commit is contained in:
YueGuobin 2026-10-02 22:13:30 +08:00
parent 4fc0398423
commit 9d937802d5
No known key found for this signature in database
5 changed files with 94 additions and 11 deletions

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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: