mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-09 16:24:45 +00:00
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
172 lines
5.6 KiB
Python
172 lines
5.6 KiB
Python
"""Random iperf3 traffic generation between node pairs.
|
|
|
|
Spawns iperf3 clients as background processes in containers. The iperf3
|
|
server is already running in each container (started by the Dockerfile
|
|
entrypoint).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
import random
|
|
import time
|
|
from dataclasses import dataclass, field
|
|
|
|
from .docker_exec import docker_exec_quiet
|
|
from .scenario import TrafficConfig
|
|
from .topology import SimTopology
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
@dataclass
|
|
class TrafficSession:
|
|
client_node: str
|
|
server_node: str
|
|
started_at: float
|
|
duration_secs: int
|
|
container: str
|
|
result_file: str = ""
|
|
|
|
|
|
class TrafficManager:
|
|
"""Manages random iperf3 sessions across the mesh."""
|
|
|
|
def __init__(
|
|
self,
|
|
topology: SimTopology,
|
|
config: TrafficConfig,
|
|
rng: random.Random,
|
|
down_nodes: set[str] | None = None,
|
|
):
|
|
self.topology = topology
|
|
self.config = config
|
|
self.rng = rng
|
|
self.down_nodes = down_nodes or set()
|
|
self.active_sessions: list[TrafficSession] = []
|
|
self.completed_results: list[dict] = []
|
|
|
|
@property
|
|
def active_count(self) -> int:
|
|
return len(self.active_sessions)
|
|
|
|
def maybe_spawn(self):
|
|
"""Spawn a new iperf3 session if under the concurrency limit."""
|
|
if self.active_count >= self.config.max_concurrent:
|
|
log.debug(
|
|
"At max_concurrent (%d), skipping traffic spawn",
|
|
self.config.max_concurrent,
|
|
)
|
|
return
|
|
|
|
node_ids = [nid for nid in self.topology.nodes if nid not in self.down_nodes]
|
|
if len(node_ids) < 2:
|
|
return
|
|
|
|
# Pick random client and server (different nodes, both up)
|
|
client, server = self.rng.sample(node_ids, 2)
|
|
server_npub = self.topology.nodes[server].npub
|
|
container = self.topology.container_name(client)
|
|
|
|
duration = int(
|
|
self.rng.uniform(
|
|
self.config.duration_secs.min,
|
|
self.config.duration_secs.max,
|
|
)
|
|
)
|
|
streams = self.config.parallel_streams
|
|
|
|
# Result file inside the container for JSON capture
|
|
ts = int(time.time())
|
|
result_file = f"/tmp/iperf3-{client}-{server}-{ts}.json"
|
|
|
|
# Start iperf3 in background with JSON output
|
|
cmd = (
|
|
f"nohup iperf3 -c {server_npub}.fips -t {duration} "
|
|
f"-P {streams} --json > {result_file} 2>&1 &"
|
|
)
|
|
result = docker_exec_quiet(container, cmd)
|
|
if result is not None:
|
|
session = TrafficSession(
|
|
client_node=client,
|
|
server_node=server,
|
|
started_at=time.time(),
|
|
duration_secs=duration,
|
|
container=container,
|
|
result_file=result_file,
|
|
)
|
|
self.active_sessions.append(session)
|
|
log.info(
|
|
"Traffic: %s -> %s (%ds, %d streams)",
|
|
client,
|
|
server,
|
|
duration,
|
|
streams,
|
|
)
|
|
else:
|
|
log.warning("Failed to start iperf3 on %s", container)
|
|
|
|
def cleanup_expired(self):
|
|
"""Remove sessions that have completed (based on time)."""
|
|
now = time.time()
|
|
grace = 5 # seconds after expected completion
|
|
still_active = []
|
|
for s in self.active_sessions:
|
|
if now - s.started_at >= s.duration_secs + grace:
|
|
self._collect_result(s)
|
|
else:
|
|
still_active.append(s)
|
|
removed = len(self.active_sessions) - len(still_active)
|
|
self.active_sessions = still_active
|
|
if removed > 0:
|
|
log.debug("Cleaned up %d expired traffic sessions", removed)
|
|
|
|
def _collect_result(self, session: TrafficSession):
|
|
"""Retrieve iperf3 JSON result from container."""
|
|
if not session.result_file:
|
|
return
|
|
if session.client_node in self.down_nodes:
|
|
return
|
|
stdout = docker_exec_quiet(
|
|
session.container,
|
|
f"cat {session.result_file} 2>/dev/null; rm -f {session.result_file}",
|
|
)
|
|
if stdout is None:
|
|
return
|
|
try:
|
|
data = json.loads(stdout.strip())
|
|
except (json.JSONDecodeError, ValueError):
|
|
log.debug("Could not parse iperf3 result for %s -> %s",
|
|
session.client_node, session.server_node)
|
|
return
|
|
data["_meta"] = {
|
|
"client": session.client_node,
|
|
"server": session.server_node,
|
|
"duration_secs": session.duration_secs,
|
|
}
|
|
self.completed_results.append(data)
|
|
|
|
def collect_results(self) -> list[dict]:
|
|
"""Return all completed iperf3 JSON results."""
|
|
# Collect any remaining active sessions that may have finished
|
|
for s in self.active_sessions:
|
|
self._collect_result(s)
|
|
return self.completed_results
|
|
|
|
def stop_all(self):
|
|
"""Kill all iperf3 client processes in running containers."""
|
|
seen = set()
|
|
for session in self.active_sessions:
|
|
if session.container not in seen:
|
|
if session.client_node not in self.down_nodes:
|
|
docker_exec_quiet(
|
|
session.container,
|
|
"killall iperf3 2>/dev/null; true",
|
|
)
|
|
seen.add(session.container)
|
|
# Collect results before clearing (iperf3 writes partial JSON on kill)
|
|
for s in self.active_sessions:
|
|
self._collect_result(s)
|
|
self.active_sessions.clear()
|