A "Combining filters" section for packet-filters.md: deterministic drops
are an OR-gate (first kill wins — a clsact-dropped frame consumes no rate
serialization, no limit capacity and no netem counter), netem draws are
independent probabilities that multiply in one qdisc pass, and a round
trip squares everything. The couplings that do exist: quota's byte count
sees only frequency_drop survivors (ordered evaluation slows the burn),
delay/reorder/rate fold into one time_to_send, limit x rate x delay is
the tail-drop triangle to watch, and seed pins netem's draws but not the
eBPF program's own PRNG. Closes with a fully-loaded 12-filter walkthrough
of one frame through the gauntlet.
The Packet filters section of docker-kernel-datapath.md was the de-facto
filter documentation every other datapath doc pointed at; move it to
features/packet-filters.md as the node-type-neutral home (veth or TAP
anchor), centered on a mermaid walkthrough of all 13 types through the
anchor's egress chain: clsact first (the eBPF stateful classifier's nth /
quota / window modes at prio 1, then the bpf expression lines at 10-99,
first drop wins), the single netem qdisc behind it (enqueue draws, one
time_to_send drained by the delay queue).
Clarify a naming trap the table verbs invite: uBridge's tc module only
borrows the iproute2 vocabulary — the tc ... tokens are its own hypervisor
command names over hand-built netlink messages (RTM_NEWQDISC /
RTM_NEWTFILTER), raw bpf() syscalls, embedded distribution tables and
instruction array, libpcap as a library only; the tc binary is never
invoked outside the test suite's oracles. Verified against the uBridge
sources (hypervisor_tc.c, tc_impair.bpf.c, tc_ebpf.c), not just the
commands gns3-server sends.
docker-kernel-datapath.md keeps a pointer section; the iol doc's filter
reference and docs/README.md's tree and catalog point at the new doc.
The live suite asserted the datapath over IPv4 only. Give every guest a
ULA IPv6 address next to its v4 one and twin every v4 assertion point,
sharing the impairment window (one filter application, both families
asserted) so the runtime grows by minutes, not double.
- harness: configure_ios(eth_ipv6=), ping() dispatches to `ping ipv6`,
docker_bring_up_ipv6 (the Docker daemon leaves eth0 with
disable_ipv6=1 on v6-less bridge networks, so the interfaces-file
route cannot work), family-aware docker_wait_address, pcap_records
scans every record (a leading DAD/MLD frame must not mask the
sampled ethertype), and pcap_icmp_rtt measures the wire round trip
from capture timestamps;
- scenarios: connectivity/delay/suspend/isolation/churn checks in both
families, captures assert 86dd next to 0800, the libpcap bpf drops
are asserted family-selective (icmp vs icmp6, each killing its own
family and leaving the other alive), the eBPF classifier modes on
both families, marker v6 twins with an echo-specific expression
(bare icmp6 also matches DAD/MLD and would break the exact counts),
the v6 sharkd replay, and the link-local matrix gains the 86dd
all-nodes row; serial (HDLC) legs stay v4-only;
- the dynamips-emulated classic IOS under-reports its `ping ipv6` RTT
by ~40 % of a delayed round trip (pcap-verified: the wire carries the
v6 echo exchange in the same ~230 ms as v4 while the console reports
~130 ms), so the dynamips-family scenarios assert the v6 delay from
the capture's own timestamps — recorded in tests/e2e/README.md.
Full suite: 17 passed in 58m50s.
Support uploading images into nested subfolders inside predefined image directories, and check whether templates can be created for uploaded images based on existing appliance definitions.
Both fixes from the code-review pass were carried by other tests'
side effects: the move_to_ns failure path (the branch that used to skip
the delete_veth) and the required-but-nullable bridge field on the NIO
create routes.
The persistent-TAP lifecycle — probe, sweep, create, register, delete —
was implemented four times, once per node type, and the copies had drifted
(probe mechanism, _tap_datapath reset, keep-vs-recreate policy). The
mechanical half moves into KernelDatapathMixin:
- _probe_persistent_taps(probe_name): the scratch-TAP probe, returning the
refusal for the caller's own warning (QEMU, Dynamips);
- _create_anchor_taps(ports, set_owner, keep_existing): sweep + create +
optional ownership handover + carrier down + registry, with the ports
enumerated by the node type and named through the new _tap_name hook;
- _delete_anchor_taps(): the suppressed tap sweep + per-link bridge
cleanup + registry clear.
Each node type keeps what its fd holder dictates: the release step before
the tap sweep (the QEMU process, the IOL bridge, the hypervisor's NIO
bindings, the container port bridges), its probe policy, and its registry
lifetime. QEMU's now-unconditional registry clear aligns it with the other
three; everything else is behavior-preserving.
Regression coverage added with the fixes:
- capture-slot semantics: second-port refusal, failed-start rollback, and
a non-owner port's stop not reaching `capture stop_kernel`;
- QEMU stop/start teardown on a mid-body or pre-launch failure, IOU start
cleanup and filter guards, Dynamips half-attach undo plus retry;
- switch VLAN reconcile skipping a deferred anchor and rolling the
ports_mapping back on failure;
- Docker/QEMU occupied-port refusal, probe ctime invalidation, probe
survival of a failing uBridge start;
- env-marker dialects (mid-line mentions, falsy values) and the agent
merge rejecting entries without a device_name.
Test-body imports of product modules move to the shared header.
Compute:
- Track the anchor that owns uBridge's single AF_PACKET capture slot: a
second kernel port's capture is refused with a clear error (its flag
rolled back), a failed start no longer wedges the port, and only the
owning port's stop issues the process-wide `capture stop_kernel`
(docker/qemu/iou/dynamips/switch, via the mixin's slot helpers).
- Unwind failed starts instead of leaking uBridge and the anchor TAPs:
QEMU's stop body runs under try/finally, QEMU and IOU start the datapath
inside the launch try, Dynamips undoes a half-attached port (the retry
used to be short-circuited into a silent black hole), and Docker deletes
the veth pair when the namespace move fails.
- Restore the relay filter guard on IOU (kernel-only filters raise
NodeError, an uncompilable BPF line degrades to a warning), validate the
eBPF/quota/window/bpf filter shapes before touching the bridge (short or
non-numeric values surfaced as 500s after partial state), and refuse a
second NIO on an occupied Docker/QEMU port (the guard IOU and Dynamips
already had).
- Probe fixes: `_probe_bridge_tap` survives a failing uBridge start, and
the probe caches key on ctime too, so a chmod/setcap remedy re-probes.
Controller:
- Parse GNS3_UNIX_SOCKET_NIO like the compute does (line-anchored, truthy
value) and mirror the compute's class selection in kernel_anchor_type.
- Read partial capability reports defensively; make BridgeNIO.bridge
required-but-nullable so an omitted key is a 422 rather than a KeyError.
- Strip the packet_loss correlation slot even when it is zero: the relay
filter takes exactly one argument and rejected `packet_loss 10 0`.
Ethernet switch: skip ports whose absorbed anchor does not exist yet (the
join is deferred by design) and roll the ports_mapping back when the VLAN
reconcile fails, so a retry still diffs against the old mapping.
Agent: reject device config entries without a device_name instead of
silently dropping them (a malformed batch used to execute partially while
reporting success).
Also split the netem builder to clear C901 and drop the two per-file ruff
ignores the PR had added, keeping the remove-only policy.
The rebase onto 65c858d45 brought upstream's UP032 enforcement; the
conflict resolution kept this branch's semantics on the overlapping
lines, so their .format() calls needed the conversion afterwards
(ruff check --fix, 41 sites) plus the formatter's rewrap (7 files).
Replicates the CI recipe locally: pip check, ruff check/format, mypy
2.3.1 (clean, 434 files), unit suite 2216 passed.
Adds what running the suite on a real machine actually requires (uBridge
with caps, tc for the filter assertions, a Docker daemon, cc -static for
the raw-frame probe), the full image-selection matrix (preferred
filenames, fallback patterns, per-folder layout, the alpine digest
ladder; missing images skip, never false-fail), the NetworkManager host
hazard and its record, timing/load sensitivity notes, the complete
GNS3_E2E_* environment-variable list — and the link-local scenario,
which the scenario list was missing.
The 'impairment cleared' asserts used avg < 50 on three-packet IOS pings;
the unimpaired baseline is 20-45 ms with 4 ms quantization, and host
scheduling jitter pushed averages to 54/56 in two full-suite runs
(esw, dynamips) — a margin, not an impairment (the qdisc asserts pass,
success is 100 %). The contrasted delayed regime is >= 150 ms, so a
100 ms bound still separates the two cleanly. Docker's sub-millisecond
veth asserts keep their 50 ms bound (three orders of magnitude of
headroom, never observed to flake).
The exec stream can truncate after busybox's 4294967 ms RTT timer-wrap
artifact, dropping the summary line — docker_ping then reported a
phantom 100 % loss and made the corrupt-filter scenario fail spuriously.
Factor the parsing into _busybox_ping_parse: the summary stays
authoritative when present; otherwise the loss is derived from the
seq= replies actually seen (no replies at all still reports 100 %).
Same guard the ethernet-switch scenario already carries: NetworkManager
can release an NM-managed TAP from its bridge shortly after
(re-)enslavement (docs/bugs/networkmanager-tap-release.md). Applied to
the Dynamips, QEMU, IOU, IOL-runner and switch-cascade scenarios after
their nodes have booted (no-op where nmcli/NetworkManager is absent).
This keeps those tests deterministic on un-declared hosts.
The reproduction section described the harness only in prose; the actual
driver scripts were throwaway lab files. Embed the distilled, runnable
reproducer (drives uBridge's control socket through the repo's own client,
kept tap for the single-variable A/B), the exact measurement semantics
(40 re-enslavements, ~1 kHz / 200 ms membership sampling, a release = the
brif entry vanishing, latency print) and the flip protocol, so a reader
can reproduce the 19-20/40 vs 0/40 numbers on an affected host. Also point
the host-fix layer at the reproducer for re-verification.
A kernel-datapath link could die silently after a delete/recreate: on
NetworkManager hosts, NM's state machine releases an NM-managed TAP from
its bridge 58-87 ms after (re)enslavement (~50% while the tap has not
been assumed yet; the port stays up, no GNS3 process logs anything).
Root-caused with a server-free minimal harness (one uBridge, one tap,
one bridge, an addif/delif loop) and a single-variable NM A/B; the
NetworkManager side is explained by three of its commits (assumption
recheck on external enslavement, master removal during activation,
external changes not touching assumed slaves).
Records: forensic signatures (ifindex unchanged, flags 0x300 cleared,
heal-on-next-update), the reproducer, the verified host fix
(unmanaged-devices conf.d snippet: 19-20/40 -> 0/40, and the e2e
scenario green with test-side workarounds disabled), scope and history
(2016 issue #440 veth incident; the 2026-10-01 ethernet-switch
incident previously misattributed). Cross-linked from the L2-anchor
spec and the switch feature doc.
The esw kernel fast-path scenario deletes and re-creates a link, which
re-enslaves the peer anchor — and on NetworkManager hosts NM's netdev
state machine can release an NM-managed TAP from its bridge some 60-70 ms
after enslavement, ~50% of re-enslavements, while the tap is in NM's
not-yet-assumed state. The freed port stays administratively up and no
GNS3 process logs anything: the membership assert sees the join, then the
ping measures a silent blackhole (this was the line-277 e2e failure).
Root-caused with a server-free harness (one uBridge, one fresh tap, one
bridge, an addif/delif loop): the same tap frees 20/40 while NM-managed,
0/40 after `nmcli device set <tap> managed no`, 20/40 again when
re-managed. Docker's veth host ends are not NM-managed, which is why only
TAP anchors ever showed it — including the "anchor lost its bridge
membership after a ports_mapping update (TAP peers only)" incident from
the switch's development, whose conclusion is corrected in the docs.
Unmanage the anchors once the routers have booted, before the scenario
re-enslaves them; document the failure signature and the host-side remedy
(per-device nmcli call, or NM unmanaged-devices configuration) in the
L2-anchor spec and the switch feature doc.
Upstream's mypy pass narrowed the filter copy to UDPNIO only (the
Ethernet/TAP members of the union have no filters field). BridgeNIO —
the kernel-datapath NIO — carries filters too, so on an IOU kernel link
every PUT of a link's filters cleared them and applied nothing: netem
never landed on the anchor (e2e test_iou_kernel_datapath caught it).
Narrow to (UDPNIO, BridgeNIO): still avoids attribute access on the
models without the field, keeps both filter-carrying types.
The rebase onto upstream 3.1 (mypy + ruff I001/F401 waves) left the tree
short of the newly enforced baselines in three ways:
* contextlib / UbridgeError imports dropped as false duplicates during
conflict resolution left NameErrors in iou_vm and qemu_vm (F821);
the Dynamips router grabbed the compute-level NIOTAP instead of the
hypervisor one, breaking TAP anchoring (test failure).
* unused imports (KERNEL_UNSUPPORTED_FILTERS, UbridgeError, List) and
unsorted blocks left behind where the stack's later commits moved the
last usage elsewhere — ruff I001/F401/RUF022 now enforce these.
* mypy, now baseline-free: cache dict annotations in tc_probe, the
locators candidate list, the resolved-params None branch in the MCP
runner, and validate_bpf_syntax keeps the upstream Dict[str, Any]
return (the strict Optional[str] was wrong for the "valid" bool entry).
The e2e raw-frame probe resolves its compiler via shutil.which (S607)
and zips with strict= (B905); the remaining subprocess call is S603 by
nature and gets a per-file ignore like other test files.
ruff check/format clean, mypy at upstream parity (no new errors),
full unit suite 2167 passed.
A batch carrying the same device twice lost all but the last entry's
commands: the per-device command map kept only the last entry while
result processing iterated every original entry, so each row reported
the last entry's output under its own command list.
c3c78f99a consolidated the merge into the MCP template renderer, but
that only runs when a template is passed — the MCP direct-commands
path and gns3-copilot's direct _run path were left uncovered.
Normalize once in each tool's _run (single point covering all entry
paths, restoring the c42b3a59b coverage) and keep the renderer merge
as boundary normalization. Blocked-command info now also accumulates
across same-device entries instead of last-winning in the per-device
map.
The peer end of an Ethernet switch fast-path link is a NIOBridge with no
bridge of its own (the switch absorbed its anchor into the switch bridge).
Its update reconcile still ran the impairment pass with the empty filter
set the controller routes to that end — and an empty set is a plain
`tc reset` on the shared anchor, silently wiping the netem/cls_bpf/eBPF
the switch had just applied. Any link update did it (filters, markers,
suspend, capture start/stop), and the outcome depended on the link's
endpoint order: UdpLink.update() PUTs node1 then node2, so with the
switch first its apply ran first and the peer's reset ran last (API
green, data plane unimpaired), while with the peer first the reset ran
first and the filter survived. That order dependence is also why the
shipped e2e never caught it — it only created peer-first links.
Ownership is now explicit: only the end that owns the anchor's tc state
applies the impairment reconcile (_owns_anchor_impairments — a NIOBridge
with `bridge is None` is a passive carrier, the same idiom the
bridge-delete path already used). Markers, capture and the suspend
carrier still apply on both ends: markers reconcile per process, so
neither end can strip the other's, and the carrier converges by
agreement.
Red/green: two new docker unit tests fail on the unfixed tree with
`tc reset "<anchor>"` and pass after; the switch e2e also creates one
link switch-first and asserts the netem qdisc and the RTT hold under
that order (failed with the wipe, passes with the fix). Full unit suite
2110 passed; the switch e2e file passed end to end (378s).
A correctness read-through of the five delivered datapath documents, plus
the two code comments that carried the same stale vocabulary.
Datapath selection: the VendorDockerVM selector is `console_type=docker_exec`
alone (`GNS3_SKIP_INIT` and friends are behavior knobs of the chosen class);
the cross-compute and anchor-less fallbacks are named for what they are; the
Ethernet-switch endpoint is the documented exception to the anchoring rule,
and the IOL runner container is called out as the Docker node that does not
anchor on a veth.
Filter vocabulary: the stateful eBPF program evaluates four modes
(nth → quota → window → flow), GNS3 exposes three of them, and window_drop
joins the kernel-only list; the correl suffixes gate on the `rate` token as
the extension-build marker.
group_fwd_mask: the 0xfffd port mask is applied inside uBridge's `brctl addif`
itself, not as a separate command — the link-creation sequences and the
operation tables no longer show an arrow the server never sends. Same class
of fix for `link set up` on the IOL anchor (addif already brought it up; the
command only pushes a suspended anchor back down) and for the QEMU carrier
replication (QMP `set_link`, gated by `replicate_network_connection_state`).
Lifecycles and stop orders re-worded around the invariant rather than the
node type: the fd holder must release before the devices are deleted, which
is QEMU's process-first order, IOU's uBridge-first order and Dynamips'
hypervisor-NIO-first order. Deferred wiring for a stopped endpoint is
documented as the common path it is, and the Dynamips "stop is not a close"
note now says what recovery from a dead hypervisor actually requires.
Dynamips specifics: a c7200 needs an adapter in slot 1 (slot 0 is the fixed
I/O slot), the tap-fd direction is stated correctly (writes inject into the
kernel, reads return what the kernel transmits), and the anchor registry is
name-keyed against the controller's port matrix by convention, not derivation.
IOL port bridge naming: `bridge{bay}` on unit 0, `bridge{bay}_{unit}`
elsewhere; the leg swap happens on link delete/re-create or a datapath
switch, while suspend is an in-place NIO update and a project reopen is a
fresh build.
Verified and Roadmap sections: the in-repo e2e suite is distinguished from
the isolated-instance development harness (IOU), assertions are stated with
their bounds and capability gates, and the roadmap entries collapse into the
one remaining project — VXLAN/GENEVE for cross-compute links.
Code comments: `flow` added to the documented mode order in the eBPF apply
path, and the `nio_bridge` module docstring corrected on which filter types
run on the kernel datapath (bpf via cls_bpf, frequency_drop/quota/window_drop
via the eBPF classifier — neither is relay-only any more).
The dynamips doc gets the full suite: anatomy (anchors reached by the
hypervisors' TAP fds, uBridge's AF_PACKET-tap / tc-qdisc sidebands), a
one-way copy-numbered relay comparison (six kernel/user crossings per
way against the kernel datapath's two TAP-fd legs), a staged walkthrough
(reserved-range diamond, egress clsact chain, capture/markers observation
sideband, copies 1-2) and a control sequence of real commands in real
order (hypervisor fd first, capture before markers, carrier last; router
handlers, not computes, as the per-node participants).
The four sibling frame paths are unified on the same form: netem qdisc
node labels, one capture/markers node outside the stages with AF_PACKET
tap edges anchored at ingress and post-chain egress, copy numbering
wherever the trace crosses the kernel/user boundary (qemu's two TAP-fd
legs, iol-docker's six socket/TAP legs), stage subgraphs with role-noted
titles everywhere (iol-docker restructured from its flat chain, iou's
chA/tapA variants folded into the canonical shapes).
The documentation skill records the rules as v3.1.0: four diagram kinds
(anatomy / comparison / control sequence / walkthrough), one step per
node, dashed sidebands per role with meaningful direction, numbered
crossings when copies are the point, glossary before the first diagram.
Sequence diagram: drop the phantom "port fwd_mask 0xfffd" step — uBridge
sets the per-port mask inside brctl addif and gns3-server never sends it.
Frame path: explain the ingress/egress mechanism in prose (the TAP-fd
write lands at the sender's anchor ingress, where nothing is installed;
the bridge's forward out the far anchor is the egress transmission the
tc chain sits on), and group the netio mailboxes into a kernel subgraph
matching the relay diagram. Suspend: a down anchor answers EIO on the
TAP fd in both directions (the section B hardening), not reads-block.
Term table: the anchor shape is per Ethernet bay (4 units per bay), not
the default 2x4. Link operations: carrier up is applied last, matching
_attach_kernel_nio. Link-local: note the verification matrix runs on the
Docker datapath. Reflow paragraphs to the soft-wrap convention.
Term table, QEMU↔QEMU topology with role-suffixed anchors and
frame-copies/qdisc-install dashed edges, relay copy-count diagram,
boxed frame path, link creation sequence; prose reflowed one
paragraph per line.
Replace the ASCII overview with mermaid diagrams per the documentation
standard: a glossary table first, then the two-container architecture
(port bridges inside their uBridge process blocks, the per-link Linux
bridge circled, dashed sidebands split by direction — frame copies flow
to uBridge, qdisc installs to the anchor), a copy-counted relay diagram
(six kernel/user crossings each way, datapath-invariant — swapping
datapaths only changes the middle two), the frame path with the
reserved-range diamond, and the link creation sequence. Details moved
from the old bullets into per-diagram prose; paragraphs reflowed to one
per line.
Replace the ASCII art with a mermaid suite (steady-state topology with
namespace boxes and sideband edge semantics, one-way frame path with
per-object boxes, link-creation sequence, full relay path with the four
kernel/user copies), add a term glossary ahead of the diagrams, reflow the
prose to soft-wrap, and refresh the stale parts: the configuration section
now describes enable_kernel_datapath as the global switch it already is,
with every anchorable node type landed and linked to its feature doc.
Two Alpine containers on a kernel link, a static raw-Ethernet probe built
at run time and docker-cp'd in (the image has no raw-frame tool; DockerDaemon
gains the cp), injecting per-destination-MAC frames from guest 1 while
guest 2 counts arrivals: ordinary multicast, STP, LACP, 802.1X and LLDP must
cross, PAUSE must not (the documented kernel limit). Asserts the anchors'
sysfs brport group_fwd_mask reads 0xfffd through the server's own wiring
(int-parsed: newer kernels print it hex), that the bridge level stays 0, and
that a container stop/start replays the mask with LACP surviving. Negative
control is the uBridge suite's setportgroupfwd 0 case.
A Linux bridge standing in for a cable does not forward the IEEE 802.1D
reserved range (01:80:c2:00:00:00-0f) by default, so LACP/LLDP/802.1X never
crossed a kernel-datapath link while the UDP relay carried them fine. uBridge
now opens every enslave'd port's IFLA_BRPORT_GROUP_FWD_MASK to 0xfffd at
brctl addif time (branch feature/bridge-tap-l2only, commits 0284500/03a18f1/
cf4e494), which the six kernel-datapath feature docs now document as a
section: STP, LACP, 802.1X and LLDP/DCBX cross; 802.3x PAUSE / PFC stays
dropped — br_handle_frame()'s case 0x01 discards it unconditionally, no mask
can enable it, and PFC labs must use a relay link. The full contract, kernel
references and requirements live in the new design spec, marked landed.
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.
The L2-anchor spec's behavioral guarantee — an idle link stays silent for
5 s once the enslavement burst settles — only ran in the Dynamips and
Docker suites. It now also runs in QEMU, IOU, the IOL container, the
switch fast path and the switch cascade, which covers every host-device
creation path:
* tap create (Dynamips/QEMU/IOU anchors), docker create_veth (Docker host
ends; the cascade's race-loser end is hardened after create by the
server), bridge add_nio_tap (the IOL container anchor, the fifth
creator), and the brctl switch bridges, whose multicast snooping is off
precisely so the bridge role can reach silence;
* each scenario silences its guests first (IOS interfaces shut through
the console; Docker guests' links down) and re-verifies the ping after;
* the cascade measures 8 devices at once (four absorbed router anchors,
both cascade ends, both switch bridges).
IOL 158 s, cascade 293 s, ESW fast path 378 s, QEMU 428 s, IOU 308 s —
all green.
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.
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 Alpine containers on a kernel-only link with traffic-insight markers
attached to a veth anchor (marker add_kernel), observed on the dedicated
marker WebSocket via a new harness piece (WebSocketCollector, a sync
facade over websockets' sync client). Real ICMP proves: the marker.match
identity is complete (filter/tag/link_id/capture node/ts/len and the
tx/rx direction set of a ping), the main project channel carries no
matches, the BPF and direction are enforced (a non-matching sibling and
a tx-only marker each see exactly their traffic), the host pcap holds
exactly the matched frames, pausing silences one marker while a sibling
keeps counting (silence is never a dead channel), tag replay serves the
paused tag's timeline with display-filter/link/window narrowing and
refuses with 409 while capturing, and deleting a marker takes its pcap
and signals with it.
Two switches cascaded through the link-owned veth pair, two c7200
routers per switch, real ICMP crossing both bridges: the pair is born
UP with exactly one end per switch bridge, a dot1q trunk cascade
carries two VLANs at once, the symmetric access/trunk mode flip on the
cascade ends isolates and revives a VLAN in place (asymmetric flips are
semantically broken — the access side egresses untagged into the far
trunk's native PVID), a delay filter lands as netem on both ends (one
per direction, RTT ~= 2x delay), suspend admin-downs both ends, and a
link delete/re-create retires the whole pair and mints a new one under
the new link id — zero host residue after the project goes.
docker create_veth hardens only its FIRST end (the Docker shape: host
anchor plus a container leg that keeps its L3 life). A cascade end is
that second, unhardened end whenever the peer switch's create won the
race, and the kernel gives it an IPv6 link-local — a silent L3 identity
on the switch fabric (ubridge-l2-anchor-spec §E). Each switch now
hardens its OWN end with 'link l2only <end> on' once the pair exists,
whatever branch brought it there (fresh create, lost race, reuse);
idempotent, and best-effort with a warning on a uBridge without the
command.