1use crate::identity::{NodeAddr, PeerIdentity, encode_npub};
7use crate::node::Node;
8use crate::node::stats_history::{ALL_METRICS, ALL_PEER_METRICS, Granularity, Metric, PeerMetric};
9use serde_json::{Value, json};
10use std::str::FromStr;
11use std::time::Duration;
12
13fn parse_peer_npub(s: &str) -> Result<NodeAddr, String> {
15 PeerIdentity::from_npub(s)
16 .map(|p| *p.node_addr())
17 .map_err(|e| format!("invalid peer npub: {e}"))
18}
19
20fn now_ms() -> u64 {
22 std::time::SystemTime::now()
23 .duration_since(std::time::UNIX_EPOCH)
24 .map(|d| d.as_millis() as u64)
25 .unwrap_or(0)
26}
27
28fn trend_label(short: f64, long: f64) -> &'static str {
30 if !short.is_finite() || !long.is_finite() || long == 0.0 {
31 return "stable";
32 }
33 let ratio = short / long;
34 if ratio > 1.05 {
35 "rising"
36 } else if ratio < 0.95 {
37 "falling"
38 } else {
39 "stable"
40 }
41}
42
43pub fn show_status(node: &Node) -> Value {
45 let pid = std::process::id();
46 let exe_path = std::env::current_exe()
47 .map(|p| p.display().to_string())
48 .unwrap_or_else(|_| "-".into());
49 let uptime_secs = node.uptime().as_secs();
50 let fwd = node.stats().snapshot().forwarding;
51
52 const SPARK_N: usize = 30;
56 let hist = node.stats_history();
57 let sparklines = json!({
58 "mesh_size": hist.recent(Metric::MeshSize, SPARK_N),
59 "tree_depth": hist.recent(Metric::TreeDepth, SPARK_N),
60 "peer_count": hist.recent(Metric::PeerCount, SPARK_N),
61 "bytes_in": hist.recent(Metric::BytesIn, SPARK_N),
62 "bytes_out": hist.recent(Metric::BytesOut, SPARK_N),
63 "loss_rate": hist.recent(Metric::LossRate, SPARK_N),
64 });
65
66 json!({
67 "version": crate::version::short_version(),
68 "npub": node.npub(),
69 "node_addr": hex::encode(node.node_addr().as_bytes()),
70 "ipv6_addr": format!("{}", node.identity().address()),
71 "state": format!("{}", node.state()),
72 "is_leaf_only": node.is_leaf_only(),
73 "peer_count": node.peer_count(),
74 "session_count": node.session_count(),
75 "link_count": node.link_count(),
76 "transport_count": node.transport_count(),
77 "connection_count": node.connection_count(),
78 "tun_state": format!("{}", node.tun_state()),
79 "tun_name": node.tun_name().unwrap_or("-"),
80 "effective_ipv6_mtu": node.effective_ipv6_mtu(),
81 "control_socket": &node.config().node.control.socket_path,
82 "pid": pid,
83 "exe_path": exe_path,
84 "uptime_secs": uptime_secs,
85 "estimated_mesh_size": node.estimated_mesh_size(),
86 "forwarding": serde_json::to_value(&fwd).unwrap_or_default(),
87 "sparklines": sparklines,
88 })
89}
90
91pub fn show_acl(node: &Node) -> Value {
93 let status = node.peer_acl_status();
94
95 json!({
96 "allow_file": status.allow_file,
97 "deny_file": status.deny_file,
98 "enforcement_active": status.enforcement_active,
99 "effective_mode": status.effective_mode,
100 "default_decision": status.default_decision,
101 "allow_all": status.allow_all,
102 "deny_all": status.deny_all,
103 "allow_file_entries": status.allow_file_entries,
104 "deny_file_entries": status.deny_file_entries,
105 "allow_entries": status.allow_entries,
106 "deny_entries": status.deny_entries,
107 })
108}
109
110pub fn show_peers(node: &Node) -> Value {
112 let tree = node.tree_state();
113 let my_addr = *tree.my_node_addr();
114 let parent_id = *tree.my_declaration().parent_id();
115 let is_root = tree.is_root();
116
117 let nostr_state: std::collections::HashMap<String, _> = node
120 .nostr_discovery_handle()
121 .map(|d| {
122 d.failure_state_snapshot()
123 .into_iter()
124 .map(|view| (view.npub.clone(), view))
125 .collect()
126 })
127 .unwrap_or_default();
128
129 let peers: Vec<Value> = node
130 .peers()
131 .map(|peer| {
132 let node_addr = *peer.node_addr();
133 let addr_hex = hex::encode(node_addr.as_bytes());
134
135 let is_parent = !is_root && node_addr == parent_id;
137 let is_child = tree
138 .peer_declaration(&node_addr)
139 .is_some_and(|decl| *decl.parent_id() == my_addr);
140
141 let mut peer_json = json!({
142 "node_addr": addr_hex,
143 "npub": peer.npub(),
144 "display_name": node.peer_display_name(&node_addr),
145 "ipv6_addr": format!("{}", peer.address()),
146 "connectivity": format!("{}", peer.connectivity()),
147 "link_id": peer.link_id().as_u64(),
148 "authenticated_at_ms": peer.authenticated_at(),
149 "last_seen_ms": peer.last_seen(),
150 "has_tree_position": peer.has_tree_position(),
151 "has_bloom_filter": peer.filter_sequence() > 0,
152 "filter_sequence": peer.filter_sequence(),
153 "is_parent": is_parent,
154 "is_child": is_child,
155 });
156
157 if let Some(addr) = peer.current_addr() {
159 peer_json["transport_addr"] = json!(format!("{}", addr));
160 }
161
162 let link_id = peer.link_id();
164 if let Some(link) = node.get_link(&link_id) {
165 peer_json["direction"] = json!(format!("{}", link.direction()));
166 let transport_id = link.transport_id();
167 if let Some(handle) = node.get_transport(&transport_id) {
168 peer_json["transport_type"] = json!(handle.transport_type().name);
169 }
170 }
171
172 if let Some(coords) = peer.coords() {
174 peer_json["tree_depth"] = json!(coords.depth());
175 }
176
177 let stats = peer.link_stats();
179 peer_json["stats"] = json!({
180 "packets_sent": stats.packets_sent,
181 "packets_recv": stats.packets_recv,
182 "bytes_sent": stats.bytes_sent,
183 "bytes_recv": stats.bytes_recv,
184 });
185
186 peer_json["replay_suppressed"] = json!(peer.replay_suppressed_count());
188 peer_json["consecutive_decrypt_failures"] = json!(peer.consecutive_decrypt_failures());
189
190 let npub = peer.npub();
195 let mut nostr_obj = json!({
196 "consecutive_failures": 0,
197 "in_cooldown": false,
198 "cooldown_until_ms": Value::Null,
199 "last_observed_skew_ms": Value::Null,
200 });
201 if let Some(state) = nostr_state.get(&npub) {
202 nostr_obj["consecutive_failures"] = json!(state.consecutive_failures);
203 nostr_obj["in_cooldown"] = json!(state.cooldown_until_ms.is_some());
204 nostr_obj["cooldown_until_ms"] = state
205 .cooldown_until_ms
206 .map(|t| json!(t))
207 .unwrap_or(Value::Null);
208 nostr_obj["last_observed_skew_ms"] = state
209 .last_observed_skew_ms
210 .map(|s| json!(s))
211 .unwrap_or(Value::Null);
212 }
213 peer_json["nostr_traversal"] = nostr_obj;
214
215 if let Some(session) = peer.noise_session() {
217 peer_json["noise"] = json!({
218 "send_counter": session.current_send_counter(),
219 "highest_recv_counter": session.highest_received_counter(),
220 });
221 }
222
223 if let Some(idx) = peer.our_index() {
225 peer_json["our_session_index"] = json!(format!("{:08x}", idx.as_u32()));
226 }
227
228 if peer.rekey_in_progress() {
230 peer_json["rekey_in_progress"] = json!(true);
231 }
232 if peer.is_draining() {
233 peer_json["rekey_draining"] = json!(true);
234 }
235 peer_json["current_k_bit"] = json!(peer.current_k_bit());
236
237 if let Some(mmp) = peer.mmp() {
239 let mut mmp_json = json!({
240 "mode": format!("{}", mmp.mode()),
241 });
242 if let Some(srtt) = mmp.metrics.srtt_ms() {
243 mmp_json["srtt_ms"] = json!(srtt);
244 }
245 mmp_json["loss_rate"] = json!(mmp.metrics.loss_rate());
246 mmp_json["etx"] = json!(mmp.metrics.etx);
247 mmp_json["goodput_bps"] = json!(mmp.metrics.goodput_bps);
248 mmp_json["delivery_ratio_forward"] = json!(mmp.metrics.delivery_ratio_forward);
249 mmp_json["delivery_ratio_reverse"] = json!(mmp.metrics.delivery_ratio_reverse);
250 if let Some(smoothed_loss) = mmp.metrics.smoothed_loss() {
251 mmp_json["smoothed_loss"] = json!(smoothed_loss);
252 }
253 if let Some(smoothed_etx) = mmp.metrics.smoothed_etx() {
254 mmp_json["smoothed_etx"] = json!(smoothed_etx);
255 }
256 if let Some(srtt) = mmp.metrics.srtt_ms()
257 && let Some(setx) = mmp.metrics.smoothed_etx()
258 {
259 mmp_json["lqi"] = json!(setx * (1.0 + srtt / 100.0));
260 }
261 peer_json["mmp"] = mmp_json;
262 }
263
264 peer_json
265 })
266 .collect();
267
268 json!({ "peers": peers })
269}
270
271pub fn show_links(node: &Node) -> Value {
273 let links: Vec<Value> = node
274 .links()
275 .map(|link| {
276 let stats = link.stats();
277 json!({
278 "link_id": link.link_id().as_u64(),
279 "transport_id": link.transport_id().as_u32(),
280 "remote_addr": format!("{}", link.remote_addr()),
281 "direction": format!("{}", link.direction()),
282 "state": format!("{}", link.state()),
283 "created_at_ms": link.created_at(),
284 "stats": {
285 "packets_sent": stats.packets_sent,
286 "packets_recv": stats.packets_recv,
287 "bytes_sent": stats.bytes_sent,
288 "bytes_recv": stats.bytes_recv,
289 "last_recv_ms": stats.last_recv_ms,
290 },
291 })
292 })
293 .collect();
294
295 json!({ "links": links })
296}
297
298pub fn show_tree(node: &Node) -> Value {
300 let tree = node.tree_state();
301 let my_coords = tree.my_coords();
302 let decl = tree.my_declaration();
303
304 let coords: Vec<String> = my_coords
306 .entries()
307 .iter()
308 .map(|e| hex::encode(e.node_addr.as_bytes()))
309 .collect();
310
311 let peers: Vec<Value> = tree
313 .peer_ids()
314 .map(|peer_id| {
315 let mut peer_json = json!({
316 "node_addr": hex::encode(peer_id.as_bytes()),
317 "display_name": node.peer_display_name(peer_id),
318 });
319 if let Some(coords) = tree.peer_coords(peer_id) {
320 let coord_path: Vec<String> = coords
321 .entries()
322 .iter()
323 .map(|e| hex::encode(e.node_addr.as_bytes()))
324 .collect();
325 peer_json["depth"] = json!(coords.depth());
326 peer_json["root"] = json!(hex::encode(coords.root_id().as_bytes()));
327 peer_json["coords"] = json!(coord_path);
328 peer_json["distance_to_us"] = json!(my_coords.distance_to(coords));
329 }
330 peer_json
331 })
332 .collect();
333
334 let parent_addr = my_coords.parent_id();
336 let parent_hex = hex::encode(parent_addr.as_bytes());
337 let parent_display = node.peer_display_name(parent_addr);
338
339 let tree_stats = node.stats().snapshot().tree;
340
341 json!({
342 "my_node_addr": hex::encode(tree.my_node_addr().as_bytes()),
343 "root": hex::encode(tree.root().as_bytes()),
344 "is_root": tree.is_root(),
345 "depth": my_coords.depth(),
346 "my_coords": coords,
347 "parent": parent_hex,
348 "parent_display_name": parent_display,
349 "declaration_sequence": decl.sequence(),
350 "declaration_signed": decl.is_signed(),
351 "peer_tree_count": tree.peer_count(),
352 "peers": peers,
353 "stats": serde_json::to_value(&tree_stats).unwrap_or_default(),
354 })
355}
356
357pub fn show_sessions(node: &Node) -> Value {
359 let sessions: Vec<Value> = node
360 .session_entries()
361 .map(|(addr, entry)| {
362 let state_str = if entry.is_established() {
363 "established"
364 } else if entry.is_initiating() {
365 "initiating"
366 } else if entry.is_awaiting_msg3() {
367 "awaiting_msg3"
368 } else {
369 "unknown"
370 };
371
372 let mut session_json = json!({
373 "remote_addr": hex::encode(addr.as_bytes()),
374 "display_name": node.peer_display_name(addr),
375 "state": state_str,
376 "is_initiator": entry.is_initiator(),
377 "last_activity_ms": entry.last_activity(),
378 });
379
380 let (xonly, _parity) = entry.remote_pubkey().x_only_public_key();
382 session_json["npub"] = json!(encode_npub(&xonly));
383
384 let (pkts_tx, pkts_rx, bytes_tx, bytes_rx) = entry.traffic_counters();
386 session_json["stats"] = json!({
387 "packets_sent": pkts_tx,
388 "packets_recv": pkts_rx,
389 "bytes_sent": bytes_tx,
390 "bytes_recv": bytes_rx,
391 });
392
393 if !entry.is_established() {
395 session_json["resend_count"] = json!(entry.resend_count());
396 }
397
398 if entry.is_established() {
400 session_json["session_start_ms"] = json!(entry.session_start_ms());
401 session_json["current_k_bit"] = json!(entry.current_k_bit());
402 session_json["coords_warmup_remaining"] = json!(entry.coords_warmup_remaining());
403 session_json["is_draining"] = json!(entry.is_draining());
404 }
405
406 if let Some(mmp) = entry.mmp() {
408 let mut mmp_json = json!({
409 "mode": format!("{}", mmp.mode()),
410 "loss_rate": mmp.metrics.loss_rate(),
411 "etx": mmp.metrics.etx,
412 "goodput_bps": mmp.metrics.goodput_bps,
413 "delivery_ratio_forward": mmp.metrics.delivery_ratio_forward,
414 "delivery_ratio_reverse": mmp.metrics.delivery_ratio_reverse,
415 "path_mtu": mmp.path_mtu.current_mtu(),
416 });
417 if let Some(srtt) = mmp.metrics.srtt_ms() {
418 mmp_json["srtt_ms"] = json!(srtt);
419 }
420 if let Some(smoothed_loss) = mmp.metrics.smoothed_loss() {
421 mmp_json["smoothed_loss"] = json!(smoothed_loss);
422 }
423 if let Some(smoothed_etx) = mmp.metrics.smoothed_etx() {
424 mmp_json["smoothed_etx"] = json!(smoothed_etx);
425 }
426 if let Some(srtt) = mmp.metrics.srtt_ms()
427 && let Some(setx) = mmp.metrics.smoothed_etx()
428 {
429 mmp_json["sqi"] = json!(setx * (1.0 + srtt / 100.0));
430 }
431 session_json["mmp"] = mmp_json;
432 }
433
434 session_json
435 })
436 .collect();
437
438 json!({ "sessions": sessions })
439}
440
441pub fn show_bloom(node: &Node) -> Value {
443 let bloom = node.bloom_state();
444
445 let leaf_deps: Vec<String> = bloom
446 .leaf_dependents()
447 .iter()
448 .map(|addr| hex::encode(addr.as_bytes()))
449 .collect();
450
451 let peer_filters: Vec<Value> = node
453 .peers()
454 .map(|peer| {
455 let addr = *peer.node_addr();
456 let mut pf = json!({
457 "peer": hex::encode(addr.as_bytes()),
458 "display_name": node.peer_display_name(&addr),
459 "has_filter": peer.filter_sequence() > 0,
460 "filter_sequence": peer.filter_sequence(),
461 });
462 if let Some(filter) = peer.inbound_filter() {
463 let max_fpr = node.config().node.bloom.max_inbound_fpr;
464 pf["estimated_count"] = json!(filter.estimated_count(max_fpr));
465 pf["set_bits"] = json!(filter.count_ones());
466 pf["fill_ratio"] = json!(filter.fill_ratio());
467 }
468 pf
469 })
470 .collect();
471
472 let bloom_stats = node.stats().snapshot().bloom;
473
474 json!({
475 "own_node_addr": hex::encode(node.node_addr().as_bytes()),
476 "is_leaf_only": node.is_leaf_only(),
477 "sequence": bloom.sequence(),
478 "leaf_dependent_count": bloom.leaf_dependents().len(),
479 "leaf_dependents": leaf_deps,
480 "peer_filters": peer_filters,
481 "stats": serde_json::to_value(&bloom_stats).unwrap_or_default(),
482 })
483}
484
485pub fn show_mmp(node: &Node) -> Value {
487 let peers: Vec<Value> = node.peers().filter_map(|peer| {
489 let mmp = peer.mmp()?;
490 let addr = *peer.node_addr();
491 let metrics = &mmp.metrics;
492
493 let mut link_layer = json!({
494 "loss_rate": metrics.loss_rate(),
495 "etx": metrics.etx,
496 "goodput_bps": metrics.goodput_bps,
497 "spin_bit_role": if mmp.spin_bit.is_initiator() { "initiator" } else { "responder" },
498 });
499
500 if let Some(smoothed_loss) = metrics.smoothed_loss() {
501 link_layer["smoothed_loss"] = json!(smoothed_loss);
502 }
503 if let Some(smoothed_etx) = metrics.smoothed_etx() {
504 link_layer["smoothed_etx"] = json!(smoothed_etx);
505 }
506 if let Some(srtt) = metrics.srtt_ms() {
507 link_layer["srtt_ms"] = json!(srtt);
508 if let Some(setx) = metrics.smoothed_etx() {
509 link_layer["lqi"] = json!(setx * (1.0 + srtt / 100.0));
510 }
511 }
512
513 if metrics.rtt_trend.initialized() {
515 link_layer["rtt_trend"] = json!(trend_label(metrics.rtt_trend.short(), metrics.rtt_trend.long()));
516 }
517 if metrics.loss_trend.initialized() {
518 link_layer["loss_trend"] = json!(trend_label(metrics.loss_trend.short(), metrics.loss_trend.long()));
519 }
520 if metrics.goodput_trend.initialized() {
521 link_layer["goodput_trend"] = json!(trend_label(metrics.goodput_trend.short(), metrics.goodput_trend.long()));
522 }
523 if metrics.jitter_trend.initialized() {
524 link_layer["jitter_trend"] = json!(trend_label(metrics.jitter_trend.short(), metrics.jitter_trend.long()));
525 }
526
527 link_layer["delivery_ratio_forward"] = json!(metrics.delivery_ratio_forward);
528 link_layer["delivery_ratio_reverse"] = json!(metrics.delivery_ratio_reverse);
529 link_layer["ecn_ce_count"] = json!(metrics.last_ecn_ce_count());
530
531 Some(json!({
532 "peer": hex::encode(addr.as_bytes()),
533 "display_name": node.peer_display_name(&addr),
534 "mode": format!("{}", mmp.mode()),
535 "link_layer": link_layer,
536 }))
537 }).collect();
538
539 let sessions: Vec<Value> = node
541 .session_entries()
542 .filter_map(|(addr, entry)| {
543 let mmp = entry.mmp()?;
544 let metrics = &mmp.metrics;
545
546 let mut session_layer = json!({
547 "loss_rate": metrics.loss_rate(),
548 "etx": metrics.etx,
549 "path_mtu": mmp.path_mtu.current_mtu(),
550 });
551
552 if let Some(smoothed_loss) = metrics.smoothed_loss() {
553 session_layer["smoothed_loss"] = json!(smoothed_loss);
554 }
555 if let Some(smoothed_etx) = metrics.smoothed_etx() {
556 session_layer["smoothed_etx"] = json!(smoothed_etx);
557 }
558 if let Some(srtt) = metrics.srtt_ms() {
559 session_layer["srtt_ms"] = json!(srtt);
560 if let Some(setx) = metrics.smoothed_etx() {
561 session_layer["sqi"] = json!(setx * (1.0 + srtt / 100.0));
562 }
563 }
564
565 Some(json!({
566 "remote": hex::encode(addr.as_bytes()),
567 "display_name": node.peer_display_name(addr),
568 "mode": format!("{}", mmp.mode()),
569 "session_layer": session_layer,
570 }))
571 })
572 .collect();
573
574 json!({
575 "peers": peers,
576 "sessions": sessions,
577 })
578}
579
580pub fn show_cache(node: &Node) -> Value {
582 let cache = node.coord_cache();
583 let now = now_ms();
584 let stats = cache.stats(now);
585
586 let entries: Vec<Value> = cache
588 .iter(now)
589 .map(|(addr, entry)| {
590 let fips_addr = crate::identity::FipsAddress::from_node_addr(addr);
591 let coord_path: Vec<String> = entry
592 .coords()
593 .entries()
594 .iter()
595 .map(|e| hex::encode(e.node_addr.as_bytes()))
596 .collect();
597 let mut entry_json = json!({
598 "node_addr": hex::encode(addr.as_bytes()),
599 "display_name": node.peer_display_name(addr),
600 "ipv6_addr": format!("{}", fips_addr),
601 "depth": entry.coords().depth(),
602 "coords": coord_path,
603 "age_ms": now.saturating_sub(entry.created_at()),
604 "last_used_ms": entry.last_used(),
605 });
606 if let Some(mtu) = entry.path_mtu() {
607 entry_json["path_mtu"] = json!(mtu);
608 }
609 entry_json
610 })
611 .collect();
612
613 json!({
614 "count": stats.entries,
615 "max_entries": stats.max_entries,
616 "fill_ratio": stats.fill_ratio(),
617 "default_ttl_ms": cache.default_ttl_ms(),
618 "expired": stats.expired,
619 "avg_age_ms": stats.avg_age_ms,
620 "entries": entries,
621 })
622}
623
624pub fn show_connections(node: &Node) -> Value {
626 let now = now_ms();
627 let connections: Vec<Value> = node
628 .connections()
629 .map(|conn| {
630 let mut conn_json = json!({
631 "link_id": conn.link_id().as_u64(),
632 "direction": format!("{}", conn.direction()),
633 "handshake_state": format!("{}", conn.handshake_state()),
634 "started_at_ms": conn.started_at(),
635 "idle_ms": now.saturating_sub(conn.last_activity()),
636 "resend_count": conn.resend_count(),
637 });
638
639 if let Some(identity) = conn.expected_identity() {
640 conn_json["expected_peer"] = json!(identity.npub());
641 }
642
643 conn_json
644 })
645 .collect();
646
647 json!({ "connections": connections })
648}
649
650pub fn show_transports(node: &Node) -> Value {
652 let transports: Vec<Value> = node
653 .transport_ids()
654 .map(|id| {
655 let handle = node.get_transport(id).unwrap();
656 let mut t_json = json!({
657 "transport_id": id.as_u32(),
658 "type": handle.transport_type().name,
659 "state": format!("{}", handle.state()),
660 "mtu": handle.mtu(),
661 });
662
663 if let Some(name) = handle.name() {
664 t_json["name"] = json!(name);
665 }
666 if let Some(addr) = handle.local_addr() {
667 t_json["local_addr"] = json!(format!("{}", addr));
668 }
669
670 if let Some(mode) = handle.tor_mode() {
672 t_json["tor_mode"] = json!(mode);
673 }
674 if let Some(onion) = handle.onion_address() {
675 t_json["onion_address"] = json!(onion);
676 }
677 if let Some(monitoring) = handle.tor_monitoring() {
678 t_json["tor_monitoring"] = serde_json::to_value(&monitoring).unwrap_or_default();
679 }
680
681 t_json["stats"] = handle.transport_stats();
682
683 t_json
684 })
685 .collect();
686
687 json!({ "transports": transports })
688}
689
690pub fn show_routing(node: &Node) -> Value {
692 let cache = node.coord_cache();
693 let now = now_ms();
694 let cache_stats = cache.stats(now);
695 let node_stats = node.stats().snapshot();
696 let learned_routes = node.learned_route_table_snapshot(now);
697
698 let lookups: Vec<Value> = node
700 .pending_lookups_iter()
701 .map(|(addr, lookup)| {
702 json!({
703 "target": hex::encode(addr.as_bytes()),
704 "display_name": node.peer_display_name(addr),
705 "initiated_ms": lookup.initiated_ms,
706 "last_sent_ms": lookup.last_sent_ms,
707 "attempt": lookup.attempt,
708 "age_ms": now.saturating_sub(lookup.initiated_ms),
709 })
710 })
711 .collect();
712
713 let retries: Vec<Value> = node
715 .retry_state_iter()
716 .map(|(addr, state)| {
717 json!({
718 "node_addr": hex::encode(addr.as_bytes()),
719 "display_name": node.peer_display_name(addr),
720 "retry_count": state.retry_count,
721 "retry_after_ms": state.retry_after_ms,
722 "auto_reconnect": state.reconnect,
723 })
724 })
725 .collect();
726
727 json!({
728 "coord_cache_entries": cache_stats.entries,
729 "routing_mode": node.config().node.routing.mode.to_string(),
730 "learned_routes": serde_json::to_value(&learned_routes).unwrap_or_default(),
731 "identity_cache_entries": node.identity_cache_len(),
732 "pending_lookups": lookups,
733 "pending_tun_destinations": node.pending_tun_destinations(),
734 "pending_tun_packets": node.pending_tun_total_packets(),
735 "recent_requests": node.recent_request_count(),
736 "retries": retries,
737 "forwarding": serde_json::to_value(&node_stats.forwarding).unwrap_or_default(),
738 "discovery": serde_json::to_value(&node_stats.discovery).unwrap_or_default(),
739 "error_signals": serde_json::to_value(&node_stats.errors).unwrap_or_default(),
740 "congestion": serde_json::to_value(&node_stats.congestion).unwrap_or_default(),
741 })
742}
743
744pub fn show_identity_cache(node: &Node) -> Value {
751 let now = now_ms();
752 let entries: Vec<Value> = node
753 .identity_cache_iter()
754 .map(|(node_addr, pubkey, last_seen_ms)| {
755 let (xonly, _parity) = pubkey.x_only_public_key();
756 let fips_addr = crate::identity::FipsAddress::from_node_addr(node_addr);
757 json!({
758 "node_addr": hex::encode(node_addr.as_bytes()),
759 "npub": encode_npub(&xonly),
760 "display_name": node.peer_display_name(node_addr),
761 "ipv6_addr": format!("{}", fips_addr),
762 "last_seen_ms": last_seen_ms,
763 "age_ms": now.saturating_sub(last_seen_ms),
764 })
765 })
766 .collect();
767 let count = entries.len();
768
769 json!({
770 "entries": entries,
771 "count": count,
772 "max_entries": node.identity_cache_max(),
773 })
774}
775
776pub fn show_stats_list() -> Value {
778 let metrics: Vec<Value> = ALL_METRICS
779 .iter()
780 .map(|m| {
781 json!({
782 "name": m.name(),
783 "unit": m.unit(),
784 "scope": "node",
785 })
786 })
787 .chain(ALL_PEER_METRICS.iter().map(|m| {
788 json!({
789 "name": m.name(),
790 "unit": m.unit(),
791 "scope": "peer",
792 })
793 }))
794 .collect();
795 json!({
796 "metrics": metrics,
797 "fast_ring_seconds": crate::node::stats_history::FAST_RING_CAPACITY,
798 "slow_ring_minutes": crate::node::stats_history::SLOW_RING_CAPACITY,
799 "peer_retention_seconds": crate::node::stats_history::PEER_EVICTION_SECS,
800 })
801}
802
803pub fn show_stats_history(node: &Node, params: Option<&Value>) -> super::protocol::Response {
815 use super::protocol::Response;
816 let Some(params) = params else {
817 return Response::error("missing params for show_stats_history");
818 };
819
820 let metric_name = match params.get("metric").and_then(|v| v.as_str()) {
821 Some(v) => v,
822 None => return Response::error("missing 'metric' parameter"),
823 };
824
825 let window_str = params
826 .get("window")
827 .and_then(|v| v.as_str())
828 .unwrap_or("10m");
829 let window = match parse_duration(window_str) {
830 Ok(d) => d,
831 Err(e) => return Response::error(e),
832 };
833
834 let granularity_str = params
835 .get("granularity")
836 .and_then(|v| v.as_str())
837 .unwrap_or("1s");
838 let granularity = match Granularity::from_str(granularity_str) {
839 Ok(g) => g,
840 Err(e) => return Response::error(e),
841 };
842
843 let peer_npub = params.get("peer").and_then(|v| v.as_str());
844 let hist = node.stats_history();
845
846 if let Some(npub) = peer_npub {
847 let addr = match parse_peer_npub(npub) {
848 Ok(a) => a,
849 Err(e) => return Response::error(e),
850 };
851 let peer_metric = match PeerMetric::from_str(metric_name) {
852 Ok(m) => m,
853 Err(e) => return Response::error(e),
854 };
855 match hist.peer_query(&addr, peer_metric, window, granularity) {
856 Some(series) => Response::ok(serde_json::to_value(&series).unwrap_or(Value::Null)),
857 None => Response::error(format!(
858 "peer not tracked in stats history: {}",
859 node.peer_display_name(&addr)
860 )),
861 }
862 } else {
863 let metric = match Metric::from_str(metric_name) {
864 Ok(m) => m,
865 Err(e) => return Response::error(e),
866 };
867 let series = hist.query(metric, window, granularity);
868 Response::ok(serde_json::to_value(&series).unwrap_or(Value::Null))
869 }
870}
871
872fn parse_duration(s: &str) -> Result<Duration, String> {
874 if s.is_empty() {
875 return Err("empty duration".to_string());
876 }
877 let (num_part, unit) = s.split_at(s.len() - 1);
878 let n: u64 = num_part
879 .parse()
880 .map_err(|_| format!("invalid duration: {s}"))?;
881 let secs = match unit {
882 "s" => n,
883 "m" => n * 60,
884 "h" => n * 3600,
885 _ => return Err(format!("unknown duration unit: {unit} (expected s, m, h)")),
886 };
887 Ok(Duration::from_secs(secs))
888}
889
890pub fn show_stats_all_history(node: &Node, params: Option<&Value>) -> super::protocol::Response {
898 use super::protocol::Response;
899 let params = params.cloned().unwrap_or_else(|| json!({}));
900
901 let window_str = params
902 .get("window")
903 .and_then(|v| v.as_str())
904 .unwrap_or("10m");
905 let window = match parse_duration(window_str) {
906 Ok(d) => d,
907 Err(e) => return Response::error(e),
908 };
909
910 let granularity_str = params
911 .get("granularity")
912 .and_then(|v| v.as_str())
913 .unwrap_or("1s");
914 let granularity = match Granularity::from_str(granularity_str) {
915 Ok(g) => g,
916 Err(e) => return Response::error(e),
917 };
918
919 let peer_npub = params.get("peer").and_then(|v| v.as_str());
920 let hist = node.stats_history();
921
922 let series: Vec<Value> = if let Some(npub) = peer_npub {
923 let addr = match parse_peer_npub(npub) {
924 Ok(a) => a,
925 Err(e) => return Response::error(e),
926 };
927 if !hist.has_peer(&addr) {
928 return Response::error(format!(
929 "peer not tracked in stats history: {}",
930 node.peer_display_name(&addr)
931 ));
932 }
933 ALL_PEER_METRICS
934 .iter()
935 .map(|m| {
936 let s = hist
937 .peer_query(&addr, *m, window, granularity)
938 .unwrap_or_else(|| {
939 crate::node::stats_history::Series {
942 metric: m.name(),
943 unit: m.unit(),
944 granularity_seconds: granularity.seconds(),
945 values: Vec::new(),
946 }
947 });
948 serde_json::to_value(&s).unwrap_or(Value::Null)
949 })
950 .collect()
951 } else {
952 ALL_METRICS
953 .iter()
954 .map(|m| {
955 let s = hist.query(*m, window, granularity);
956 serde_json::to_value(&s).unwrap_or(Value::Null)
957 })
958 .collect()
959 };
960
961 Response::ok(json!({
962 "granularity_seconds": granularity.seconds(),
963 "window_seconds": window.as_secs(),
964 "peer": peer_npub,
965 "series": series,
966 }))
967}
968
969pub fn show_stats_peers(node: &Node) -> Value {
973 let hist = node.stats_history();
974 let now = std::time::Instant::now();
975
976 let mut peers: Vec<Value> = hist
977 .peers()
978 .map(|(addr, rings)| {
979 let last_contact_secs = now.duration_since(rings.last_contact()).as_secs();
980 let first_seen_secs = now.duration_since(rings.first_seen()).as_secs();
981 let is_active = node.peers().any(|p| p.node_addr() == addr);
982 let npub = node
983 .peers()
984 .find(|p| p.node_addr() == addr)
985 .map(|p| p.npub())
986 .unwrap_or_else(|| hex::encode(addr.as_bytes()));
987 json!({
988 "npub": npub,
989 "node_addr": hex::encode(addr.as_bytes()),
990 "display_name": node.peer_display_name(addr),
991 "is_active": is_active,
992 "first_seen_secs_ago": first_seen_secs,
993 "last_contact_secs_ago": last_contact_secs,
994 })
995 })
996 .collect();
997
998 peers.sort_by(|a, b| {
1000 let a_active = a
1001 .get("is_active")
1002 .and_then(|v| v.as_bool())
1003 .unwrap_or(false);
1004 let b_active = b
1005 .get("is_active")
1006 .and_then(|v| v.as_bool())
1007 .unwrap_or(false);
1008 match (b_active, a_active) {
1009 (true, false) => std::cmp::Ordering::Greater,
1010 (false, true) => std::cmp::Ordering::Less,
1011 _ => a
1012 .get("display_name")
1013 .and_then(|v| v.as_str())
1014 .unwrap_or("")
1015 .cmp(b.get("display_name").and_then(|v| v.as_str()).unwrap_or("")),
1016 }
1017 });
1018
1019 json!({ "peers": peers, "count": peers.len() })
1020}
1021
1022pub fn show_stats_history_all_peers(
1028 node: &Node,
1029 params: Option<&Value>,
1030) -> super::protocol::Response {
1031 use super::protocol::Response;
1032 let Some(params) = params else {
1033 return Response::error("missing params for show_stats_history_all_peers");
1034 };
1035
1036 let metric_name = match params.get("metric").and_then(|v| v.as_str()) {
1037 Some(v) => v,
1038 None => return Response::error("missing 'metric' parameter"),
1039 };
1040 let metric = match PeerMetric::from_str(metric_name) {
1041 Ok(m) => m,
1042 Err(e) => return Response::error(e),
1043 };
1044
1045 let window_str = params
1046 .get("window")
1047 .and_then(|v| v.as_str())
1048 .unwrap_or("10m");
1049 let window = match parse_duration(window_str) {
1050 Ok(d) => d,
1051 Err(e) => return Response::error(e),
1052 };
1053
1054 let granularity_str = params
1055 .get("granularity")
1056 .and_then(|v| v.as_str())
1057 .unwrap_or("1s");
1058 let granularity = match Granularity::from_str(granularity_str) {
1059 Ok(g) => g,
1060 Err(e) => return Response::error(e),
1061 };
1062
1063 let hist = node.stats_history();
1064 let peer_addrs: Vec<NodeAddr> = hist.peer_addrs().copied().collect();
1065
1066 let mut peers: Vec<Value> = peer_addrs
1067 .iter()
1068 .filter_map(|addr| {
1069 let s = hist.peer_query(addr, metric, window, granularity)?;
1070 let is_active = node.peers().any(|p| p.node_addr() == addr);
1071 Some(json!({
1072 "node_addr": hex::encode(addr.as_bytes()),
1073 "display_name": node.peer_display_name(addr),
1074 "is_active": is_active,
1075 "values": serde_json::to_value(&s.values).unwrap_or(Value::Null),
1076 }))
1077 })
1078 .collect();
1079
1080 peers.sort_by(|a, b| {
1082 let a_active = a
1083 .get("is_active")
1084 .and_then(|v| v.as_bool())
1085 .unwrap_or(false);
1086 let b_active = b
1087 .get("is_active")
1088 .and_then(|v| v.as_bool())
1089 .unwrap_or(false);
1090 match (b_active, a_active) {
1091 (true, false) => std::cmp::Ordering::Greater,
1092 (false, true) => std::cmp::Ordering::Less,
1093 _ => a
1094 .get("display_name")
1095 .and_then(|v| v.as_str())
1096 .unwrap_or("")
1097 .cmp(b.get("display_name").and_then(|v| v.as_str()).unwrap_or("")),
1098 }
1099 });
1100
1101 Response::ok(json!({
1102 "metric": metric.name(),
1103 "unit": metric.unit(),
1104 "granularity_seconds": granularity.seconds(),
1105 "window_seconds": window.as_secs(),
1106 "peers": peers,
1107 }))
1108}
1109
1110pub fn dispatch(node: &Node, command: &str, params: Option<&Value>) -> super::protocol::Response {
1112 match command {
1113 "show_acl" => super::protocol::Response::ok(show_acl(node)),
1114 "show_status" => super::protocol::Response::ok(show_status(node)),
1115 "show_peers" => super::protocol::Response::ok(show_peers(node)),
1116 "show_links" => super::protocol::Response::ok(show_links(node)),
1117 "show_tree" => super::protocol::Response::ok(show_tree(node)),
1118 "show_sessions" => super::protocol::Response::ok(show_sessions(node)),
1119 "show_bloom" => super::protocol::Response::ok(show_bloom(node)),
1120 "show_mmp" => super::protocol::Response::ok(show_mmp(node)),
1121 "show_cache" => super::protocol::Response::ok(show_cache(node)),
1122 "show_connections" => super::protocol::Response::ok(show_connections(node)),
1123 "show_transports" => super::protocol::Response::ok(show_transports(node)),
1124 "show_routing" => super::protocol::Response::ok(show_routing(node)),
1125 "show_identity_cache" => super::protocol::Response::ok(show_identity_cache(node)),
1126 "show_stats_list" => super::protocol::Response::ok(show_stats_list()),
1127 "show_stats_history" => show_stats_history(node, params),
1128 "show_stats_all_history" => show_stats_all_history(node, params),
1129 "show_stats_peers" => super::protocol::Response::ok(show_stats_peers(node)),
1130 "show_stats_history_all_peers" => show_stats_history_all_peers(node, params),
1131 _ => super::protocol::Response::error(format!("unknown command: {}", command)),
1132 }
1133}
1134
1135#[cfg(test)]
1136mod tests {
1137 use super::*;
1179 use crate::config::Config;
1180 use crate::identity::Identity;
1181 use crate::node::Node;
1182 use serde_json::{Map, Value, json};
1183 use std::path::PathBuf;
1184
1185 const TEST_SEED: [u8; 32] = [0xAB; 32];
1189
1190 const VOLATILE_KEYS: &[&str] = &[
1194 "version",
1196 "pid",
1197 "exe_path",
1198 "control_socket",
1199 "tun_name",
1200 "allow_file",
1202 "deny_file",
1203 "uptime_secs",
1205 "started_at_ms",
1206 "session_start_ms",
1207 "authenticated_at_ms",
1208 "last_seen_ms",
1209 "last_activity_ms",
1210 "last_recv_ms",
1211 "created_at_ms",
1212 "initiated_ms",
1213 "last_sent_ms",
1214 "age_ms",
1215 "last_used_ms",
1216 "idle_ms",
1217 "first_seen_secs_ago",
1218 "last_contact_secs_ago",
1219 ];
1220
1221 fn build_test_node() -> Node {
1226 let identity =
1227 Identity::from_secret_bytes(&TEST_SEED).expect("test seed is a valid secret key");
1228 let config = Config::new();
1229 Node::with_identity(identity, config).expect("default config is valid")
1230 }
1231
1232 fn normalize_value(value: &mut Value) {
1236 match value {
1237 Value::Object(map) => {
1238 for (key, v) in map.iter_mut() {
1239 if VOLATILE_KEYS.contains(&key.as_str()) {
1240 *v = Value::String("<redacted>".to_string());
1241 } else {
1242 normalize_value(v);
1243 }
1244 }
1245 }
1246 Value::Array(items) => {
1247 for item in items.iter_mut() {
1248 normalize_value(item);
1249 }
1250 }
1251 _ => {}
1252 }
1253 }
1254
1255 fn render(value: Value) -> String {
1259 let mut wrapped = json!({ "status": "ok", "data": value });
1260 normalize_value(&mut wrapped);
1261 let sorted = sort_object_keys(&wrapped);
1262 serde_json::to_string_pretty(&sorted).expect("json serialization is infallible")
1263 }
1264
1265 fn render_response(resp: super::super::protocol::Response) -> String {
1268 let value = serde_json::to_value(&resp).expect("response always serializes");
1269 let mut value = value;
1270 normalize_value(&mut value);
1271 let sorted = sort_object_keys(&value);
1272 serde_json::to_string_pretty(&sorted).expect("json serialization is infallible")
1273 }
1274
1275 fn sort_object_keys(value: &Value) -> Value {
1279 match value {
1280 Value::Object(map) => {
1281 let mut sorted: Map<String, Value> = Map::new();
1282 let mut keys: Vec<&String> = map.keys().collect();
1283 keys.sort();
1284 for key in keys {
1285 sorted.insert(key.clone(), sort_object_keys(&map[key]));
1286 }
1287 Value::Object(sorted)
1288 }
1289 Value::Array(items) => Value::Array(items.iter().map(sort_object_keys).collect()),
1290 other => other.clone(),
1291 }
1292 }
1293
1294 fn snapshot_dir() -> PathBuf {
1295 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1296 .join("src")
1297 .join("control")
1298 .join("snapshots")
1299 }
1300
1301 fn assert_snapshot(name: &str, actual: &str) {
1305 let path = snapshot_dir().join(format!("{name}.json"));
1306 if !path.exists() {
1307 std::fs::create_dir_all(path.parent().unwrap())
1308 .expect("failed to create snapshots dir");
1309 std::fs::write(&path, actual).expect("failed to write new snapshot");
1310 return;
1312 }
1313 let expected = std::fs::read_to_string(&path)
1314 .unwrap_or_else(|e| panic!("failed to read snapshot {}: {e}", path.display()));
1315 let expected = expected.replace("\r\n", "\n");
1318 if expected.trim_end() != actual.trim_end() {
1320 panic!(
1321 "snapshot mismatch for {name}\n\
1322 fixture: {}\n\
1323 -- expected --\n{expected}\n\
1324 -- actual --\n{actual}\n\
1325 -- end --\n\
1326 If the schema change is intentional, delete the fixture \
1327 and re-run to regenerate.",
1328 path.display()
1329 );
1330 }
1331 }
1332
1333 #[test]
1336 fn snapshot_show_status() {
1337 let node = build_test_node();
1338 assert_snapshot("show_status", &render(show_status(&node)));
1339 }
1340
1341 #[test]
1342 fn snapshot_show_acl() {
1343 let node = build_test_node();
1344 assert_snapshot("show_acl", &render(show_acl(&node)));
1345 }
1346
1347 #[test]
1348 fn snapshot_show_peers() {
1349 let node = build_test_node();
1350 assert_snapshot("show_peers", &render(show_peers(&node)));
1351 }
1352
1353 #[test]
1354 fn snapshot_show_links() {
1355 let node = build_test_node();
1356 assert_snapshot("show_links", &render(show_links(&node)));
1357 }
1358
1359 #[test]
1360 fn snapshot_show_tree() {
1361 let node = build_test_node();
1362 assert_snapshot("show_tree", &render(show_tree(&node)));
1363 }
1364
1365 #[test]
1366 fn snapshot_show_sessions() {
1367 let node = build_test_node();
1368 assert_snapshot("show_sessions", &render(show_sessions(&node)));
1369 }
1370
1371 #[test]
1372 fn snapshot_show_bloom() {
1373 let node = build_test_node();
1374 assert_snapshot("show_bloom", &render(show_bloom(&node)));
1375 }
1376
1377 #[test]
1378 fn snapshot_show_mmp() {
1379 let node = build_test_node();
1380 assert_snapshot("show_mmp", &render(show_mmp(&node)));
1381 }
1382
1383 #[test]
1384 fn snapshot_show_cache() {
1385 let node = build_test_node();
1386 assert_snapshot("show_cache", &render(show_cache(&node)));
1387 }
1388
1389 #[test]
1390 fn snapshot_show_connections() {
1391 let node = build_test_node();
1392 assert_snapshot("show_connections", &render(show_connections(&node)));
1393 }
1394
1395 #[test]
1396 fn snapshot_show_transports() {
1397 let node = build_test_node();
1398 assert_snapshot("show_transports", &render(show_transports(&node)));
1399 }
1400
1401 #[test]
1402 fn snapshot_show_routing() {
1403 let node = build_test_node();
1404 assert_snapshot("show_routing", &render(show_routing(&node)));
1405 }
1406
1407 #[test]
1408 fn snapshot_show_identity_cache() {
1409 let node = build_test_node();
1410 assert_snapshot("show_identity_cache", &render(show_identity_cache(&node)));
1411 }
1412
1413 #[test]
1414 fn snapshot_show_stats_list() {
1415 assert_snapshot("show_stats_list", &render(show_stats_list()));
1417 }
1418
1419 #[test]
1420 fn snapshot_show_stats_history() {
1421 let node = build_test_node();
1422 let params = json!({ "metric": "mesh_size", "window": "10s", "granularity": "1s" });
1424 let resp = show_stats_history(&node, Some(¶ms));
1425 assert_snapshot("show_stats_history", &render_response(resp));
1426 }
1427
1428 #[test]
1429 fn snapshot_show_stats_all_history() {
1430 let node = build_test_node();
1431 let params = json!({ "window": "10s", "granularity": "1s" });
1434 let resp = show_stats_all_history(&node, Some(¶ms));
1435 assert_snapshot("show_stats_all_history", &render_response(resp));
1436 }
1437
1438 #[test]
1439 fn snapshot_show_stats_peers() {
1440 let node = build_test_node();
1441 assert_snapshot("show_stats_peers", &render(show_stats_peers(&node)));
1442 }
1443
1444 #[test]
1445 fn snapshot_show_stats_history_all_peers() {
1446 let node = build_test_node();
1447 let params = json!({ "metric": "srtt_ms", "window": "10s", "granularity": "1s" });
1451 let resp = show_stats_history_all_peers(&node, Some(¶ms));
1452 assert_snapshot("show_stats_history_all_peers", &render_response(resp));
1453 }
1454
1455 #[test]
1459 fn dispatch_covers_all_snapshotted_handlers() {
1460 let expected = [
1461 "show_status",
1462 "show_acl",
1463 "show_peers",
1464 "show_links",
1465 "show_tree",
1466 "show_sessions",
1467 "show_bloom",
1468 "show_mmp",
1469 "show_cache",
1470 "show_connections",
1471 "show_transports",
1472 "show_routing",
1473 "show_identity_cache",
1474 "show_stats_list",
1475 "show_stats_history",
1476 "show_stats_all_history",
1477 "show_stats_peers",
1478 "show_stats_history_all_peers",
1479 ];
1480 assert_eq!(expected.len(), 18, "expected exactly 18 query handlers");
1481 let node = build_test_node();
1482 for cmd in expected {
1483 let params = match cmd {
1486 "show_stats_history" => Some(json!({
1487 "metric": "mesh_size", "window": "10s", "granularity": "1s"
1488 })),
1489 "show_stats_all_history" => Some(json!({ "window": "10s", "granularity": "1s" })),
1490 "show_stats_history_all_peers" => Some(json!({
1491 "metric": "srtt_ms", "window": "10s", "granularity": "1s"
1492 })),
1493 _ => None,
1494 };
1495 let resp = dispatch(&node, cmd, params.as_ref());
1496 assert_eq!(
1497 resp.status, "ok",
1498 "dispatch({cmd}) returned status={} message={:?}",
1499 resp.status, resp.message
1500 );
1501 }
1502 }
1503}