Files
fips/src/bin/fipstop/ui/routing.rs
Tim O'Shea 2d342a4e47 Add diagnostic queries for security validation and mesh debugging
Extend the fipsctl control query interface with visibility into internal
state critical for protocol security auditing, mesh troubleshooting, and
operational monitoring.

New command:

  fipsctl show identity-cache

    Lists every node identity cached by the daemon (learned from DNS
    resolution, peer handshakes, sessions, and static config).  Shows
    npub, IPv6 address, display name, and LRU age alongside the
    configured cache capacity.

Extended queries:

  show peers — Noise session counters (send_counter, highest received
    counter) for rekey urgency assessment.  Per-peer replay suppression
    and consecutive decrypt failure counts for active attack detection.
    Session index visibility for hijack analysis.  Rekey lifecycle
    state (in_progress, draining, K-bit epoch).

  show sessions — Handshake resend count during establishment for
    connectivity debugging.  Rekey and session health fields
    (session_start, K-bit, coords warmup, drain state) when
    established.

  show cache — Individual coordinate cache entries with tree
    coordinates, depth, path MTU, and age.  Enables route-level
    debugging by showing exactly which destinations have cached
    routes and via what tree path.  Renames the top-level count
    field from "entries" to "count" for clarity.

  show routing — Pending discovery lookups expanded from count to
    per-target detail (attempt number, age, last sent).  Pending
    TUN packet queue depth for backpressure visibility.  Connection
    retry state per peer (retry count, next attempt, auto-reconnect
    flag).

Updates fipstop to match the revised show_cache and show_routing
response schemas.  Updates README monitoring section with the complete
fipsctl command list.

Co-authored-by: Johnathan Corgan <johnathan@corganlabs.com>
2026-04-13 17:34:01 +00:00

302 lines
9.6 KiB
Rust

