Files
fips/src/bin/fipstop/ui/routing.rs
T
Johnathan Corgan 7ccc7adab0 Merge maint into master, carrying the twenty-one security fixes
Not a replay: master had independently reorganized or reimplemented nearly
every area the batch touches, so most of this is re-derivation. Four files
maint modified do not exist on master at all, and git marks those DU with no
conflict markers, so the lazy resolution would have dropped 499 lines while
the tree still built and still passed tests.

Two conflicts turned out to be the same defect already present on master in a
different place, and fixing it there reaches further than the original did.
Master had centralized socket binding into sockbind across three sockets, one
of which maint does not have, with the same create-wide-then-narrow window; and
master's shared PerAddrRateLimiter swept the whole map on every admit with no
entry ceiling, which is the routing-error limiter finding in a primitive the
lookup-forward limiter also uses.

Two others would have added dead code if replayed. Master had already deleted
the pre-refactor restart block the epoch dampener patched, so it went onto
InboundDecision::RestartThenPromote instead; and master already carried the
relocated reactive-MTU floor check, so the copy inside the conflict was the old
post-apply one that had been removed.

The lookup dedup eviction was re-derived into the hoisted lookup core, and its
target-side signing gate re-added to the request path, where the merge had
dropped it entirely and left the limiter inert.

Two regression tests were repaired rather than accepted. The gap-tracker test
adapted into master's ReceiverState style asserted an empty burst after a jump
to the ceiling counter, which is not what that code does; and the epoch
dampener's stamp-on-acceptance ordering was pinned by nothing, so a sustained
replay could have starved a restarting peer with every test still green. Both
now red when the fix they guard is reverted.

Gate: fmt, build, clippy -D warnings, 2273 tests passing, six repo guards on
the committed tree.
2026-08-23 15:49:39 +01:00

381 lines
14 KiB
Rust

