Files
fips/testing/chaos/sim/logs.py
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

177 lines
6.7 KiB
Python

"""Log collection and post-run analysis."""
from __future__ import annotations
import os
import re
import subprocess
import logging
from dataclasses import dataclass, field
log = logging.getLogger(__name__)
# Regex to strip ANSI escape codes from tracing output
_ANSI_RE = re.compile(r"\x1b\[[0-9;]*m")
@dataclass
class AnalysisResult:
errors: list[tuple[str, str]] = field(default_factory=list)
warnings: list[tuple[str, str]] = field(default_factory=list)
sessions_established: list[tuple[str, str]] = field(default_factory=list)
peers_promoted: list[tuple[str, str]] = field(default_factory=list)
peer_removals: list[tuple[str, str]] = field(default_factory=list)
parent_switches: list[tuple[str, str]] = field(default_factory=list)
mmp_link_metrics: list[tuple[str, str]] = field(default_factory=list)
mmp_session_metrics: list[tuple[str, str]] = field(default_factory=list)
handshake_timeouts: list[tuple[str, str]] = field(default_factory=list)
panics: list[tuple[str, str]] = field(default_factory=list)
congestion_detected: list[tuple[str, str]] = field(default_factory=list)
kernel_drop_events: list[tuple[str, str]] = field(default_factory=list)
def summary(self) -> str:
lines = [
"=== Simulation Analysis ===",
"",
f"Panics: {len(self.panics)}",
f"Errors: {len(self.errors)}",
f"Warnings: {len(self.warnings)}",
f"Sessions established: {len(self.sessions_established)}",
f"Peers promoted: {len(self.peers_promoted)}",
f"Peer removals: {len(self.peer_removals)}",
f"Parent switches: {len(self.parent_switches)}",
f"Handshake timeouts: {len(self.handshake_timeouts)}",
f"MMP link samples: {len(self.mmp_link_metrics)}",
f"MMP session samples: {len(self.mmp_session_metrics)}",
f"Congestion events: {len(self.congestion_detected)}",
f"Kernel drop events: {len(self.kernel_drop_events)}",
]
if self.panics:
lines.append("")
lines.append("--- PANICS ---")
for container, line in self.panics[:10]:
lines.append(f" [{container}] {line.strip()}")
if self.errors:
lines.append("")
lines.append("--- ERRORS (first 20) ---")
for container, line in self.errors[:20]:
lines.append(f" [{container}] {line.strip()}")
if self.handshake_timeouts:
lines.append("")
lines.append("--- HANDSHAKE TIMEOUTS (first 10) ---")
for container, line in self.handshake_timeouts[:10]:
lines.append(f" [{container}] {line.strip()}")
lines.append("")
return "\n".join(lines)
def collect_logs(container_names: list[str], output_dir: str) -> dict[str, str]:
"""Collect all output (stdout + stderr) from all containers."""
os.makedirs(output_dir, exist_ok=True)
logs = {}
for name in container_names:
try:
result = subprocess.run(
["docker", "logs", name],
capture_output=True,
text=True,
timeout=30,
)
# Combine stdout and stderr — tracing may go to either
# depending on the subscriber configuration.
# Strip ANSI escape codes for clean log files.
raw = result.stdout + result.stderr
log_text = _ANSI_RE.sub("", raw)
logs[name] = log_text
path = os.path.join(output_dir, f"{name}.log")
with open(path, "w") as f:
f.write(log_text)
except (subprocess.TimeoutExpired, Exception) as e:
log.warning("Failed to collect logs from %s: %s", name, e)
logs[name] = ""
return logs
def analyze_logs(logs: dict[str, str]) -> AnalysisResult:
"""Parse structured tracing output and categorize events."""
result = AnalysisResult()
for container, log_text in logs.items():
for raw_line in log_text.splitlines():
# Strip ANSI escape codes for reliable matching
line = _ANSI_RE.sub("", raw_line)
# Panics
if "panicked" in line or "PANIC" in line:
result.panics.append((container, line))
# Errors and warnings
elif " ERROR " in line:
result.errors.append((container, line))
elif " WARN " in line:
result.warnings.append((container, line))
# Session establishment
if "Session established" in line:
result.sessions_established.append((container, line))
# Peer promotion
if "Inbound peer promoted" in line or "Outbound handshake completed" in line:
result.peers_promoted.append((container, line))
# Peer removal
if "Peer removed" in line:
result.peer_removals.append((container, line))
# Parent switches
if "Parent switched" in line:
result.parent_switches.append((container, line))
# Handshake timeouts
if "timed out" in line and ("handshake" in line.lower() or "Handshake" in line):
result.handshake_timeouts.append((container, line))
# MMP metrics
if "MMP link metrics" in line:
result.mmp_link_metrics.append((container, line))
if "MMP session metrics" in line:
result.mmp_session_metrics.append((container, line))
# Congestion events
if "Congestion detected" in line:
result.congestion_detected.append((container, line))
if "Kernel recv drops first observed" in line:
result.kernel_drop_events.append((container, line))
return result
def write_sim_metadata(
output_dir: str,
scenario_name: str,
seed: int,
num_nodes: int,
num_edges: int,
duration_secs: int,
topology=None,
):
"""Write simulation metadata for reproducibility."""
path = os.path.join(output_dir, "metadata.txt")
with open(path, "w") as f:
f.write(f"scenario: {scenario_name}\n")
f.write(f"seed: {seed}\n")
f.write(f"nodes: {num_nodes}\n")
f.write(f"edges: {num_edges}\n")
f.write(f"duration_secs: {duration_secs}\n")
if topology:
f.write("\nadjacency:\n")
for nid in sorted(topology.nodes):
node = topology.nodes[nid]
peers = sorted(node.peers)
f.write(f" {nid} ({node.docker_ip}): {', '.join(peers)}\n")
f.write("\nedges:\n")
for a, b in sorted(topology.edges):
f.write(f" {a} -- {b}\n")