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

143 lines
4.8 KiB
Python

"""Control socket querying via docker exec.
Queries FIPS nodes' control sockets to observe runtime state (tree
structure, MMP metrics, peers) without requiring fipsctl in the
container. Uses a Python one-liner inside docker exec since python3
is available in the Docker image.
"""
from __future__ import annotations
import json
import logging
from .docker_exec import docker_exec, docker_exec_quiet
from .topology import SimTopology
log = logging.getLogger(__name__)
# Default control socket path inside containers (running as root,
# so /run/fips/ is created by the daemon).
CONTROL_SOCKET = "/run/fips/control.sock"
def query_node(container: str, command: str, timeout: int = 10) -> dict | None:
"""Send a command to a node's control socket, return the data dict.
Returns None if the query fails (node down, socket not ready, etc.).
"""
# Python one-liner that connects to the Unix socket, sends the JSON
# command, and prints the response. Runs inside the container.
script = (
"import socket,json,sys; "
"s=socket.socket(socket.AF_UNIX,socket.SOCK_STREAM); "
f"s.connect('{CONTROL_SOCKET}'); "
f"s.sendall(json.dumps({{'command':'{command}'}}).encode()+b'\\n'); "
"s.shutdown(socket.SHUT_WR); "
"chunks=[]; "
"[chunks.append(d) for d in iter(lambda:s.recv(65536),b'')]; "
"print(b''.join(chunks).decode())"
)
stdout = docker_exec_quiet(container, f"python3 -c \"{script}\"", timeout=timeout)
if stdout is None:
return None
try:
response = json.loads(stdout.strip())
except json.JSONDecodeError as e:
log.warning("Invalid JSON from %s: %s", container, e)
return None
if response.get("status") != "ok":
msg = response.get("message", "unknown error")
log.warning("Control query %s on %s failed: %s", command, container, msg)
return None
return response.get("data", {})
def query_tree(container: str) -> dict | None:
"""Query a node's spanning tree state."""
return query_node(container, "show_tree")
def query_mmp(container: str) -> dict | None:
"""Query a node's MMP metrics."""
return query_node(container, "show_mmp")
def query_peers(container: str) -> dict | None:
"""Query a node's peer list with MMP metrics."""
return query_node(container, "show_peers")
def snapshot_all_trees(topology: SimTopology) -> dict[str, dict]:
"""Query show_tree on all nodes, return {node_id: tree_data}.
Nodes that fail to respond are omitted from the result.
"""
result = {}
for node_id in sorted(topology.nodes):
container = topology.container_name(node_id)
data = query_tree(container)
if data is not None:
result[node_id] = data
else:
log.warning("No tree data from %s", node_id)
return result
def snapshot_all_mmp(topology: SimTopology) -> dict[str, dict]:
"""Query show_mmp on all nodes, return {node_id: mmp_data}.
Nodes that fail to respond are omitted from the result.
"""
result = {}
for node_id in sorted(topology.nodes):
container = topology.container_name(node_id)
data = query_mmp(container)
if data is not None:
result[node_id] = data
else:
log.warning("No MMP data from %s", node_id)
return result
def query_routing(container: str) -> dict | None:
"""Query a node's routing stats (includes congestion counters)."""
return query_node(container, "show_routing")
def query_transports(container: str) -> dict | None:
"""Query a node's transport state (includes kernel drop counters)."""
return query_node(container, "show_transports")
def snapshot_all_congestion(topology: SimTopology) -> dict[str, dict]:
"""Query show_routing on all nodes to capture congestion counters.
Returns {node_id: {"congestion": {...}, "kernel_drops": [...]}}.
Nodes that fail to respond are omitted from the result.
"""
result = {}
for node_id in sorted(topology.nodes):
container = topology.container_name(node_id)
routing = query_routing(container)
transports = query_transports(container)
if routing is not None:
entry = {"congestion": routing.get("congestion", {})}
if transports is not None:
drops = []
for t in transports.get("transports", []):
stats = t.get("stats", {})
drops.append({
"transport_id": t.get("transport_id"),
"name": t.get("name"),
"kernel_drops": stats.get("kernel_drops"),
})
entry["kernel_drops"] = drops
result[node_id] = entry
else:
log.warning("No routing data from %s", node_id)
return result