Files
fips/testing/chaos/ecn-ab-test.sh
T
Johnathan Corgan 56d39f223b Add ECN congestion signaling and transport congestion detection
Implement hop-by-hop ECN congestion signaling through the FMP layer,
transport-level congestion detection via kernel drop counters, and
chaos harness integration for end-to-end validation.

FMP/session ECN plumbing:

- Thread ce_flag parsed at link layer through dispatch_link_message,
  handle_session_datagram, handle_session_payload, and
  handle_encrypted_session_msg to session delivery
- Replace hardcoded false in session-layer record_recv() with actual
  ce_flag, activating ecn_ce_count tracking in session MMP

ECN congestion detection and CE relay:

- Add EcnConfig (node.ecn.*) with configurable loss_threshold (5%)
  and etx_threshold (3.0) for transit congestion detection
- Add send_encrypted_link_message_with_ce() that ORs FLAG_CE into FMP
  header flags; original method delegates with ce_flag=false
- Compute outgoing_ce = incoming_ce || local congestion on next-hop
  link, enabling hop-by-hop CE relay through transit nodes

IPv6 ECN-CE marking:

- Mark ECN-CE (0b11) in IPv6 Traffic Class on received DataPackets
  before TUN delivery when FMP CE flag is set
- Only marks ECN-capable packets (ECT(0)/ECT(1)); Not-ECT packets
  unchanged per RFC 3168

Transport congestion abstraction and UDP kernel drop detection:

- Add TransportCongestion struct to transport layer for transport-
  agnostic local congestion indicators
- Replace tokio::UdpSocket with AsyncFd<socket2::Socket> using
  libc::recvmsg() with ancillary data parsing
- Enable SO_RXQ_OVFL for kernel receive buffer drop counter on every
  packet, wiring up previously-stubbed UdpStats.kernel_drops
- Add TransportDropState for per-transport delta tracking with 1s
  tick sampling via sample_transport_congestion()
- Extend detect_congestion() with transport kernel drop check
  alongside MMP loss metrics

Congestion monitoring and control:

- Add CongestionStats (ce_forwarded, ce_received, congestion_detected,
  kernel_drop_events) to NodeStats with snapshot serialization
- Wire counters into forwarding path, session handler, and transport
  drop sampling with rate-limited warn logging (5s interval)
- Expose congestion data in show_routing control query and
  ecn_ce_count in show_mmp peer entries
- Add congestion counters to fipstop routing tab in two-column layout

Chaos harness integration:

- Add query_routing(), query_transports(), snapshot_all_congestion()
  to chaos control module
- Add congestion/kernel-drop log analysis in logs module
- Add congestion-stress scenario: 10-node tree, 1 Mbps bandwidth,
  5-10% netem loss, heavy iperf3 traffic
- Add IngressConfig for tc ingress policing with per-peer policer
  filters simulating upstream bandwidth bottlenecks
- Add iperf3 JSON result capture to traffic manager for throughput
  measurement across scenarios
- Add ECN A/B test scenarios (ecn-ab-on/off.yaml) with ingress
  policing and comparison script
- Enable TCP ECN negotiation (tcp_ecn=1 sysctl) in container
  entrypoint for end-to-end CE propagation

Tests:

- 10 ECN unit/integration tests: mark_ipv6_ecn_ce variants, CE relay
  chain (3-node propagation), EcnConfig serde roundtrip
- 3 transport drop congestion detection unit tests

Documentation:

- Update fips-mesh-layer.md: replace outdated CE Echo stub with full
  ECN Congestion Signaling section covering detection logic, CE relay,
  IPv6 marking, session tracking, and monitoring counters
- Update fips-configuration.md: add node.ecn.* parameter table and
  ecn block in complete reference YAML
- Update fips-transport-layer.md: add Congestion Reporting section
  with TransportCongestion struct, congestion() trait method, and
  per-transport status; document AsyncFd/recvmsg/SO_RXQ_OVFL in UDP
- Update chaos README: add congestion/ECN scenario docs, ingress
  traffic control, and iperf3 JSON capture sections
- Update README.md: add ECN to features list and "What works today";
  update transport and tooling entries
2026-03-05 17:05:57 +00:00

128 lines
4.0 KiB
Bash
Executable File

