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
143 lines
4.8 KiB
Python
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
|