Add fipstop TUI monitoring tool with smoothed metrics and quality indices

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
This commit is contained in:
Johnathan Corgan
2026-03-01 16:33:33 +00:00
parent 71a5c68fa9
commit 77ac8c822e
33 changed files with 4122 additions and 46 deletions
+4 -2
View File
@@ -263,11 +263,12 @@ impl Node {
}
}
// Only application data resets the idle timer — MMP reports
// (SenderReport, ReceiverReport, PathMtuNotification) do not.
// Only application data resets the idle timer and traffic counters —
// MMP reports (SenderReport, ReceiverReport, PathMtuNotification) do not.
if msg_type == SessionMessageType::DataPacket.to_byte()
&& let Some(entry) = self.sessions.get_mut(src_addr)
{
entry.record_recv(rest.len());
entry.touch(Self::now_ms());
}
@@ -1026,6 +1027,7 @@ impl Node {
// Re-borrow after send (which borrowed &mut self)
if let Some(entry) = self.sessions.get_mut(dest_addr) {
entry.record_sent(plaintext.len());
if let Some(mmp) = entry.mmp_mut() {
mmp.sender.record_sent(counter, timestamp, ciphertext.len());
}
+15
View File
@@ -216,6 +216,9 @@ pub struct Node {
/// Exchanged inside Noise handshake messages so peers can detect restarts.
startup_epoch: [u8; 8],
/// Instant when the node was created, for uptime reporting.
started_at: std::time::Instant,
// === Configuration ===
/// Loaded configuration.
config: Config,
@@ -415,6 +418,7 @@ impl Node {
Ok(Self {
identity,
startup_epoch,
started_at: std::time::Instant::now(),
config,
state: NodeState::Created,
is_leaf_only,
@@ -509,6 +513,7 @@ impl Node {
Self {
identity,
startup_epoch,
started_at: std::time::Instant::now(),
config,
state: NodeState::Created,
is_leaf_only: false,
@@ -764,6 +769,11 @@ impl Node {
self.state
}
/// Get the node uptime.
pub fn uptime(&self) -> std::time::Duration {
self.started_at.elapsed()
}
/// Check if node is operational.
pub fn is_running(&self) -> bool {
self.state.is_operational()
@@ -829,6 +839,11 @@ impl Node {
self.tun_state
}
/// Get the TUN interface name, if active.
pub fn tun_name(&self) -> Option<&str> {
self.tun_name.as_deref()
}
// === Resource Limits ===
+33
View File
@@ -73,6 +73,16 @@ pub(crate) struct SessionEntry {
/// Session-layer MMP state. Initialized on Established transition.
mmp: Option<MmpSessionState>,
// === Traffic Counters ===
/// Total data packets sent on this session.
packets_sent: u64,
/// Total data packets received on this session.
packets_recv: u64,
/// Total data bytes sent on this session (FSP payload).
bytes_sent: u64,
/// Total data bytes received on this session (FSP payload).
bytes_recv: u64,
// === Handshake Resend ===
/// Encoded session-layer payload for resend (SessionSetup or SessionAck).
/// Cleared on Established transition.
@@ -102,6 +112,10 @@ impl SessionEntry {
coords_warmup_remaining: 0,
is_initiator,
mmp: None,
packets_sent: 0,
packets_recv: 0,
bytes_sent: 0,
bytes_recv: 0,
handshake_payload: None,
resend_count: 0,
next_resend_at_ms: 0,
@@ -219,6 +233,25 @@ impl SessionEntry {
self.mmp = Some(MmpSessionState::new(config, self.is_initiator));
}
// === Traffic Counters ===
/// Record a sent data packet.
pub(crate) fn record_sent(&mut self, bytes: usize) {
self.packets_sent += 1;
self.bytes_sent += bytes as u64;
}
/// Record a received data packet.
pub(crate) fn record_recv(&mut self, bytes: usize) {
self.packets_recv += 1;
self.bytes_recv += bytes as u64;
}
/// Get traffic counters: (packets_sent, packets_recv, bytes_sent, bytes_recv).
pub(crate) fn traffic_counters(&self) -> (u64, u64, u64, u64) {
(self.packets_sent, self.packets_recv, self.bytes_sent, self.bytes_recv)
}
// === Handshake Resend ===
/// Store the encoded session-layer payload for potential resend.