use ratatui::Frame;
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::style::{Color, Style};
use ratatui::text::Line;
use ratatui::widgets::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);
let focused = app.focused_pane();
draw_routing_state(frame, data, app.pane_scroll(0), focused == 0, chunks[0]);
draw_coord_cache(frame, app, app.pane_scroll(1), focused == 1, chunks[1]);
draw_routing_stats(frame, data, app.pane_scroll(2), focused == 2, chunks[2]);
}
fn draw_routing_state(
frame: &mut Frame,
data: &serde_json::Value,
scroll: u16,
focused: bool,
area: Rect,
) {
let lines = helpers::kv_lines(&[
(
"Coord Cache",
helpers::u64_field(data, "coord_cache_entries"),
),
(
"Identity Cache",
helpers::u64_field(data, "identity_cache_entries"),
),
(
"Pending Lookups",
data.get("pending_lookups")
.and_then(|v| v.as_array())
.map(|a| a.len().to_string())
.unwrap_or_else(|| "0".into()),
),
(
"Recent Requests",
helpers::u64_field(data, "recent_requests"),
),
// Not a drop: the frame is still delivered or forwarded, only the
// coordinate-cache warm attempt was abandoned. It belongs here beside
// the cache it failed to warm, not in the Dropped section.
(
"Warm Malformed",
fwd_value(data, "warm_malformed_packets", "warm_malformed_bytes"),
),
]);
let block = helpers::pane_block(" Routing State ", focused);
let inner = block.inner(area);
frame.render_widget(block, area);
let scroll = helpers::clamp_scroll(scroll, lines.len(), inner.height as usize);
frame.render_widget(Paragraph::new(lines).scroll((scroll, 0)), inner);
}
/// Format a forwarding counter as "N pkts (formatted_bytes)".
fn fwd_value(data: &serde_json::Value, pkt_key: &str, byte_key: &str) -> String {
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);
format!("{} pkts ({})", pkts, helpers::format_bytes(bytes))
}
/// Read a raw forwarding counter as a u64 (0 if missing), for arithmetic
/// (percentages, derived totals) that the string-returning helpers can't do.
fn fwd_count(data: &serde_json::Value, key: &str) -> u64 {
data.get("forwarding")
.and_then(|f| f.get(key))
.and_then(|v| v.as_u64())
.unwrap_or(0)
}
/// Total mesh egress = locally-originated + transit-forwarded, formatted as
/// "N pkts (B)". There is no single daemon counter for everything this node
/// transmits to peers, so it is derived from its two contributors.
fn mesh_tx_value(data: &serde_json::Value) -> String {
let pkts = fwd_count(data, "originated_packets") + fwd_count(data, "forwarded_packets");
let bytes = fwd_count(data, "originated_bytes") + fwd_count(data, "forwarded_bytes");
format!("{} pkts ({})", pkts, helpers::format_bytes(bytes))
}
/// Format a route-class count as "N (xx.x%)" where the percentage is the class's
/// share of total forwarded (transit) packets. Zero forwarded yields "0.0%".
fn route_class_value(count: u64, total_forwarded: u64) -> String {
let pct = if total_forwarded > 0 {
count as f64 / total_forwarded as f64 * 100.0
} else {
0.0
};
format!("{count} ({pct:.1}%)")
}
/// Build a section: a styled header line followed by the kv pairs rendered
/// through the group helper so the section's values share a left edge.
fn section(title: &str, pairs: &[(&str, String)]) -> Vec<Line<'static>> {
let mut out = vec![helpers::section_header(title)];
out.extend(helpers::kv_lines(pairs));
out
}
fn draw_routing_stats(
frame: &mut Frame,
data: &serde_json::Value,
scroll: u16,
focused: bool,
area: Rect,
) {
let block = helpers::pane_block(" Routing Statistics ", focused);
let inner = block.inner(area);
frame.render_widget(block, area);
let cols =
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]).split(inner);
// Shorthand for a nested counter value (e.g. lookup.req_received).
let lookup = |key: &str| helpers::nested_u64(data, "lookup", key);
let err = |key: &str| helpers::nested_u64(data, "error_signals", key);
let cong = |key: &str| helpers::nested_u64(data, "congestion", key);
// The node is an interface adapter between the local host stack and the
// mesh; the left column reads each side as a Transmitted/Received pair.
//
// Local Stack — traffic crossing the TUN / local-origination boundary:
// Transmitted is what the host injects into the mesh (originated), Received
// is what the mesh hands up to the host (delivered).
let mut left = section(
"Local Stack",
&[
(
"Transmitted",
fwd_value(data, "originated_packets", "originated_bytes"),
),
(
"Received",
fwd_value(data, "delivered_packets", "delivered_bytes"),
),
],
);
left.push(Line::from(""));
// Mesh — traffic crossing the peer-link boundary: Transmitted is everything
// this node puts on the wire (originated + forwarded, derived), Received is
// the ingress aggregate from peers (own-delivered + transit + drops).
left.extend(section(
"Mesh",
&[
("Transmitted", mesh_tx_value(data)),
(
"Received",
fwd_value(data, "received_packets", "received_bytes"),
),
],
));
left.push(Line::from(""));
left.extend(section(
"Lookup Requests",
&[
("Received", lookup("req_received")),
("Forwarded", lookup("req_forwarded")),
("Initiated", lookup("req_initiated")),
("Deduplicated", lookup("req_deduplicated")),
("Target Is Us", lookup("req_target_is_us")),
("Duplicate", lookup("req_duplicate")),
("Bloom Miss", lookup("req_bloom_miss")),
("Backoff Suppressed", lookup("req_backoff_suppressed")),
("Fwd Rate Limited", lookup("req_forward_rate_limited")),
("Sign Rate Limited", lookup("req_sign_rate_limited")),
("Dedup Evicted", lookup("req_dedup_evicted")),
("TTL Exhausted", lookup("req_ttl_exhausted")),
("Decode Error", lookup("req_decode_error")),
],
));
left.push(Line::from(""));
left.extend(section(
"Lookup Responses",
&[
("Received", lookup("resp_received")),
("Accepted", lookup("resp_accepted")),
("Forwarded", lookup("resp_forwarded")),
("Timed Out", lookup("resp_timed_out")),
("Identity Miss", lookup("resp_identity_miss")),
("Proof Failed", lookup("resp_proof_failed")),
("Unsolicited", lookup("resp_unsolicited")),
("Decode Error", lookup("resp_decode_error")),
],
));
// Right column — "Forwarded" (transit / routed through this node).
// Forwarded total, then the route-class breakdown (a percentage partition
// of the total), then the transit-path drop reasons.
let fwd_total = fwd_count(data, "forwarded_packets");
let mut right = section(
"Forwarded",
&[(
"Forwarded",
fwd_value(data, "forwarded_packets", "forwarded_bytes"),
)],
);
// Blank separator after the Forwarded total, matching the spacing between
// every other section pair; the total and its route-class breakdown read
// as two distinct groups.
right.push(Line::from(""));
// Route-class breakdown: a partition of Forwarded, each line annotated with
// its share of the total. Tree-down cross — the dive-to-tree-child
// cut-through — is the last class; Tree-down + Tree-down cross sum to the
// pre-split tree-down total.
right.extend(section(
"Route Class",
&[
(
"Direct Peer",
route_class_value(fwd_count(data, "route_direct_peer"), fwd_total),
),
(
"Tree-down",
route_class_value(fwd_count(data, "route_tree_down"), fwd_total),
),
(
"Tree-up",
route_class_value(fwd_count(data, "route_tree_up"), fwd_total),
),
(
"Cross-link descend",
route_class_value(fwd_count(data, "route_crosslink_descend"), fwd_total),
),
(
"Cross-link ascend",
route_class_value(fwd_count(data, "route_crosslink_ascend"), fwd_total),
),
(
"Tree-down cross",
route_class_value(fwd_count(data, "route_tree_down_cross"), fwd_total),
),
],
));
right.push(Line::from(""));
right.extend(section(
"Dropped",
&[
(
"No Route",
fwd_value(data, "drop_no_route_packets", "drop_no_route_bytes"),
),
(
"TTL Exhausted",
fwd_value(data, "ttl_exhausted_packets", "ttl_exhausted_bytes"),
),
(
"Decode Error",
fwd_value(data, "decode_error_packets", "decode_error_bytes"),
),
(
"MTU Exceeded",
fwd_value(data, "drop_mtu_exceeded_packets", "drop_mtu_exceeded_bytes"),
),
(
"Send Error",
fwd_value(data, "drop_send_error_packets", "drop_send_error_bytes"),
),
],
));
right.push(Line::from(""));
right.extend(section(
"Error Signals",
&[
("Coords Required", err("coords_required")),
("Path Broken", err("path_broken")),
("MTU Exceeded", err("mtu_exceeded")),
("PMTU Notif < Floor", err("path_mtu_notif_below_floor")),
("MTU Exceeded < Floor", err("mtu_exceeded_below_floor")),
("Lookup PMTU < Floor", err("lookup_resp_mtu_below_floor")),
("Coords Required Refused", err("unbound_coords")),
("Path Broken Refused", err("unbound_broken")),
("MTU Exceeded Refused", err("unbound_mtu")),
("Forged Pairing", err("unbound_forged")),
("Emit Over Peer Budget", err("emit_over_peer_budget")),
("Emit Over Dest Interval", err("emit_over_dest_interval")),
("Emit Limiter At Capacity", err("emit_limiter_at_capacity")),
(
"MTU Exceeded Uncorroborated",
err("mtu_exceeded_uncorroborated"),
),
],
));
right.push(Line::from(""));
right.extend(section(
"Congestion",
&[
("CE Forwarded", cong("ce_forwarded")),
("CE Received", cong("ce_received")),
("Congestion Detected", cong("congestion_detected")),
("Kernel Drops", cong("kernel_drop_events")),
],
));
// Both columns scroll together under the focused-pane offset, clamped to
// the taller column so neither over-scrolls past its content.
let visible = cols[0].height as usize;
let content = left.len().max(right.len());
let scroll = helpers::clamp_scroll(scroll, content, visible);
frame.render_widget(Paragraph::new(left).scroll((scroll, 0)), cols[0]);
frame.render_widget(Paragraph::new(right).scroll((scroll, 0)), cols[1]);
}
fn draw_coord_cache(frame: &mut Frame, app: &App, scroll: u16, focused: bool, area: Rect) {
let data = match app.data.get(&Tab::Cache) {
Some(d) => d,
None => {
let block = helpers::pane_block(" Coordinate Cache ", focused);
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 = helpers::kv_lines(&[
("Entries", format!("{entries} / {max_entries}")),
("Fill Ratio", fill_pct),
("Default TTL", ttl),
("Expired", expired),
("Avg Age", avg_age),
]);
let block = helpers::pane_block(" Coordinate Cache ", focused);
let inner = block.inner(area);
frame.render_widget(block, area);
let scroll = helpers::clamp_scroll(scroll, lines.len(), inner.height as usize);
frame.render_widget(Paragraph::new(lines).scroll((scroll, 0)), inner);
}