mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-22 07:48:26 +00:00
Add an outbound-only Nym mixnet transport that tunnels FMP peer links through a local nym-socks5-client SOCKS5 proxy into the Nym mixnet. It structurally mirrors the Tor SOCKS5 transport (connection pool, connect-on-send background promotion, FMP-v0 framing reused from TCP) with the onion, inbound-listener, and control-port machinery removed. Wires the transport through the full TransportHandle dispatch, NymConfig (standard transport-instance pattern), and node instantiation, and surfaces its counters in fipstop. Includes a mock SOCKS5 harness and unit coverage for the address-parsing paths. Also adds an isolated single-container example (examples/sidecar-nostr-mixnet-relay/) demonstrating FIPS peering across the mixnet end to end. No new crate dependencies: tokio_socks, socket2, and futures are already pulled in by the Tor transport.
734 lines
26 KiB
Rust
734 lines
26 KiB
Rust
use ratatui::Frame;
|
|
use ratatui::layout::{Constraint, Layout, Rect};
|
|
use ratatui::style::{Color, Modifier, Style};
|
|
use ratatui::text::{Line, Span};
|
|
use ratatui::widgets::{
|
|
Block, Borders, Cell, Paragraph, Row, Scrollbar, ScrollbarOrientation, ScrollbarState, Table,
|
|
};
|
|
|
|
use crate::app::{App, SelectedTreeItem, Tab};
|
|
|
|
use super::helpers;
|
|
|
|
/// A single visible row in the tree view — either a transport or a nested link.
|
|
enum TreeRow {
|
|
Transport {
|
|
index: usize,
|
|
transport_id: u64,
|
|
link_count: usize,
|
|
},
|
|
Link {
|
|
index: usize,
|
|
is_last: bool,
|
|
},
|
|
}
|
|
|
|
pub fn draw(frame: &mut Frame, app: &mut App, area: Rect) {
|
|
let transports = get_transports(app);
|
|
let links = get_links(app);
|
|
let tree_rows = build_tree_rows(&transports, &links, app);
|
|
|
|
// Update app state for navigation
|
|
app.tree_row_count = tree_rows.len();
|
|
update_selected_tree_item(app, &tree_rows);
|
|
|
|
if app.detail_view.is_some() {
|
|
let chunks = Layout::horizontal([Constraint::Percentage(40), Constraint::Percentage(60)])
|
|
.split(area);
|
|
|
|
draw_table(frame, app, chunks[0], &transports, &links, &tree_rows);
|
|
draw_detail(frame, app, chunks[1], &transports, &links, &tree_rows);
|
|
} else {
|
|
draw_table(frame, app, area, &transports, &links, &tree_rows);
|
|
}
|
|
}
|
|
|
|
fn get_transports(app: &App) -> Vec<serde_json::Value> {
|
|
app.data
|
|
.get(&Tab::Transports)
|
|
.and_then(|v| v.get("transports"))
|
|
.and_then(|v| v.as_array())
|
|
.cloned()
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
fn get_links(app: &App) -> Vec<serde_json::Value> {
|
|
app.data
|
|
.get(&Tab::Links)
|
|
.and_then(|v| v.get("links"))
|
|
.and_then(|v| v.as_array())
|
|
.cloned()
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
fn build_tree_rows(
|
|
transports: &[serde_json::Value],
|
|
links: &[serde_json::Value],
|
|
app: &App,
|
|
) -> Vec<TreeRow> {
|
|
let mut rows = Vec::new();
|
|
for (t_idx, transport) in transports.iter().enumerate() {
|
|
let tid = transport
|
|
.get("transport_id")
|
|
.and_then(|v| v.as_u64())
|
|
.unwrap_or(0);
|
|
|
|
// Gather links belonging to this transport
|
|
let transport_links: Vec<(usize, &serde_json::Value)> = links
|
|
.iter()
|
|
.enumerate()
|
|
.filter(|(_, l)| l.get("transport_id").and_then(|v| v.as_u64()) == Some(tid))
|
|
.collect();
|
|
|
|
rows.push(TreeRow::Transport {
|
|
index: t_idx,
|
|
transport_id: tid,
|
|
link_count: transport_links.len(),
|
|
});
|
|
|
|
if app.expanded_transports.contains(&tid) {
|
|
let last_idx = transport_links.len().saturating_sub(1);
|
|
for (pos, (l_idx, _)) in transport_links.iter().enumerate() {
|
|
rows.push(TreeRow::Link {
|
|
index: *l_idx,
|
|
is_last: pos == last_idx,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
rows
|
|
}
|
|
|
|
fn update_selected_tree_item(app: &mut App, tree_rows: &[TreeRow]) {
|
|
let selected = app
|
|
.table_states
|
|
.get(&Tab::Transports)
|
|
.and_then(|s| s.selected())
|
|
.unwrap_or(0);
|
|
|
|
app.selected_tree_item = match tree_rows.get(selected) {
|
|
Some(TreeRow::Transport { transport_id, .. }) => SelectedTreeItem::Transport(*transport_id),
|
|
Some(TreeRow::Link { .. }) => SelectedTreeItem::Link,
|
|
None => SelectedTreeItem::None,
|
|
};
|
|
}
|
|
|
|
fn draw_table(
|
|
frame: &mut Frame,
|
|
app: &mut App,
|
|
area: Rect,
|
|
transports: &[serde_json::Value],
|
|
links: &[serde_json::Value],
|
|
tree_rows: &[TreeRow],
|
|
) {
|
|
let header = Row::new(vec![
|
|
Cell::from("Transport / Link"),
|
|
Cell::from("State"),
|
|
Cell::from("Peer"),
|
|
Cell::from("Tx"),
|
|
Cell::from("Rx"),
|
|
])
|
|
.style(
|
|
Style::default()
|
|
.fg(Color::Yellow)
|
|
.add_modifier(Modifier::BOLD),
|
|
);
|
|
|
|
let rows: Vec<Row> = tree_rows
|
|
.iter()
|
|
.map(|tree_row| match tree_row {
|
|
TreeRow::Transport {
|
|
index,
|
|
transport_id,
|
|
link_count,
|
|
} => {
|
|
let t = &transports[*index];
|
|
let indicator = if *link_count == 0 {
|
|
" "
|
|
} else if app.expanded_transports.contains(transport_id) {
|
|
"\u{25BC} " // ▼
|
|
} else {
|
|
"\u{25B6} " // ▶
|
|
};
|
|
let typ = helpers::str_field(t, "type");
|
|
let name = t.get("name").and_then(|v| v.as_str()).unwrap_or("");
|
|
let addr = t.get("local_addr").and_then(|v| v.as_str()).unwrap_or("");
|
|
let label = if !name.is_empty() {
|
|
format!("{indicator}{typ} {name}")
|
|
} else if typ == "tor" {
|
|
let mode = t
|
|
.get("tor_mode")
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or("socks5");
|
|
let onion_hint = t
|
|
.get("onion_address")
|
|
.and_then(|v| v.as_str())
|
|
.map(|a| {
|
|
let short = if a.len() > 16 { &a[..16] } else { a };
|
|
format!(" {short}..")
|
|
})
|
|
.unwrap_or_default();
|
|
format!("{indicator}tor({mode}){onion_hint}")
|
|
} else if !addr.is_empty() {
|
|
format!("{indicator}{typ} {addr}")
|
|
} else {
|
|
format!("{indicator}{typ} #{transport_id}")
|
|
};
|
|
|
|
let state = helpers::str_field(t, "state");
|
|
let tx = t
|
|
.get("stats")
|
|
.and_then(|s| s.get("packets_sent").or_else(|| s.get("frames_sent")))
|
|
.and_then(|v| v.as_u64())
|
|
.map(|n| n.to_string())
|
|
.unwrap_or_else(|| "-".into());
|
|
let rx = t
|
|
.get("stats")
|
|
.and_then(|s| s.get("packets_recv").or_else(|| s.get("frames_recv")))
|
|
.and_then(|v| v.as_u64())
|
|
.map(|n| n.to_string())
|
|
.unwrap_or_else(|| "-".into());
|
|
|
|
Row::new(vec![
|
|
Cell::from(label),
|
|
Cell::from(state.to_string()),
|
|
Cell::from(""),
|
|
Cell::from(tx),
|
|
Cell::from(rx),
|
|
])
|
|
.style(Style::default().fg(Color::White))
|
|
}
|
|
TreeRow::Link { index, is_last } => {
|
|
let link = &links[*index];
|
|
let tree_char = if *is_last {
|
|
"\u{2514}\u{2500}"
|
|
} else {
|
|
"\u{251C}\u{2500}"
|
|
}; // └─ or ├─
|
|
let dir = helpers::str_field(link, "direction");
|
|
let dir_short = match dir {
|
|
"Outbound" => "Out",
|
|
"Inbound" => "In",
|
|
other => other,
|
|
};
|
|
// Wide enough to render a full MAC (~17) or `hci0/MAC`
|
|
// (~22) without chopping mid-octet; the link detail view
|
|
// shows the untruncated address.
|
|
let addr = helpers::truncate_hex(helpers::str_field(link, "remote_addr"), 24);
|
|
let label = format!(" {tree_char} {dir_short} {addr}");
|
|
|
|
let state = helpers::str_field(link, "state");
|
|
let peer_name = lookup_peer_for_link(app, link)
|
|
.map(|p| helpers::str_field(&p, "display_name").to_string())
|
|
.unwrap_or_default();
|
|
|
|
Row::new(vec![
|
|
Cell::from(Span::styled(
|
|
label,
|
|
Style::default().fg(if dir == "Outbound" {
|
|
Color::Cyan
|
|
} else {
|
|
Color::Green
|
|
}),
|
|
)),
|
|
Cell::from(state.to_string()),
|
|
Cell::from(peer_name),
|
|
Cell::from(""),
|
|
Cell::from(""),
|
|
])
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
let transport_count = transports.len();
|
|
let link_count: usize = tree_rows
|
|
.iter()
|
|
.filter(|r| matches!(r, TreeRow::Link { .. }))
|
|
.count();
|
|
let title = if link_count > 0 {
|
|
format!(" Transports ({transport_count}) Links ({link_count}) ")
|
|
} else {
|
|
format!(" Transports ({transport_count}) ")
|
|
};
|
|
|
|
let widths = [
|
|
Constraint::Min(28),
|
|
Constraint::Length(12),
|
|
Constraint::Length(14),
|
|
Constraint::Length(9),
|
|
Constraint::Length(9),
|
|
];
|
|
|
|
let table = Table::new(rows, widths)
|
|
.header(header)
|
|
.block(Block::default().borders(Borders::ALL).title(title))
|
|
.row_highlight_style(
|
|
Style::default()
|
|
.bg(Color::DarkGray)
|
|
.add_modifier(Modifier::BOLD),
|
|
)
|
|
.highlight_symbol("\u{25B8} "); // ▸
|
|
|
|
let row_count = tree_rows.len();
|
|
let state = app.table_states.entry(Tab::Transports).or_default();
|
|
frame.render_stateful_widget(table, area, state);
|
|
|
|
if row_count > 0 {
|
|
let selected = state.selected().unwrap_or(0);
|
|
let mut scrollbar_state = ScrollbarState::new(row_count).position(selected);
|
|
frame.render_stateful_widget(
|
|
Scrollbar::new(ScrollbarOrientation::VerticalRight)
|
|
.begin_symbol(None)
|
|
.end_symbol(None),
|
|
area,
|
|
&mut scrollbar_state,
|
|
);
|
|
}
|
|
}
|
|
|
|
fn draw_detail(
|
|
frame: &mut Frame,
|
|
app: &App,
|
|
area: Rect,
|
|
transports: &[serde_json::Value],
|
|
links: &[serde_json::Value],
|
|
tree_rows: &[TreeRow],
|
|
) {
|
|
let selected = app
|
|
.table_states
|
|
.get(&Tab::Transports)
|
|
.and_then(|s| s.selected())
|
|
.unwrap_or(0);
|
|
|
|
let Some(tree_row) = tree_rows.get(selected) else {
|
|
let block = Block::default().borders(Borders::ALL).title(" Detail ");
|
|
let inner = block.inner(area);
|
|
frame.render_widget(block, area);
|
|
let msg = Paragraph::new(" No item selected").style(Style::default().fg(Color::DarkGray));
|
|
frame.render_widget(msg, inner);
|
|
return;
|
|
};
|
|
|
|
match tree_row {
|
|
TreeRow::Transport { index, .. } => {
|
|
draw_transport_detail(frame, app, area, &transports[*index]);
|
|
}
|
|
TreeRow::Link { index, .. } => {
|
|
draw_link_detail(frame, app, area, &links[*index]);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn draw_transport_detail(frame: &mut Frame, app: &App, area: Rect, t: &serde_json::Value) {
|
|
let block = Block::default()
|
|
.borders(Borders::ALL)
|
|
.title(" Transport Detail ");
|
|
let inner = block.inner(area);
|
|
frame.render_widget(block, area);
|
|
|
|
let mut lines: Vec<Line> = vec![
|
|
helpers::section_header("Transport Info"),
|
|
helpers::kv_line("Transport ID", &helpers::u64_field(t, "transport_id")),
|
|
helpers::kv_line("Type", helpers::str_field(t, "type")),
|
|
helpers::kv_line("State", helpers::str_field(t, "state")),
|
|
helpers::kv_line("MTU", &helpers::u64_field(t, "mtu")),
|
|
];
|
|
|
|
if let Some(name) = t.get("name").and_then(|v| v.as_str()) {
|
|
lines.push(helpers::kv_line("Name", name));
|
|
}
|
|
if let Some(addr) = t.get("local_addr").and_then(|v| v.as_str()) {
|
|
lines.push(helpers::kv_line("Local Addr", addr));
|
|
}
|
|
|
|
// Tor-specific info
|
|
if let Some(mode) = t.get("tor_mode").and_then(|v| v.as_str()) {
|
|
lines.push(helpers::kv_line("Tor Mode", mode));
|
|
}
|
|
if let Some(onion) = t.get("onion_address").and_then(|v| v.as_str()) {
|
|
lines.push(helpers::kv_line("Onion Address", onion));
|
|
}
|
|
|
|
// Transport stats
|
|
if let Some(stats) = t.get("stats") {
|
|
let typ = helpers::str_field(t, "type");
|
|
lines.push(Line::from(""));
|
|
lines.push(helpers::section_header("Traffic"));
|
|
|
|
match typ {
|
|
"ethernet" => {
|
|
lines.push(helpers::kv_line(
|
|
"Frames Sent",
|
|
&helpers::nested_u64(t, "stats", "frames_sent"),
|
|
));
|
|
lines.push(helpers::kv_line(
|
|
"Frames Recv",
|
|
&helpers::nested_u64(t, "stats", "frames_recv"),
|
|
));
|
|
}
|
|
_ => {
|
|
lines.push(helpers::kv_line(
|
|
"Pkts Sent",
|
|
&helpers::nested_u64(t, "stats", "packets_sent"),
|
|
));
|
|
lines.push(helpers::kv_line(
|
|
"Pkts Recv",
|
|
&helpers::nested_u64(t, "stats", "packets_recv"),
|
|
));
|
|
}
|
|
}
|
|
|
|
let bytes_sent = stats
|
|
.get("bytes_sent")
|
|
.and_then(|v| v.as_u64())
|
|
.unwrap_or(0);
|
|
let bytes_recv = stats
|
|
.get("bytes_recv")
|
|
.and_then(|v| v.as_u64())
|
|
.unwrap_or(0);
|
|
lines.push(helpers::kv_line(
|
|
"Bytes Sent",
|
|
&helpers::format_bytes(bytes_sent),
|
|
));
|
|
lines.push(helpers::kv_line(
|
|
"Bytes Recv",
|
|
&helpers::format_bytes(bytes_recv),
|
|
));
|
|
|
|
lines.push(Line::from(""));
|
|
lines.push(helpers::section_header("Errors"));
|
|
lines.push(helpers::kv_line(
|
|
"Send Errors",
|
|
&helpers::nested_u64(t, "stats", "send_errors"),
|
|
));
|
|
lines.push(helpers::kv_line(
|
|
"Recv Errors",
|
|
&helpers::nested_u64(t, "stats", "recv_errors"),
|
|
));
|
|
|
|
match typ {
|
|
"udp" => {
|
|
lines.push(helpers::kv_line(
|
|
"MTU Exceeded",
|
|
&helpers::nested_u64(t, "stats", "mtu_exceeded"),
|
|
));
|
|
lines.push(helpers::kv_line(
|
|
"Kernel Drops",
|
|
&helpers::nested_u64(t, "stats", "kernel_drops"),
|
|
));
|
|
}
|
|
"tcp" => {
|
|
lines.push(helpers::kv_line(
|
|
"MTU Exceeded",
|
|
&helpers::nested_u64(t, "stats", "mtu_exceeded"),
|
|
));
|
|
lines.push(Line::from(""));
|
|
lines.push(helpers::section_header("Connections"));
|
|
lines.push(helpers::kv_line(
|
|
"Established",
|
|
&helpers::nested_u64(t, "stats", "connections_established"),
|
|
));
|
|
lines.push(helpers::kv_line(
|
|
"Accepted",
|
|
&helpers::nested_u64(t, "stats", "connections_accepted"),
|
|
));
|
|
lines.push(helpers::kv_line(
|
|
"Rejected",
|
|
&helpers::nested_u64(t, "stats", "connections_rejected"),
|
|
));
|
|
lines.push(helpers::kv_line(
|
|
"Timeouts",
|
|
&helpers::nested_u64(t, "stats", "connect_timeouts"),
|
|
));
|
|
lines.push(helpers::kv_line(
|
|
"Refused",
|
|
&helpers::nested_u64(t, "stats", "connect_refused"),
|
|
));
|
|
}
|
|
"tor" => {
|
|
lines.push(helpers::kv_line(
|
|
"MTU Exceeded",
|
|
&helpers::nested_u64(t, "stats", "mtu_exceeded"),
|
|
));
|
|
lines.push(helpers::kv_line(
|
|
"SOCKS5 Errors",
|
|
&helpers::nested_u64(t, "stats", "socks5_errors"),
|
|
));
|
|
lines.push(helpers::kv_line(
|
|
"Control Errors",
|
|
&helpers::nested_u64(t, "stats", "control_errors"),
|
|
));
|
|
lines.push(Line::from(""));
|
|
lines.push(helpers::section_header("Connections"));
|
|
lines.push(helpers::kv_line(
|
|
"Established",
|
|
&helpers::nested_u64(t, "stats", "connections_established"),
|
|
));
|
|
lines.push(helpers::kv_line(
|
|
"Accepted",
|
|
&helpers::nested_u64(t, "stats", "connections_accepted"),
|
|
));
|
|
lines.push(helpers::kv_line(
|
|
"Rejected",
|
|
&helpers::nested_u64(t, "stats", "connections_rejected"),
|
|
));
|
|
lines.push(helpers::kv_line(
|
|
"Timeouts",
|
|
&helpers::nested_u64(t, "stats", "connect_timeouts"),
|
|
));
|
|
lines.push(helpers::kv_line(
|
|
"Refused",
|
|
&helpers::nested_u64(t, "stats", "connect_refused"),
|
|
));
|
|
}
|
|
"nym" => {
|
|
lines.push(helpers::kv_line(
|
|
"MTU Exceeded",
|
|
&helpers::nested_u64(t, "stats", "mtu_exceeded"),
|
|
));
|
|
lines.push(helpers::kv_line(
|
|
"SOCKS5 Errors",
|
|
&helpers::nested_u64(t, "stats", "socks5_errors"),
|
|
));
|
|
lines.push(Line::from(""));
|
|
lines.push(helpers::section_header("Connections"));
|
|
lines.push(helpers::kv_line(
|
|
"Established",
|
|
&helpers::nested_u64(t, "stats", "connections_established"),
|
|
));
|
|
lines.push(helpers::kv_line(
|
|
"Timeouts",
|
|
&helpers::nested_u64(t, "stats", "connect_timeouts"),
|
|
));
|
|
}
|
|
"ethernet" => {
|
|
lines.push(Line::from(""));
|
|
lines.push(helpers::section_header("Beacons"));
|
|
lines.push(helpers::kv_line(
|
|
"Beacons Sent",
|
|
&helpers::nested_u64(t, "stats", "beacons_sent"),
|
|
));
|
|
lines.push(helpers::kv_line(
|
|
"Beacons Recv",
|
|
&helpers::nested_u64(t, "stats", "beacons_recv"),
|
|
));
|
|
lines.push(Line::from(""));
|
|
lines.push(helpers::section_header("Frame Errors"));
|
|
lines.push(helpers::kv_line(
|
|
"Too Short",
|
|
&helpers::nested_u64(t, "stats", "frames_too_short"),
|
|
));
|
|
lines.push(helpers::kv_line(
|
|
"Too Long",
|
|
&helpers::nested_u64(t, "stats", "frames_too_long"),
|
|
));
|
|
}
|
|
_ => {}
|
|
}
|
|
|
|
// Tor daemon monitoring (when control port data is available)
|
|
if let Some(mon) = t.get("tor_monitoring") {
|
|
lines.push(Line::from(""));
|
|
lines.push(helpers::section_header("Tor Daemon"));
|
|
lines.push(helpers::kv_line(
|
|
"Bootstrap",
|
|
&format!("{}%", helpers::nested_u64(t, "tor_monitoring", "bootstrap")),
|
|
));
|
|
lines.push(helpers::kv_line(
|
|
"Circuit",
|
|
if mon
|
|
.get("circuit_established")
|
|
.and_then(|v| v.as_bool())
|
|
.unwrap_or(false)
|
|
{
|
|
"established"
|
|
} else {
|
|
"none"
|
|
},
|
|
));
|
|
lines.push(helpers::kv_line(
|
|
"Version",
|
|
&helpers::nested_str(t, "tor_monitoring", "version"),
|
|
));
|
|
lines.push(helpers::kv_line(
|
|
"Network",
|
|
&helpers::nested_str(t, "tor_monitoring", "network_liveness"),
|
|
));
|
|
lines.push(helpers::kv_line(
|
|
"Dormant",
|
|
helpers::bool_field(mon, "dormant"),
|
|
));
|
|
|
|
let tor_read = mon
|
|
.get("traffic_read")
|
|
.and_then(|v| v.as_u64())
|
|
.unwrap_or(0);
|
|
let tor_written = mon
|
|
.get("traffic_written")
|
|
.and_then(|v| v.as_u64())
|
|
.unwrap_or(0);
|
|
lines.push(helpers::kv_line(
|
|
"Tor Read",
|
|
&helpers::format_bytes(tor_read),
|
|
));
|
|
lines.push(helpers::kv_line(
|
|
"Tor Written",
|
|
&helpers::format_bytes(tor_written),
|
|
));
|
|
}
|
|
}
|
|
|
|
let detail_scroll = app.detail_view.as_ref().map(|d| d.scroll).unwrap_or(0);
|
|
let paragraph = Paragraph::new(lines).scroll((detail_scroll, 0));
|
|
frame.render_widget(paragraph, inner);
|
|
}
|
|
|
|
fn draw_link_detail(frame: &mut Frame, app: &App, area: Rect, link: &serde_json::Value) {
|
|
let block = Block::default()
|
|
.borders(Borders::ALL)
|
|
.title(" Link Detail ");
|
|
let inner = block.inner(area);
|
|
frame.render_widget(block, area);
|
|
|
|
let mut lines: Vec<Line> = vec![
|
|
helpers::section_header("Link Info"),
|
|
helpers::kv_line("Link ID", &helpers::u64_field(link, "link_id")),
|
|
helpers::kv_line("Direction", helpers::str_field(link, "direction")),
|
|
helpers::kv_line("State", helpers::str_field(link, "state")),
|
|
helpers::kv_line("Remote Addr", helpers::str_field(link, "remote_addr")),
|
|
helpers::kv_line(
|
|
"Created",
|
|
&helpers::format_elapsed_ms(
|
|
link.get("created_at_ms")
|
|
.and_then(|v| v.as_u64())
|
|
.unwrap_or(0),
|
|
),
|
|
),
|
|
Line::from(""),
|
|
];
|
|
|
|
// Transport cross-reference
|
|
if let Some(transport) = lookup_transport(app, link) {
|
|
lines.push(helpers::section_header("Transport"));
|
|
lines.push(helpers::kv_line(
|
|
"Transport ID",
|
|
&helpers::u64_field(link, "transport_id"),
|
|
));
|
|
lines.push(helpers::kv_line(
|
|
"Type",
|
|
helpers::str_field(&transport, "type"),
|
|
));
|
|
if let Some(name) = transport.get("name").and_then(|v| v.as_str()) {
|
|
lines.push(helpers::kv_line("Name", name));
|
|
}
|
|
lines.push(helpers::kv_line(
|
|
"MTU",
|
|
&helpers::u64_field(&transport, "mtu"),
|
|
));
|
|
if let Some(addr) = transport.get("local_addr").and_then(|v| v.as_str()) {
|
|
lines.push(helpers::kv_line("Local Addr", addr));
|
|
}
|
|
lines.push(helpers::kv_line(
|
|
"State",
|
|
helpers::str_field(&transport, "state"),
|
|
));
|
|
lines.push(Line::from(""));
|
|
}
|
|
|
|
// Peer cross-reference
|
|
if let Some(peer) = lookup_peer_for_link(app, link) {
|
|
lines.push(helpers::section_header("Peer"));
|
|
lines.push(helpers::kv_line(
|
|
"Name",
|
|
helpers::str_field(&peer, "display_name"),
|
|
));
|
|
lines.push(helpers::kv_line(
|
|
"Connectivity",
|
|
helpers::str_field(&peer, "connectivity"),
|
|
));
|
|
lines.push(helpers::kv_line(
|
|
"Last Seen",
|
|
&helpers::format_elapsed_ms(
|
|
peer.get("last_seen_ms")
|
|
.and_then(|v| v.as_u64())
|
|
.unwrap_or(0),
|
|
),
|
|
));
|
|
lines.push(Line::from(""));
|
|
lines.push(helpers::section_header("Peer Stats"));
|
|
lines.push(helpers::kv_line(
|
|
"Pkts Sent",
|
|
&helpers::nested_u64(&peer, "stats", "packets_sent"),
|
|
));
|
|
lines.push(helpers::kv_line(
|
|
"Pkts Recv",
|
|
&helpers::nested_u64(&peer, "stats", "packets_recv"),
|
|
));
|
|
lines.push(helpers::kv_line(
|
|
"Bytes Sent",
|
|
&helpers::format_bytes(
|
|
peer.get("stats")
|
|
.and_then(|s| s.get("bytes_sent"))
|
|
.and_then(|v| v.as_u64())
|
|
.unwrap_or(0),
|
|
),
|
|
));
|
|
lines.push(helpers::kv_line(
|
|
"Bytes Recv",
|
|
&helpers::format_bytes(
|
|
peer.get("stats")
|
|
.and_then(|s| s.get("bytes_recv"))
|
|
.and_then(|v| v.as_u64())
|
|
.unwrap_or(0),
|
|
),
|
|
));
|
|
if peer.get("mmp").is_some() {
|
|
lines.push(Line::from(""));
|
|
lines.push(helpers::section_header("MMP Metrics"));
|
|
lines.push(helpers::kv_line(
|
|
"SRTT",
|
|
&format!("{}ms", helpers::nested_f64(&peer, "mmp", "srtt_ms", 1)),
|
|
));
|
|
lines.push(helpers::kv_line(
|
|
"Loss Rate",
|
|
&helpers::nested_f64_prefer(&peer, "mmp", "smoothed_loss", "loss_rate", 4),
|
|
));
|
|
lines.push(helpers::kv_line(
|
|
"ETX",
|
|
&helpers::nested_f64_prefer(&peer, "mmp", "smoothed_etx", "etx", 2),
|
|
));
|
|
lines.push(helpers::kv_line(
|
|
"LQI",
|
|
&helpers::nested_f64(&peer, "mmp", "lqi", 2),
|
|
));
|
|
}
|
|
}
|
|
|
|
let detail_scroll = app.detail_view.as_ref().map(|d| d.scroll).unwrap_or(0);
|
|
let paragraph = Paragraph::new(lines).scroll((detail_scroll, 0));
|
|
frame.render_widget(paragraph, inner);
|
|
}
|
|
|
|
/// Look up the peer associated with a link by matching link_id.
|
|
fn lookup_peer_for_link(app: &App, link: &serde_json::Value) -> Option<serde_json::Value> {
|
|
let link_id = link.get("link_id").and_then(|v| v.as_u64())?;
|
|
let peers = app.data.get(&Tab::Peers)?;
|
|
peers
|
|
.get("peers")?
|
|
.as_array()?
|
|
.iter()
|
|
.find(|p| p.get("link_id").and_then(|v| v.as_u64()) == Some(link_id))
|
|
.cloned()
|
|
}
|
|
|
|
fn lookup_transport(app: &App, link: &serde_json::Value) -> Option<serde_json::Value> {
|
|
let transport_id = link.get("transport_id").and_then(|v| v.as_u64())?;
|
|
let transports = app.data.get(&Tab::Transports)?;
|
|
transports
|
|
.get("transports")?
|
|
.as_array()?
|
|
.iter()
|
|
.find(|t| t.get("transport_id").and_then(|v| v.as_u64()) == Some(transport_id))
|
|
.cloned()
|
|
}
|