use ratatui::Frame;
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::style::{Color, Style};
use ratatui::text::Line;
use ratatui::widgets::{Block, Borders, Paragraph};
use crate::app::{App, Tab};
use super::helpers;
pub fn draw(frame: &mut Frame, app: &App, area: Rect) {
let data = match app.data.get(&Tab::Routing) {
Some(d) => d,
None => {
let msg =
Paragraph::new(" Waiting for data...").style(Style::default().fg(Color::DarkGray));
frame.render_widget(msg, area);
return;
}
};
let chunks = Layout::vertical([
Constraint::Length(7), // Routing State
Constraint::Length(8), // Coordinate Cache
Constraint::Min(3), // Routing Statistics
])
.split(area);
draw_routing_state(frame, data, chunks[0]);
draw_coord_cache(frame, app, chunks[1]);
draw_routing_stats(frame, data, chunks[2]);
}
fn draw_routing_state(frame: &mut Frame, data: &serde_json::Value, area: Rect) {
let lines = vec![
helpers::kv_line(
"Coord Cache",
&helpers::u64_field(data, "coord_cache_entries"),
),
helpers::kv_line(
"Identity Cache",
&helpers::u64_field(data, "identity_cache_entries"),
),
helpers::kv_line(
"Pending Lookups",
&data
.get("pending_lookups")
.and_then(|v| v.as_array())
.map(|a| a.len().to_string())
.unwrap_or_else(|| "0".into()),
),
helpers::kv_line(
"Recent Requests",
&helpers::u64_field(data, "recent_requests"),
),
];
let block = Block::default()
.borders(Borders::ALL)
.title(" Routing State ");
let inner = block.inner(area);
frame.render_widget(block, area);
frame.render_widget(Paragraph::new(lines), inner);
}
/// Format a forwarding counter as "N pkts (formatted_bytes)".
fn fwd_line(data: &serde_json::Value, label: &str, pkt_key: &str, byte_key: &str) -> Line<'static> {
let pkts = data
.get("forwarding")
.and_then(|f| f.get(pkt_key))
.and_then(|v| v.as_u64())
.unwrap_or(0);
let bytes = data
.get("forwarding")
.and_then(|f| f.get(byte_key))
.and_then(|v| v.as_u64())
.unwrap_or(0);
helpers::kv_line(
label,
&format!("{} pkts ({})", pkts, helpers::format_bytes(bytes)),
)
}
fn draw_routing_stats(frame: &mut Frame, data: &serde_json::Value, area: Rect) {
let block = Block::default()
.borders(Borders::ALL)
.title(" Routing Statistics ");
let inner = block.inner(area);
frame.render_widget(block, area);
let cols =
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]).split(inner);
// Left column: Forwarding + Discovery
let mut left = vec![
helpers::section_header("Forwarding"),
fwd_line(data, "Received", "received_packets", "received_bytes"),
fwd_line(data, "Delivered", "delivered_packets", "delivered_bytes"),
fwd_line(data, "Forwarded", "forwarded_packets", "forwarded_bytes"),
fwd_line(data, "Originated", "originated_packets", "originated_bytes"),
fwd_line(
data,
"Decode Error",
"decode_error_packets",
"decode_error_bytes",
),
fwd_line(
data,
"TTL Exhausted",
"ttl_exhausted_packets",
"ttl_exhausted_bytes",
),
fwd_line(
data,
"No Route",
"drop_no_route_packets",
"drop_no_route_bytes",
),
fwd_line(
data,
"MTU Exceeded",
"drop_mtu_exceeded_packets",
"drop_mtu_exceeded_bytes",
),
fwd_line(
data,
"Send Error",
"drop_send_error_packets",
"drop_send_error_bytes",
),
Line::from(""),
helpers::section_header("Discovery Requests"),
helpers::kv_line(
"Received",
&helpers::nested_u64(data, "discovery", "req_received"),
),
helpers::kv_line(
"Forwarded",
&helpers::nested_u64(data, "discovery", "req_forwarded"),
),
helpers::kv_line(
"Initiated",
&helpers::nested_u64(data, "discovery", "req_initiated"),
),
helpers::kv_line(
"Deduplicated",
&helpers::nested_u64(data, "discovery", "req_deduplicated"),
),
helpers::kv_line(
"Target Is Us",
&helpers::nested_u64(data, "discovery", "req_target_is_us"),
),
helpers::kv_line(
"Duplicate",
&helpers::nested_u64(data, "discovery", "req_duplicate"),
),
helpers::kv_line(
"Bloom Miss",
&helpers::nested_u64(data, "discovery", "req_bloom_miss"),
),
helpers::kv_line(
"Backoff Suppressed",
&helpers::nested_u64(data, "discovery", "req_backoff_suppressed"),
),
helpers::kv_line(
"Fwd Rate Limited",
&helpers::nested_u64(data, "discovery", "req_forward_rate_limited"),
),
helpers::kv_line(
"TTL Exhausted",
&helpers::nested_u64(data, "discovery", "req_ttl_exhausted"),
),
helpers::kv_line(
"Decode Error",
&helpers::nested_u64(data, "discovery", "req_decode_error"),
),
Line::from(""),
helpers::section_header("Discovery Responses"),
helpers::kv_line(
"Received",
&helpers::nested_u64(data, "discovery", "resp_received"),
),
helpers::kv_line(
"Accepted",
&helpers::nested_u64(data, "discovery", "resp_accepted"),
),
helpers::kv_line(
"Forwarded",
&helpers::nested_u64(data, "discovery", "resp_forwarded"),
),
helpers::kv_line(
"Timed Out",
&helpers::nested_u64(data, "discovery", "resp_timed_out"),
),
helpers::kv_line(
"Identity Miss",
&helpers::nested_u64(data, "discovery", "resp_identity_miss"),
),
helpers::kv_line(
"Proof Failed",
&helpers::nested_u64(data, "discovery", "resp_proof_failed"),
),
helpers::kv_line(
"Decode Error",
&helpers::nested_u64(data, "discovery", "resp_decode_error"),
),
];
// Right column: Error Signals + Congestion
let mut right = vec![
helpers::section_header("Error Signals"),
helpers::kv_line(
"Coords Required",
&helpers::nested_u64(data, "error_signals", "coords_required"),
),
helpers::kv_line(
"Path Broken",
&helpers::nested_u64(data, "error_signals", "path_broken"),
),
helpers::kv_line(
"MTU Exceeded",
&helpers::nested_u64(data, "error_signals", "mtu_exceeded"),
),
Line::from(""),
helpers::section_header("Congestion"),
helpers::kv_line(
"CE Forwarded",
&helpers::nested_u64(data, "congestion", "ce_forwarded"),
),
helpers::kv_line(
"CE Received",
&helpers::nested_u64(data, "congestion", "ce_received"),
),
helpers::kv_line(
"Congestion Detected",
&helpers::nested_u64(data, "congestion", "congestion_detected"),
),
helpers::kv_line(
"Kernel Drops",
&helpers::nested_u64(data, "congestion", "kernel_drop_events"),
),
];
let max_lines = cols[0].height as usize;
left.truncate(max_lines);
right.truncate(max_lines);
frame.render_widget(Paragraph::new(left), cols[0]);
frame.render_widget(Paragraph::new(right), cols[1]);
}
fn draw_coord_cache(frame: &mut Frame, app: &App, area: Rect) {
let data = match app.data.get(&Tab::Cache) {
Some(d) => d,
None => {
let block = Block::default()
.borders(Borders::ALL)
.title(" Coordinate Cache ");
let inner = block.inner(area);
frame.render_widget(block, area);
let msg =
Paragraph::new(" Waiting for data...").style(Style::default().fg(Color::DarkGray));
frame.render_widget(msg, inner);
return;
}
};
let entries = helpers::u64_field(data, "count");
let max_entries = helpers::u64_field(data, "max_entries");
let fill_pct = data
.get("fill_ratio")
.and_then(|v| v.as_f64())
.map(|r| format!("{:.1}%", r * 100.0))
.unwrap_or_else(|| "-".into());
let ttl = data
.get("default_ttl_ms")
.and_then(|v| v.as_u64())
.map(helpers::format_duration_ms)
.unwrap_or_else(|| "-".into());
let expired = helpers::u64_field(data, "expired");
let avg_age = data
.get("avg_age_ms")
.and_then(|v| v.as_u64())
.map(helpers::format_duration_ms)
.unwrap_or_else(|| "-".into());
let lines = vec![
helpers::kv_line("Entries", &format!("{entries} / {max_entries}")),
helpers::kv_line("Fill Ratio", &fill_pct),
helpers::kv_line("Default TTL", &ttl),
helpers::kv_line("Expired", &expired),
helpers::kv_line("Avg Age", &avg_age),
];
let block = Block::default()
.borders(Borders::ALL)
.title(" Coordinate Cache ");
let inner = block.inner(area);
frame.render_widget(block, area);
frame.render_widget(Paragraph::new(lines), inner);
}