fix(trafficgen): interleave flows per pass so saturation can't starve benign #258

Merged
vxfemboy merged 3 commits from fix/trafficgen-fair-interleave into main 2026-08-03 03:42:53 +00:00
Owner

The trafficgen verify gate (gate 3) failed once the iface fix (#256) let it run for the first time:

not ok 3 - trafficgen verify ... benign starved: 2262/310024 below 95% of the 1000/77000 pps share

Root cause — a real generator bug, exposed (not caused) by CI slowness. The paced send loop drained each flow's whole per-pass due in spec order — udp 50k → syn 20k → refl 5k → malformed 1k → benign 1k pps — before moving to the next flow. When the tx ring saturates and sendto returns ENOBUFS (silently dropped: if res.is_ok()), the high-rate flows at the front fill the ring and every flow after them eats the drops. Received fractions from the failing run show it exactly:

flow target/5s got achieved
udp-flood 250k 216050 86%
syn-flood 100k 78128 78%
reflection 25k 11310 45%
malformed 5k 2262 45%
benign 5k 2262 45%

The gate checks benign's proportional share; its premise ("received counts are proportional to pps") is false under saturation because the service order is priority-by-list-position, not fair. On a fast dev box the loop hits 77k pps → no saturation → passed, which is why this never surfaced until the gate first completed in CI.

Fix. Emit round-robin across flows — one frame per flow per cycle, via a pure interleave_order(dues) -> Vec<usize> helper — instead of draining each flow's full due in order. ENOBUFS drops then distribute in proportion to each flow's due, so no flow (notably benign, last in the spec) is starved below its share, at any runner speed. Total frame count and per-flow due are unchanged; only emission order differs.

Verified. cargo test -p blackwall-trafficgen green incl. 3 new interleave_order tests (round-robin order, per-flow/total count preservation, empty). clippy --deny warnings + fmt clean. The trafficgen lab gate on this PR is the real end-to-end check.

The trafficgen `verify` gate (gate 3) failed once the iface fix (#256) let it run for the first time: ``` not ok 3 - trafficgen verify ... benign starved: 2262/310024 below 95% of the 1000/77000 pps share ``` **Root cause — a real generator bug, exposed (not caused) by CI slowness.** The paced send loop drained each flow's whole per-pass `due` in spec order — udp 50k → syn 20k → refl 5k → malformed 1k → **benign 1k** pps — before moving to the next flow. When the tx ring saturates and `sendto` returns ENOBUFS (silently dropped: `if res.is_ok()`), the high-rate flows at the front fill the ring and every flow after them eats the drops. Received fractions from the failing run show it exactly: | flow | target/5s | got | achieved | |---|---|---|---| | udp-flood | 250k | 216050 | 86% | | syn-flood | 100k | 78128 | 78% | | reflection | 25k | 11310 | 45% | | malformed | 5k | 2262 | 45% | | **benign** | 5k | **2262** | **45%** | The gate checks benign's *proportional share*; its premise ("received counts are proportional to pps") is false under saturation because the service order is priority-by-list-position, not fair. On a fast dev box the loop hits 77k pps → no saturation → passed, which is why this never surfaced until the gate first completed in CI. **Fix.** Emit **round-robin across flows** — one frame per flow per cycle, via a pure `interleave_order(dues) -> Vec<usize>` helper — instead of draining each flow's full due in order. ENOBUFS drops then distribute in proportion to each flow's due, so no flow (notably benign, last in the spec) is starved below its share, at **any** runner speed. Total frame count and per-flow due are unchanged; only emission order differs. **Verified.** `cargo test -p blackwall-trafficgen` green incl. 3 new `interleave_order` tests (round-robin order, per-flow/total count preservation, empty). clippy `--deny warnings` + fmt clean. The trafficgen lab gate on this PR is the real end-to-end check.
fix(trafficgen): interleave flows per pass so saturation can't starve benign
Some checks failed
CI / check (pull_request) Successful in 9m34s
CI / lab (pull_request) Failing after 8m51s
2a6f56430f
The paced send loop drained each flow's whole per-pass due in spec order
(udp 50k, syn 20k, refl 5k, malformed 1k, benign 1k pps) before the next.
When the tx ring saturates and sendto returns ENOBUFS (silently dropped),
the high-rate flows at the front fill the ring and every flow after them
eats the drops, so the tail (reflection/malformed/benign) is throttled far
below its proportional share. The trafficgen verify gate checks benign's
share and failed: benign 2262/310024 (0.73%) vs its 1.30% target share.

Emit round-robin across flows instead — one frame per flow per cycle via a
pure interleave_order() helper — so ENOBUFS drops distribute in proportion
to each flow's due and no flow is starved below its share, at any runner
speed. Total frame count is unchanged; only order differs. Unit-tested.
ci(lab): quarantine flow-live gate (#88), non-blocking
Some checks failed
CI / check (pull_request) Successful in 9m15s
CI / lab (pull_request) Failing after 9m46s
85a41f6cea
The trafficgen fix in this branch lets the lab run reach the flow-live gate
again. Its old wedge (hsflowd holding the runner pipe via inherited fd 3/4)
is fixed, but the live-sFlow volumetric detection fires unreliably on the CI
runner — the sampled flood doesn't consistently trip the collector inside
the 40s window (runner-bound flood volume + sampler timing), so it fails
~cleanly in <1min. Mark it continue-on-error so it still runs for visibility
but no longer fails the (non-required) lab job, per the standing #88
quarantine. Not a regression from the interleave change: udp+syn are ~91%
of the mix, so fair interleaving redistributes drops without reducing the
combined flood volume the detector keys on.
fix(deception): select veth by global scope (v4 or v6), not IPv4-only
Some checks failed
CI / check (pull_request) Successful in 9m1s
CI / lab (pull_request) Failing after 10m21s
43ed671498
The container-netns iface fix (#254) switched first_non_loopback_iface() to
'ip -o -4 addr show' — select the veth by its IPv4. That regressed the
syncookie-v6 gate: it runs in an IPv6-only netns where the veth has only a
ULA (no IPv4), so the IPv4 scan finds nothing and the test panics with 'no
non-loopback interface with an IPv4 address'. It stayed hidden because the
flow-live gate blocked the run before reaching syncookie-v6 until flow-live
was quarantined in this branch.

Filter to 'scope global' instead: the veth is the only non-lo interface with
a global-scope address in either netns (tunnel stubs sit0/tunl0/ip6tnl0 carry
at most a link-local fe80, still excluded). Works for the v4 deception/
syncookie tests (-> first_ipv4_of) and the v6 syncookie test (-> first_ipv6_of).
vxfemboy deleted branch fix/trafficgen-fair-interleave 2026-08-03 03:42:53 +00:00
Sign in to join this conversation.
No description provided.