mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-12 09:33:23 +00:00
fipstop: ratatui-based TUI for real-time monitoring of a running FIPS daemon. Tabs and navigation: - 8 navigable tabs: Node, Peers, Transports, Sessions, Tree, Filters, Performance, Routing - Tab/BackTab navigation with group separators in tab bar - Table views with selectable rows, detail drill-down panels, and scrollbars Node tab: - Runtime info: pid, exe path, uptime, control socket path, TUN adapter name - Identity: npub, node_addr, ipv6 address - State summary with peer/session/link/transport/connection counts - TUN IPv6 traffic and forwarded transit traffic counters Peers tab: - Table with Name, Address, Conn, Depth, SRTT, Loss, LQI, Pkts Tx/Rx - Detail panel: identity, connection info, transport cross-reference, tree/bloom state, link stats, MMP metrics with LQI Sessions tab: - Table with Name, Remote Addr, State, Role, SRTT, Loss, SQI, Path MTU, Last Activity - Detail panel: identity, session info, traffic stats, MMP metrics with SQI Transports tab: - Hierarchical tree view: expandable transport parents with nested links (▼/▶ indicators, ├─/└─ tree chars, Space/Arrow to expand/collapse) - Transport detail: type-specific stats (UDP/TCP/Ethernet) - Link detail: peer cross-reference with MMP metrics and LQI Performance tab: - Link-layer MMP: SRTT, loss, ETX, LQI, goodput per peer - Session-layer MMP: SRTT, loss, ETX, SQI, path MTU per session - Trend indicators (rising/falling/stable) with context-aware coloring Routing tab: - Routing state: cache sizes, pending lookups, recent requests - Coordinate cache: entries, fill ratio, TTL, expiry, avg age - Statistics: forwarding, discovery request/response, error signal counters Tree tab: - Spanning tree position with 16 announce stats (inbound/outbound/cumulative) Filters tab: - Bloom filter announce stats, per-peer fill ratio and estimated node count MMP metrics enhancements: - Add etx_trend DualEwma for smoothed ETX tracking - Add smoothed_loss() and smoothed_etx() accessors (long-term EWMA) - LQI (Link Quality Index) = smoothed_etx * (1 + srtt_ms / 100) - SQI (Session Quality Index) = same formula for session layer - All loss/ETX displays prefer smoothed values with raw fallback Control socket: - Add smoothed_loss, smoothed_etx, lqi/sqi to show_peers, show_sessions, and show_mmp JSON responses - Rename fips_address to ipv6_addr in show_status and show_peers - Add tun_name and control_socket to show_status - FHS-compliant 3-tier default path: $XDG_RUNTIME_DIR, /run/fips, /tmp Node extensions: - Add started_at/uptime() to Node - Add tun_name() accessor Docker sidecar updates: - TCP transport support via FIPS_PEER_TRANSPORT env var - Build scripts include fipstop binary
104 lines
3.3 KiB
Python
104 lines
3.3 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
|