#!/usr/bin/env bash
# ECN A/B Throughput Test
#
# Runs two identical chaos scenarios — one with ECN enabled, one disabled —
# and compares iperf3 throughput and congestion counter results.
#
# Usage: ./ecn-ab-test.sh [--seed N] [--duration N]
set -euo pipefail
cd "$(dirname "$0")"
EXTRA_ARGS=()
while [[ $# -gt 0 ]]; do
case "$1" in
--seed|--duration)
EXTRA_ARGS+=("$1" "$2"); shift 2 ;;
*)
echo "Unknown arg: $1"; exit 1 ;;
esac
done
echo "=== ECN A/B Throughput Test ==="
echo ""
# --- Run A: ECN ON ---
echo "--- Phase A: ECN ENABLED ---"
sudo python3 -m sim scenarios/ecn-ab-on.yaml "${EXTRA_ARGS[@]}" || true
echo ""
# --- Run B: ECN OFF ---
echo "--- Phase B: ECN DISABLED ---"
sudo python3 -m sim scenarios/ecn-ab-off.yaml "${EXTRA_ARGS[@]}" || true
echo ""
# --- Compare results ---
echo "=== Results ==="
echo ""
python3 - <<'PYEOF'
import json
import os
import sys
def load_results(path):
if not os.path.exists(path):
return []
with open(path) as f:
return json.load(f)
def extract_throughput(results):
"""Extract per-session throughput in Mbps from iperf3 JSON."""
sessions = []
for r in results:
meta = r.get("_meta", {})
end = r.get("end", {})
# sum_sent / sum_received contain aggregate stats
sent = end.get("sum_sent", {})
recv = end.get("sum_received", {})
sent_mbps = sent.get("bits_per_second", 0) / 1e6
recv_mbps = recv.get("bits_per_second", 0) / 1e6
sessions.append({
"client": meta.get("client", "?"),
"server": meta.get("server", "?"),
"sent_mbps": sent_mbps,
"recv_mbps": recv_mbps,
})
return sessions
def load_congestion(path):
if not os.path.exists(path):
return {}
with open(path) as f:
return json.load(f)
def print_sessions(label, sessions):
# Filter out incomplete sessions (killed at teardown, no valid data)
valid = [s for s in sessions if s["recv_mbps"] > 0]
incomplete = len(sessions) - len(valid)
if not valid:
print(f" {label}: no completed iperf3 sessions ({incomplete} incomplete)")
return 0
print(f" {label}:")
total_sent = 0
total_recv = 0
for s in valid:
print(f" {s['client']:>4} -> {s['server']:<4} "
f"sent={s['sent_mbps']:7.2f} Mbps recv={s['recv_mbps']:7.2f} Mbps")
total_sent += s["sent_mbps"]
total_recv += s["recv_mbps"]
n = len(valid)
print(f" {'':>14} avg sent={total_sent/n:7.2f} Mbps avg recv={total_recv/n:7.2f} Mbps")
print(f" {'':>14} completed={n} incomplete={incomplete}")
return total_recv / n
on_results = load_results("sim-results/ecn-ab-on/iperf3-results.json")
off_results = load_results("sim-results/ecn-ab-off/iperf3-results.json")
on_sessions = extract_throughput(on_results)
off_sessions = extract_throughput(off_results)
print("Throughput:")
avg_on = print_sessions("ECN ON", on_sessions) or 0
print()
avg_off = print_sessions("ECN OFF", off_sessions) or 0
print()
if avg_on and avg_off:
delta_pct = ((avg_on - avg_off) / avg_off) * 100
print(f" Delta: ECN ON vs OFF = {delta_pct:+.1f}% avg recv throughput")
print()
# Congestion counters
print("Congestion Counters (final snapshot):")
for label, path in [("ECN ON", "sim-results/ecn-ab-on/congestion-snapshot-final.json"),
("ECN OFF", "sim-results/ecn-ab-off/congestion-snapshot-final.json")]:
snap = load_congestion(path)
if not snap:
print(f" {label}: no snapshot")
continue
totals = {"ce_forwarded": 0, "ce_received": 0, "congestion_detected": 0, "kernel_drop_events": 0}
for node_id, data in sorted(snap.items()):
cong = data.get("congestion", {})
for k in totals:
totals[k] += cong.get(k, 0)
print(f" {label}: ce_fwd={totals['ce_forwarded']} ce_recv={totals['ce_received']} "
f"cong_detect={totals['congestion_detected']} kern_drops={totals['kernel_drop_events']}")
PYEOF