The relay engines' observable paths had no live coverage: every
capture/start call in the suite sat on a kernel anchor (AF_PACKET
capture start_kernel) and markers were only asserted on marker
add_kernel. The two relay engines' own command shapes were unit-tested
only:
* bridge engine - bridge start_capture / bridge add_packet_filter ... mark
* iol_bridge engine - iol_bridge start_capture / iol_bridge add_packet_filter ... mark
Docker relay control (bridge engine, 1 passed 33s): the capture start
REST call writes the ICMP exchange to a pcap off the relay bridge; a
mark filter attached with capture_node_id n1 signals exactly 10
marker.match events over the dedicated marker WS for 5 pings (5
requests + 5 replies, both directions), carrying node_id/link_id/tag and
the busybox frame length, writes exactly those 10 frames to the marker
pcap, and deleting the marker takes the pcap with it.
IOU relay control (iol_bridge engine, 1 passed 173s): the same two
checks on iol_bridge start_capture and the port's mark filter, driven
by the real IOS ping through the fabric socket relay.
Not duplicated in the ESW/VPCS/cloud relay controls: they issue the same
uBridge bridge commands through their own call sites, which stay covered
by unit tests; the IOL container relay goes through the same iol_bridge
engine already exercised here.
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.
Two real Alpine containers on an isolated instance, addressed the way a
GNS3 user does it (the node's persistent /etc/network/interfaces, applied
by init.sh) and driven through the Docker daemon socket the server itself
uses. Four scenarios:
* fast path: the per-link kernel bridge enslaves exactly the two veth host
ends, real ICMP crosses, the L2-anchor spec E.1/E.2 assertions hold
(idle silence measured with the guests' links down -- a container's own
boot-time IGMP report re-fires ~10 s later, so silencing is not just
IPv6), netem delay, suspend/resume, capture, link delete/re-create,
container stop/start (the veth host ends die with the adapter and are
rebuilt from the NIO), zero residue on project delete;
* filter matrix, one type at a time, each asserted through real traffic
and its tc state: netem core (delay, jitter+distribution, loss, corrupt,
duplicate), the extensions (reorder+correl+gap, seed+limit, gemodel,
rate), cls_bpf match-drop including a byte-size discriminator, the eBPF
classifier (frequency_drop every-Nth and -1, quota byte-cap cutoff,
window_drop single outage with per-seq pass/drop/pass evidence), plus
available_filters offering the kernel-only types;
* relay control: no per-link bridge, the kernel-only types hidden;
* relay->kernel reopen upgrade: the datapath choice is flipped across a
server restart and the project reopened -- the link is rebuilt on the
kernel datapath and forwards.
The harness gains a Docker guest driver (a stdlib Engine API client over
the daemon socket: exec with stream demux, container id by deterministic
name -- the controller's node payload does not carry it, image inspect),
a digest-pinned image helper (cache hit = no network, miss = pull through
the server's own route, neither = skip) and Server.restart() for the
reopen scenario (which also needs the local compute's capabilities to be
up before a project open is accepted).
Live: 4/4 scenarios, 142 s for the file; full e2e suite 10/10 (1503 s);
unit suite 2149 passed; ruff clean. The matrix measures loss with 20
packets at 0.2 s spacing (143 s -> 58 s) and the jitter RTT assertion
keeps statistical margin (per-direction N(50,20); 10 packets, >= 65 ms).