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 let retry_state: std::collections::HashMap<_, _> = node
129 .retry_state_iter()
130 .map(|(addr, state)| (*addr, state.retry_after_ms))
131 .collect();
132
133 let peers: Vec<Value> = node
134 .peers()
135 .map(|peer| {
136 let node_addr = *peer.node_addr();
137 let addr_hex = hex::encode(node_addr.as_bytes());
138
139 let is_parent = !is_root && node_addr == parent_id;
141 let is_child = tree
142 .peer_declaration(&node_addr)
143 .is_some_and(|decl| *decl.parent_id() == my_addr);
144
145 let mut peer_json = json!({
146 "node_addr": addr_hex,
147 "npub": peer.npub(),
148 "display_name": node.peer_display_name(&node_addr),
149 "ipv6_addr": format!("{}", peer.address()),
150 "connectivity": format!("{}", peer.connectivity()),
151 "link_id": peer.link_id().as_u64(),
152 "authenticated_at_ms": peer.authenticated_at(),
153 "last_seen_ms": peer.last_seen(),
154 "has_tree_position": peer.has_tree_position(),
155 "has_bloom_filter": peer.filter_sequence() > 0,
156 "filter_sequence": peer.filter_sequence(),
157 "is_parent": is_parent,
158 "is_child": is_child,
159 });
160
161 if let Some(addr) = peer.current_addr() {
163 peer_json["transport_addr"] = json!(format!("{}", addr));
164 }
165
166 let link_id = peer.link_id();
168 if let Some(link) = node.get_link(&link_id) {
169 peer_json["direction"] = json!(format!("{}", link.direction()));
170 let transport_id = link.transport_id();
171 if let Some(handle) = node.get_transport(&transport_id) {
172 peer_json["transport_type"] = json!(handle.transport_type().name);
173 }
174 }
175
176 if let Some(coords) = peer.coords() {
178 peer_json["tree_depth"] = json!(coords.depth());
179 }
180
181 let stats = peer.link_stats();
183 peer_json["stats"] = json!({
184 "packets_sent": stats.packets_sent,
185 "packets_recv": stats.packets_recv,
186 "bytes_sent": stats.bytes_sent,
187 "bytes_recv": stats.bytes_recv,
188 });
189
190 peer_json["replay_suppressed"] = json!(peer.replay_suppressed_count());
192 peer_json["consecutive_decrypt_failures"] = json!(peer.consecutive_decrypt_failures());
193
194 let npub = peer.npub();
199 let mut nostr_obj = json!({
200 "consecutive_failures": 0,
201 "in_cooldown": false,
202 "cooldown_until_ms": Value::Null,
203 "last_observed_skew_ms": Value::Null,
204 "direct_probe_pending": retry_state.contains_key(&node_addr),
205 "direct_probe_after_ms": retry_state
206 .get(&node_addr)
207 .map(|t| json!(t))
208 .unwrap_or(Value::Null),
209 });
210 if let Some(state) = nostr_state.get(&npub) {
211 nostr_obj["consecutive_failures"] = json!(state.consecutive_failures);
212 nostr_obj["in_cooldown"] = json!(state.cooldown_until_ms.is_some());
213 nostr_obj["cooldown_until_ms"] = state
214 .cooldown_until_ms
215 .map(|t| json!(t))
216 .unwrap_or(Value::Null);
217 nostr_obj["last_observed_skew_ms"] = state
218 .last_observed_skew_ms
219 .map(|s| json!(s))
220 .unwrap_or(Value::Null);
221 }
222 peer_json["nostr_traversal"] = nostr_obj;
223
224 if let Some(session) = peer.noise_session() {
226 peer_json["noise"] = json!({
227 "send_counter": session.current_send_counter(),
228 "highest_recv_counter": session.highest_received_counter(),
229 });
230 }
231
232 if let Some(idx) = peer.our_index() {
234 peer_json["our_session_index"] = json!(format!("{:08x}", idx.as_u32()));
235 }
236
237 if peer.rekey_in_progress() {
239 peer_json["rekey_in_progress"] = json!(true);
240 }
241 if peer.is_draining() {
242 peer_json["rekey_draining"] = json!(true);
243 }
244 peer_json["current_k_bit"] = json!(peer.current_k_bit());
245
246 if let Some(mmp) = peer.mmp() {
248 let mut mmp_json = json!({
249 "mode": format!("{}", mmp.mode()),
250 });
251 if let Some(srtt) = mmp.metrics.srtt_ms() {
252 mmp_json["srtt_ms"] = json!(srtt);
253 }
254 mmp_json["loss_rate"] = json!(mmp.metrics.loss_rate());
255 mmp_json["etx"] = json!(mmp.metrics.etx);
256 mmp_json["goodput_bps"] = json!(mmp.metrics.goodput_bps);
257 mmp_json["delivery_ratio_forward"] = json!(mmp.metrics.delivery_ratio_forward);
258 mmp_json["delivery_ratio_reverse"] = json!(mmp.metrics.delivery_ratio_reverse);
259 if let Some(smoothed_loss) = mmp.metrics.smoothed_loss() {
260 mmp_json["smoothed_loss"] = json!(smoothed_loss);
261 }
262 if let Some(smoothed_etx) = mmp.metrics.smoothed_etx() {
263 mmp_json["smoothed_etx"] = json!(smoothed_etx);
264 }
265 if let Some(srtt) = mmp.metrics.srtt_ms()
266 && let Some(setx) = mmp.metrics.smoothed_etx()
267 {
268 mmp_json["lqi"] = json!(setx * (1.0 + srtt / 100.0));
269 }
270 peer_json["mmp"] = mmp_json;
271 }
272
273 peer_json
274 })
275 .collect();
276
277 json!({ "peers": peers })
278}
279
280pub fn show_links(node: &Node) -> Value {
282 let links: Vec<Value> = node
283 .links()
284 .map(|link| {
285 let stats = link.stats();
286 json!({
287 "link_id": link.link_id().as_u64(),
288 "transport_id": link.transport_id().as_u32(),
289 "remote_addr": format!("{}", link.remote_addr()),
290 "direction": format!("{}", link.direction()),
291 "state": format!("{}", link.state()),
292 "created_at_ms": link.created_at(),
293 "stats": {
294 "packets_sent": stats.packets_sent,
295 "packets_recv": stats.packets_recv,
296 "bytes_sent": stats.bytes_sent,
297 "bytes_recv": stats.bytes_recv,
298 "last_recv_ms": stats.last_recv_ms,
299 },
300 })
301 })
302 .collect();
303
304 json!({ "links": links })
305}
306
307pub fn show_tree(node: &Node) -> Value {
309 let tree = node.tree_state();
310 let my_coords = tree.my_coords();
311 let decl = tree.my_declaration();
312
313 let coords: Vec<String> = my_coords
315 .entries()
316 .iter()
317 .map(|e| hex::encode(e.node_addr.as_bytes()))
318 .collect();
319
320 let peers: Vec<Value> = tree
322 .peer_ids()
323 .map(|peer_id| {
324 let mut peer_json = json!({
325 "node_addr": hex::encode(peer_id.as_bytes()),
326 "display_name": node.peer_display_name(peer_id),
327 });
328 if let Some(coords) = tree.peer_coords(peer_id) {
329 let coord_path: Vec<String> = coords
330 .entries()
331 .iter()
332 .map(|e| hex::encode(e.node_addr.as_bytes()))
333 .collect();
334 peer_json["depth"] = json!(coords.depth());
335 peer_json["root"] = json!(hex::encode(coords.root_id().as_bytes()));
336 peer_json["coords"] = json!(coord_path);
337 peer_json["distance_to_us"] = json!(my_coords.distance_to(coords));
338 }
339 peer_json
340 })
341 .collect();
342
343 let parent_addr = my_coords.parent_id();
345 let parent_hex = hex::encode(parent_addr.as_bytes());
346 let parent_display = node.peer_display_name(parent_addr);
347
348 let tree_stats = node.stats().snapshot().tree;
349
350 json!({
351 "my_node_addr": hex::encode(tree.my_node_addr().as_bytes()),
352 "root": hex::encode(tree.root().as_bytes()),
353 "is_root": tree.is_root(),
354 "depth": my_coords.depth(),
355 "my_coords": coords,
356 "parent": parent_hex,
357 "parent_display_name": parent_display,
358 "declaration_sequence": decl.sequence(),
359 "declaration_signed": decl.is_signed(),
360 "peer_tree_count": tree.peer_count(),
361 "peers": peers,
362 "stats": serde_json::to_value(&tree_stats).unwrap_or_default(),
363 })
364}
365
366pub fn show_sessions(node: &Node) -> Value {
368 let sessions: Vec<Value> = node
369 .session_entries()
370 .map(|(addr, entry)| {
371 let state_str = if entry.is_established() {
372 "established"
373 } else if entry.is_initiating() {
374 "initiating"
375 } else if entry.is_awaiting_msg3() {
376 "awaiting_msg3"
377 } else {
378 "unknown"
379 };
380
381 let mut session_json = json!({
382 "remote_addr": hex::encode(addr.as_bytes()),
383 "display_name": node.peer_display_name(addr),
384 "state": state_str,
385 "is_initiator": entry.is_initiator(),
386 "last_activity_ms": entry.last_activity(),
387 });
388
389 let (xonly, _parity) = entry.remote_pubkey().x_only_public_key();
391 session_json["npub"] = json!(encode_npub(&xonly));
392
393 let (pkts_tx, pkts_rx, bytes_tx, bytes_rx) = entry.traffic_counters();
395 session_json["stats"] = json!({
396 "packets_sent": pkts_tx,
397 "packets_recv": pkts_rx,
398 "bytes_sent": bytes_tx,
399 "bytes_recv": bytes_rx,
400 });
401
402 if !entry.is_established() {
404 session_json["resend_count"] = json!(entry.resend_count());
405 }
406
407 if entry.is_established() {
409 session_json["session_start_ms"] = json!(entry.session_start_ms());
410 session_json["current_k_bit"] = json!(entry.current_k_bit());
411 session_json["coords_warmup_remaining"] = json!(entry.coords_warmup_remaining());
412 session_json["is_draining"] = json!(entry.is_draining());
413 }
414
415 if let Some(mmp) = entry.mmp() {
417 let mut mmp_json = json!({
418 "mode": format!("{}", mmp.mode()),
419 "loss_rate": mmp.metrics.loss_rate(),
420 "etx": mmp.metrics.etx,
421 "goodput_bps": mmp.metrics.goodput_bps,
422 "delivery_ratio_forward": mmp.metrics.delivery_ratio_forward,
423 "delivery_ratio_reverse": mmp.metrics.delivery_ratio_reverse,
424 "path_mtu": mmp.path_mtu.current_mtu(),
425 });
426 if let Some(srtt) = mmp.metrics.srtt_ms() {
427 mmp_json["srtt_ms"] = json!(srtt);
428 }
429 if let Some(smoothed_loss) = mmp.metrics.smoothed_loss() {
430 mmp_json["smoothed_loss"] = json!(smoothed_loss);
431 }
432 if let Some(smoothed_etx) = mmp.metrics.smoothed_etx() {
433 mmp_json["smoothed_etx"] = json!(smoothed_etx);
434 }
435 if let Some(srtt) = mmp.metrics.srtt_ms()
436 && let Some(setx) = mmp.metrics.smoothed_etx()
437 {
438 mmp_json["sqi"] = json!(setx * (1.0 + srtt / 100.0));
439 }
440 session_json["mmp"] = mmp_json;
441 }
442
443 session_json
444 })
445 .collect();
446
447 json!({ "sessions": sessions })
448}
449
450pub fn show_bloom(node: &Node) -> Value {
452 let bloom = node.bloom_state();
453
454 let leaf_deps: Vec<String> = bloom
455 .leaf_dependents()
456 .iter()
457 .map(|addr| hex::encode(addr.as_bytes()))
458 .collect();
459
460 let peer_filters: Vec<Value> = node
462 .peers()
463 .map(|peer| {
464 let addr = *peer.node_addr();
465 let mut pf = json!({
466 "peer": hex::encode(addr.as_bytes()),
467 "display_name": node.peer_display_name(&addr),
468 "has_filter": peer.filter_sequence() > 0,
469 "filter_sequence": peer.filter_sequence(),
470 });
471 if let Some(filter) = peer.inbound_filter() {
472 let max_fpr = node.config().node.bloom.max_inbound_fpr;
473 pf["estimated_count"] = json!(filter.estimated_count(max_fpr));
474 pf["set_bits"] = json!(filter.count_ones());
475 pf["fill_ratio"] = json!(filter.fill_ratio());
476 }
477 pf
478 })
479 .collect();
480
481 let bloom_stats = node.stats().snapshot().bloom;
482
483 json!({
484 "own_node_addr": hex::encode(node.node_addr().as_bytes()),
485 "is_leaf_only": node.is_leaf_only(),
486 "sequence": bloom.sequence(),
487 "leaf_dependent_count": bloom.leaf_dependents().len(),
488 "leaf_dependents": leaf_deps,
489 "peer_filters": peer_filters,
490 "stats": serde_json::to_value(&bloom_stats).unwrap_or_default(),
491 })
492}
493
494pub fn show_mmp(node: &Node) -> Value {
496 let peers: Vec<Value> = node.peers().filter_map(|peer| {
498 let mmp = peer.mmp()?;
499 let addr = *peer.node_addr();
500 let metrics = &mmp.metrics;
501
502 let mut link_layer = json!({
503 "loss_rate": metrics.loss_rate(),
504 "etx": metrics.etx,
505 "goodput_bps": metrics.goodput_bps,
506 "spin_bit_role": if mmp.spin_bit.is_initiator() { "initiator" } else { "responder" },
507 });
508
509 if let Some(smoothed_loss) = metrics.smoothed_loss() {
510 link_layer["smoothed_loss"] = json!(smoothed_loss);
511 }
512 if let Some(smoothed_etx) = metrics.smoothed_etx() {
513 link_layer["smoothed_etx"] = json!(smoothed_etx);
514 }
515 if let Some(srtt) = metrics.srtt_ms() {
516 link_layer["srtt_ms"] = json!(srtt);
517 if let Some(setx) = metrics.smoothed_etx() {
518 link_layer["lqi"] = json!(setx * (1.0 + srtt / 100.0));
519 }
520 }
521
522 if metrics.rtt_trend.initialized() {
524 link_layer["rtt_trend"] = json!(trend_label(metrics.rtt_trend.short(), metrics.rtt_trend.long()));
525 }
526 if metrics.loss_trend.initialized() {
527 link_layer["loss_trend"] = json!(trend_label(metrics.loss_trend.short(), metrics.loss_trend.long()));
528 }
529 if metrics.goodput_trend.initialized() {
530 link_layer["goodput_trend"] = json!(trend_label(metrics.goodput_trend.short(), metrics.goodput_trend.long()));
531 }
532 if metrics.jitter_trend.initialized() {
533 link_layer["jitter_trend"] = json!(trend_label(metrics.jitter_trend.short(), metrics.jitter_trend.long()));
534 }
535
536 link_layer["delivery_ratio_forward"] = json!(metrics.delivery_ratio_forward);
537 link_layer["delivery_ratio_reverse"] = json!(metrics.delivery_ratio_reverse);
538 link_layer["ecn_ce_count"] = json!(metrics.last_ecn_ce_count());
539
540 Some(json!({
541 "peer": hex::encode(addr.as_bytes()),
542 "display_name": node.peer_display_name(&addr),
543 "mode": format!("{}", mmp.mode()),
544 "link_layer": link_layer,
545 }))
546 }).collect();
547
548 let sessions: Vec<Value> = node
550 .session_entries()
551 .filter_map(|(addr, entry)| {
552 let mmp = entry.mmp()?;
553 let metrics = &mmp.metrics;
554
555 let mut session_layer = json!({
556 "loss_rate": metrics.loss_rate(),
557 "etx": metrics.etx,
558 "path_mtu": mmp.path_mtu.current_mtu(),
559 });
560
561 if let Some(smoothed_loss) = metrics.smoothed_loss() {
562 session_layer["smoothed_loss"] = json!(smoothed_loss);
563 }
564 if let Some(smoothed_etx) = metrics.smoothed_etx() {
565 session_layer["smoothed_etx"] = json!(smoothed_etx);
566 }
567 if let Some(srtt) = metrics.srtt_ms() {
568 session_layer["srtt_ms"] = json!(srtt);
569 if let Some(setx) = metrics.smoothed_etx() {
570 session_layer["sqi"] = json!(setx * (1.0 + srtt / 100.0));
571 }
572 }
573
574 Some(json!({
575 "remote": hex::encode(addr.as_bytes()),
576 "display_name": node.peer_display_name(addr),
577 "mode": format!("{}", mmp.mode()),
578 "session_layer": session_layer,
579 }))
580 })
581 .collect();
582
583 json!({
584 "peers": peers,
585 "sessions": sessions,
586 })
587}
588
589pub fn show_cache(node: &Node) -> Value {
591 let cache = node.coord_cache();
592 let now = now_ms();
593 let stats = cache.stats(now);
594
595 let entries: Vec<Value> = cache
597 .iter(now)
598 .map(|(addr, entry)| {
599 let fips_addr = crate::identity::FipsAddress::from_node_addr(addr);
600 let coord_path: Vec<String> = entry
601 .coords()
602 .entries()
603 .iter()
604 .map(|e| hex::encode(e.node_addr.as_bytes()))
605 .collect();
606 let mut entry_json = json!({
607 "node_addr": hex::encode(addr.as_bytes()),
608 "display_name": node.peer_display_name(addr),
609 "ipv6_addr": format!("{}", fips_addr),
610 "depth": entry.coords().depth(),
611 "coords": coord_path,
612 "age_ms": now.saturating_sub(entry.created_at()),
613 "last_used_ms": entry.last_used(),
614 });
615 if let Some(mtu) = entry.path_mtu() {
616 entry_json["path_mtu"] = json!(mtu);
617 }
618 entry_json
619 })
620 .collect();
621
622 json!({
623 "count": stats.entries,
624 "max_entries": stats.max_entries,
625 "fill_ratio": stats.fill_ratio(),
626 "default_ttl_ms": cache.default_ttl_ms(),
627 "expired": stats.expired,
628 "avg_age_ms": stats.avg_age_ms,
629 "entries": entries,
630 })
631}
632
633pub fn show_connections(node: &Node) -> Value {
635 let now = now_ms();
636 let connections: Vec<Value> = node
637 .connections()
638 .map(|conn| {
639 let mut conn_json = json!({
640 "link_id": conn.link_id().as_u64(),
641 "direction": format!("{}", conn.direction()),
642 "handshake_state": format!("{}", conn.handshake_state()),
643 "started_at_ms": conn.started_at(),
644 "idle_ms": now.saturating_sub(conn.last_activity()),
645 "resend_count": conn.resend_count(),
646 });
647
648 if let Some(identity) = conn.expected_identity() {
649 conn_json["expected_peer"] = json!(identity.npub());
650 }
651
652 conn_json
653 })
654 .collect();
655
656 json!({ "connections": connections })
657}
658
659pub fn show_transports(node: &Node) -> Value {
661 let transports: Vec<Value> = node
662 .transport_ids()
663 .map(|id| {
664 let handle = node.get_transport(id).unwrap();
665 let mut t_json = json!({
666 "transport_id": id.as_u32(),
667 "type": handle.transport_type().name,
668 "state": format!("{}", handle.state()),
669 "mtu": handle.mtu(),
670 });
671
672 if let Some(name) = handle.name() {
673 t_json["name"] = json!(name);
674 }
675 if let Some(addr) = handle.local_addr() {
676 t_json["local_addr"] = json!(format!("{}", addr));
677 }
678
679 if let Some(mode) = handle.tor_mode() {
681 t_json["tor_mode"] = json!(mode);
682 }
683 if let Some(onion) = handle.onion_address() {
684 t_json["onion_address"] = json!(onion);
685 }
686 if let Some(monitoring) = handle.tor_monitoring() {
687 t_json["tor_monitoring"] = serde_json::to_value(&monitoring).unwrap_or_default();
688 }
689
690 t_json["stats"] = handle.transport_stats();
691
692 t_json
693 })
694 .collect();
695
696 json!({ "transports": transports })
697}
698
699pub fn show_routing(node: &Node) -> Value {
701 let cache = node.coord_cache();
702 let now = now_ms();
703 let cache_stats = cache.stats(now);
704 let node_stats = node.stats().snapshot();
705 let learned_routes = node.learned_route_table_snapshot(now);
706
707 let lookups: Vec<Value> = node
709 .pending_lookups_iter()
710 .map(|(addr, lookup)| {
711 json!({
712 "target": hex::encode(addr.as_bytes()),
713 "display_name": node.peer_display_name(addr),
714 "initiated_ms": lookup.initiated_ms,
715 "last_sent_ms": lookup.last_sent_ms,
716 "attempt": lookup.attempt,
717 "age_ms": now.saturating_sub(lookup.initiated_ms),
718 })
719 })
720 .collect();
721
722 let retries: Vec<Value> = node
724 .retry_state_iter()
725 .map(|(addr, state)| {
726 json!({
727 "node_addr": hex::encode(addr.as_bytes()),
728 "display_name": node.peer_display_name(addr),
729 "retry_count": state.retry_count,
730 "retry_after_ms": state.retry_after_ms,
731 "auto_reconnect": state.reconnect,
732 })
733 })
734 .collect();
735
736 json!({
737 "coord_cache_entries": cache_stats.entries,
738 "routing_mode": node.config().node.routing.mode.to_string(),
739 "learned_routes": serde_json::to_value(&learned_routes).unwrap_or_default(),
740 "identity_cache_entries": node.identity_cache_len(),
741 "pending_lookups": lookups,
742 "pending_tun_destinations": node.pending_tun_destinations(),
743 "pending_tun_packets": node.pending_tun_total_packets(),
744 "recent_requests": node.recent_request_count(),
745 "retries": retries,
746 "forwarding": serde_json::to_value(&node_stats.forwarding).unwrap_or_default(),
747 "discovery": serde_json::to_value(&node_stats.discovery).unwrap_or_default(),
748 "error_signals": serde_json::to_value(&node_stats.errors).unwrap_or_default(),
749 "congestion": serde_json::to_value(&node_stats.congestion).unwrap_or_default(),
750 })
751}
752
753pub fn show_identity_cache(node: &Node) -> Value {
760 let now = now_ms();
761 let entries: Vec<Value> = node
762 .identity_cache_iter()
763 .map(|(node_addr, pubkey, last_seen_ms)| {
764 let (xonly, _parity) = pubkey.x_only_public_key();
765 let fips_addr = crate::identity::FipsAddress::from_node_addr(node_addr);
766 json!({
767 "node_addr": hex::encode(node_addr.as_bytes()),
768 "npub": encode_npub(&xonly),
769 "display_name": node.peer_display_name(node_addr),
770 "ipv6_addr": format!("{}", fips_addr),
771 "last_seen_ms": last_seen_ms,
772 "age_ms": now.saturating_sub(last_seen_ms),
773 })
774 })
775 .collect();
776 let count = entries.len();
777
778 json!({
779 "entries": entries,
780 "count": count,
781 "max_entries": node.identity_cache_max(),
782 })
783}
784
785pub fn show_stats_list() -> Value {
787 let metrics: Vec<Value> = ALL_METRICS
788 .iter()
789 .map(|m| {
790 json!({
791 "name": m.name(),
792 "unit": m.unit(),
793 "scope": "node",
794 })
795 })
796 .chain(ALL_PEER_METRICS.iter().map(|m| {
797 json!({
798 "name": m.name(),
799 "unit": m.unit(),
800 "scope": "peer",
801 })
802 }))
803 .collect();
804 json!({
805 "metrics": metrics,
806 "fast_ring_seconds": crate::node::stats_history::FAST_RING_CAPACITY,
807 "slow_ring_minutes": crate::node::stats_history::SLOW_RING_CAPACITY,
808 "peer_retention_seconds": crate::node::stats_history::PEER_EVICTION_SECS,
809 })
810}
811
812pub fn show_stats_history(node: &Node, params: Option<&Value>) -> super::protocol::Response {
824 use super::protocol::Response;
825 let Some(params) = params else {
826 return Response::error("missing params for show_stats_history");
827 };
828
829 let metric_name = match params.get("metric").and_then(|v| v.as_str()) {
830 Some(v) => v,
831 None => return Response::error("missing 'metric' parameter"),
832 };
833
834 let window_str = params
835 .get("window")
836 .and_then(|v| v.as_str())
837 .unwrap_or("10m");
838 let window = match parse_duration(window_str) {
839 Ok(d) => d,
840 Err(e) => return Response::error(e),
841 };
842
843 let granularity_str = params
844 .get("granularity")
845 .and_then(|v| v.as_str())
846 .unwrap_or("1s");
847 let granularity = match Granularity::from_str(granularity_str) {
848 Ok(g) => g,
849 Err(e) => return Response::error(e),
850 };
851
852 let peer_npub = params.get("peer").and_then(|v| v.as_str());
853 let hist = node.stats_history();
854
855 if let Some(npub) = peer_npub {
856 let addr = match parse_peer_npub(npub) {
857 Ok(a) => a,
858 Err(e) => return Response::error(e),
859 };
860 let peer_metric = match PeerMetric::from_str(metric_name) {
861 Ok(m) => m,
862 Err(e) => return Response::error(e),
863 };
864 match hist.peer_query(&addr, peer_metric, window, granularity) {
865 Some(series) => Response::ok(serde_json::to_value(&series).unwrap_or(Value::Null)),
866 None => Response::error(format!(
867 "peer not tracked in stats history: {}",
868 node.peer_display_name(&addr)
869 )),
870 }
871 } else {
872 let metric = match Metric::from_str(metric_name) {
873 Ok(m) => m,
874 Err(e) => return Response::error(e),
875 };
876 let series = hist.query(metric, window, granularity);
877 Response::ok(serde_json::to_value(&series).unwrap_or(Value::Null))
878 }
879}
880
881fn parse_duration(s: &str) -> Result<Duration, String> {
883 if s.is_empty() {
884 return Err("empty duration".to_string());
885 }
886 let (num_part, unit) = s.split_at(s.len() - 1);
887 let n: u64 = num_part
888 .parse()
889 .map_err(|_| format!("invalid duration: {s}"))?;
890 let secs = match unit {
891 "s" => n,
892 "m" => n * 60,
893 "h" => n * 3600,
894 _ => return Err(format!("unknown duration unit: {unit} (expected s, m, h)")),
895 };
896 Ok(Duration::from_secs(secs))
897}
898
899pub fn show_stats_all_history(node: &Node, params: Option<&Value>) -> super::protocol::Response {
907 use super::protocol::Response;
908 let params = params.cloned().unwrap_or_else(|| json!({}));
909
910 let window_str = params
911 .get("window")
912 .and_then(|v| v.as_str())
913 .unwrap_or("10m");
914 let window = match parse_duration(window_str) {
915 Ok(d) => d,
916 Err(e) => return Response::error(e),
917 };
918
919 let granularity_str = params
920 .get("granularity")
921 .and_then(|v| v.as_str())
922 .unwrap_or("1s");
923 let granularity = match Granularity::from_str(granularity_str) {
924 Ok(g) => g,
925 Err(e) => return Response::error(e),
926 };
927
928 let peer_npub = params.get("peer").and_then(|v| v.as_str());
929 let hist = node.stats_history();
930
931 let series: Vec<Value> = if let Some(npub) = peer_npub {
932 let addr = match parse_peer_npub(npub) {
933 Ok(a) => a,
934 Err(e) => return Response::error(e),
935 };
936 if !hist.has_peer(&addr) {
937 return Response::error(format!(
938 "peer not tracked in stats history: {}",
939 node.peer_display_name(&addr)
940 ));
941 }
942 ALL_PEER_METRICS
943 .iter()
944 .map(|m| {
945 let s = hist
946 .peer_query(&addr, *m, window, granularity)
947 .unwrap_or_else(|| {
948 crate::node::stats_history::Series {
951 metric: m.name(),
952 unit: m.unit(),
953 granularity_seconds: granularity.seconds(),
954 values: Vec::new(),
955 }
956 });
957 serde_json::to_value(&s).unwrap_or(Value::Null)
958 })
959 .collect()
960 } else {
961 ALL_METRICS
962 .iter()
963 .map(|m| {
964 let s = hist.query(*m, window, granularity);
965 serde_json::to_value(&s).unwrap_or(Value::Null)
966 })
967 .collect()
968 };
969
970 Response::ok(json!({
971 "granularity_seconds": granularity.seconds(),
972 "window_seconds": window.as_secs(),
973 "peer": peer_npub,
974 "series": series,
975 }))
976}
977
978pub fn show_stats_peers(node: &Node) -> Value {
982 let hist = node.stats_history();
983 let now = std::time::Instant::now();
984
985 let mut peers: Vec<Value> = hist
986 .peers()
987 .map(|(addr, rings)| {
988 let last_contact_secs = now.duration_since(rings.last_contact()).as_secs();
989 let first_seen_secs = now.duration_since(rings.first_seen()).as_secs();
990 let is_active = node.peers().any(|p| p.node_addr() == addr);
991 let npub = node
992 .peers()
993 .find(|p| p.node_addr() == addr)
994 .map(|p| p.npub())
995 .unwrap_or_else(|| hex::encode(addr.as_bytes()));
996 json!({
997 "npub": npub,
998 "node_addr": hex::encode(addr.as_bytes()),
999 "display_name": node.peer_display_name(addr),
1000 "is_active": is_active,
1001 "first_seen_secs_ago": first_seen_secs,
1002 "last_contact_secs_ago": last_contact_secs,
1003 })
1004 })
1005 .collect();
1006
1007 peers.sort_by(|a, b| {
1009 let a_active = a
1010 .get("is_active")
1011 .and_then(|v| v.as_bool())
1012 .unwrap_or(false);
1013 let b_active = b
1014 .get("is_active")
1015 .and_then(|v| v.as_bool())
1016 .unwrap_or(false);
1017 match (b_active, a_active) {
1018 (true, false) => std::cmp::Ordering::Greater,
1019 (false, true) => std::cmp::Ordering::Less,
1020 _ => a
1021 .get("display_name")
1022 .and_then(|v| v.as_str())
1023 .unwrap_or("")
1024 .cmp(b.get("display_name").and_then(|v| v.as_str()).unwrap_or("")),
1025 }
1026 });
1027
1028 json!({ "peers": peers, "count": peers.len() })
1029}
1030
1031pub fn show_stats_history_all_peers(
1037 node: &Node,
1038 params: Option<&Value>,
1039) -> super::protocol::Response {
1040 use super::protocol::Response;
1041 let Some(params) = params else {
1042 return Response::error("missing params for show_stats_history_all_peers");
1043 };
1044
1045 let metric_name = match params.get("metric").and_then(|v| v.as_str()) {
1046 Some(v) => v,
1047 None => return Response::error("missing 'metric' parameter"),
1048 };
1049 let metric = match PeerMetric::from_str(metric_name) {
1050 Ok(m) => m,
1051 Err(e) => return Response::error(e),
1052 };
1053
1054 let window_str = params
1055 .get("window")
1056 .and_then(|v| v.as_str())
1057 .unwrap_or("10m");
1058 let window = match parse_duration(window_str) {
1059 Ok(d) => d,
1060 Err(e) => return Response::error(e),
1061 };
1062
1063 let granularity_str = params
1064 .get("granularity")
1065 .and_then(|v| v.as_str())
1066 .unwrap_or("1s");
1067 let granularity = match Granularity::from_str(granularity_str) {
1068 Ok(g) => g,
1069 Err(e) => return Response::error(e),
1070 };
1071
1072 let hist = node.stats_history();
1073 let peer_addrs: Vec<NodeAddr> = hist.peer_addrs().copied().collect();
1074
1075 let mut peers: Vec<Value> = peer_addrs
1076 .iter()
1077 .filter_map(|addr| {
1078 let s = hist.peer_query(addr, metric, window, granularity)?;
1079 let is_active = node.peers().any(|p| p.node_addr() == addr);
1080 Some(json!({
1081 "node_addr": hex::encode(addr.as_bytes()),
1082 "display_name": node.peer_display_name(addr),
1083 "is_active": is_active,
1084 "values": serde_json::to_value(&s.values).unwrap_or(Value::Null),
1085 }))
1086 })
1087 .collect();
1088
1089 peers.sort_by(|a, b| {
1091 let a_active = a
1092 .get("is_active")
1093 .and_then(|v| v.as_bool())
1094 .unwrap_or(false);
1095 let b_active = b
1096 .get("is_active")
1097 .and_then(|v| v.as_bool())
1098 .unwrap_or(false);
1099 match (b_active, a_active) {
1100 (true, false) => std::cmp::Ordering::Greater,
1101 (false, true) => std::cmp::Ordering::Less,
1102 _ => a
1103 .get("display_name")
1104 .and_then(|v| v.as_str())
1105 .unwrap_or("")
1106 .cmp(b.get("display_name").and_then(|v| v.as_str()).unwrap_or("")),
1107 }
1108 });
1109
1110 Response::ok(json!({
1111 "metric": metric.name(),
1112 "unit": metric.unit(),
1113 "granularity_seconds": granularity.seconds(),
1114 "window_seconds": window.as_secs(),
1115 "peers": peers,
1116 }))
1117}
1118
1119pub fn show_listening_sockets(node: &Node) -> Value {
1122 let fips0 = crate::FipsAddress::from_node_addr(node.identity().node_addr()).to_ipv6();
1123 #[cfg(test)]
1124 let sockets: Vec<super::listening::ListeningSocket> = Vec::new();
1125 #[cfg(not(test))]
1126 let sockets = super::listening::enumerate(fips0);
1127 #[cfg(test)]
1128 let classifier = super::firewall_state::FilterClassifier::no_firewall();
1129 #[cfg(not(test))]
1130 let classifier = super::firewall_state::FilterClassifier::query();
1131
1132 let rows: Vec<Value> = sockets
1133 .iter()
1134 .map(|socket| {
1135 let filter = classifier.classify(socket.proto, socket.port);
1136 json!({
1137 "proto": socket.proto.as_str(),
1138 "local_addr": socket.local_addr.to_string(),
1139 "port": socket.port,
1140 "pid": socket.pid,
1141 "process": socket.process,
1142 "filter": filter.as_str(),
1143 "wildcard_bind": socket.wildcard_bind,
1144 })
1145 })
1146 .collect();
1147
1148 json!({
1149 "fips0_addr": fips0.to_string(),
1150 "firewall_active": classifier.is_active(),
1151 "sockets": rows,
1152 })
1153}
1154
1155pub fn dispatch(node: &Node, command: &str, params: Option<&Value>) -> super::protocol::Response {
1157 match command {
1158 "show_acl" => super::protocol::Response::ok(show_acl(node)),
1159 "show_status" => super::protocol::Response::ok(show_status(node)),
1160 "show_peers" => super::protocol::Response::ok(show_peers(node)),
1161 "show_links" => super::protocol::Response::ok(show_links(node)),
1162 "show_tree" => super::protocol::Response::ok(show_tree(node)),
1163 "show_sessions" => super::protocol::Response::ok(show_sessions(node)),
1164 "show_bloom" => super::protocol::Response::ok(show_bloom(node)),
1165 "show_mmp" => super::protocol::Response::ok(show_mmp(node)),
1166 "show_cache" => super::protocol::Response::ok(show_cache(node)),
1167 "show_connections" => super::protocol::Response::ok(show_connections(node)),
1168 "show_transports" => super::protocol::Response::ok(show_transports(node)),
1169 "show_routing" => super::protocol::Response::ok(show_routing(node)),
1170 "show_identity_cache" => super::protocol::Response::ok(show_identity_cache(node)),
1171 "show_listening_sockets" => super::protocol::Response::ok(show_listening_sockets(node)),
1172 "show_stats_list" => super::protocol::Response::ok(show_stats_list()),
1173 "show_stats_history" => show_stats_history(node, params),
1174 "show_stats_all_history" => show_stats_all_history(node, params),
1175 "show_stats_peers" => super::protocol::Response::ok(show_stats_peers(node)),
1176 "show_stats_history_all_peers" => show_stats_history_all_peers(node, params),
1177 _ => super::protocol::Response::error(format!("unknown command: {}", command)),
1178 }
1179}
1180
1181#[cfg(test)]
1182mod tests {
1183 use super::*;
1225 use crate::config::Config;
1226 use crate::identity::Identity;
1227 use crate::node::Node;
1228 use serde_json::{Map, Value, json};
1229 use std::path::PathBuf;
1230
1231 const TEST_SEED: [u8; 32] = [0xAB; 32];
1235
1236 const VOLATILE_KEYS: &[&str] = &[
1240 "version",
1242 "pid",
1243 "exe_path",
1244 "control_socket",
1245 "tun_name",
1246 "allow_file",
1248 "deny_file",
1249 "uptime_secs",
1251 "started_at_ms",
1252 "session_start_ms",
1253 "authenticated_at_ms",
1254 "last_seen_ms",
1255 "last_activity_ms",
1256 "last_recv_ms",
1257 "created_at_ms",
1258 "initiated_ms",
1259 "last_sent_ms",
1260 "age_ms",
1261 "last_used_ms",
1262 "idle_ms",
1263 "first_seen_secs_ago",
1264 "last_contact_secs_ago",
1265 ];
1266
1267 fn build_test_node() -> Node {
1272 let identity =
1273 Identity::from_secret_bytes(&TEST_SEED).expect("test seed is a valid secret key");
1274 let config = Config::new();
1275 Node::with_identity(identity, config).expect("default config is valid")
1276 }
1277
1278 fn normalize_value(value: &mut Value) {
1282 match value {
1283 Value::Object(map) => {
1284 for (key, v) in map.iter_mut() {
1285 if VOLATILE_KEYS.contains(&key.as_str()) {
1286 *v = Value::String("<redacted>".to_string());
1287 } else {
1288 normalize_value(v);
1289 }
1290 }
1291 }
1292 Value::Array(items) => {
1293 for item in items.iter_mut() {
1294 normalize_value(item);
1295 }
1296 }
1297 _ => {}
1298 }
1299 }
1300
1301 fn render(value: Value) -> String {
1305 let mut wrapped = json!({ "status": "ok", "data": value });
1306 normalize_value(&mut wrapped);
1307 let sorted = sort_object_keys(&wrapped);
1308 serde_json::to_string_pretty(&sorted).expect("json serialization is infallible")
1309 }
1310
1311 fn render_response(resp: super::super::protocol::Response) -> String {
1314 let value = serde_json::to_value(&resp).expect("response always serializes");
1315 let mut value = value;
1316 normalize_value(&mut value);
1317 let sorted = sort_object_keys(&value);
1318 serde_json::to_string_pretty(&sorted).expect("json serialization is infallible")
1319 }
1320
1321 fn sort_object_keys(value: &Value) -> Value {
1325 match value {
1326 Value::Object(map) => {
1327 let mut sorted: Map<String, Value> = Map::new();
1328 let mut keys: Vec<&String> = map.keys().collect();
1329 keys.sort();
1330 for key in keys {
1331 sorted.insert(key.clone(), sort_object_keys(&map[key]));
1332 }
1333 Value::Object(sorted)
1334 }
1335 Value::Array(items) => Value::Array(items.iter().map(sort_object_keys).collect()),
1336 other => other.clone(),
1337 }
1338 }
1339
1340 fn snapshot_dir() -> PathBuf {
1341 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1342 .join("src")
1343 .join("control")
1344 .join("snapshots")
1345 }
1346
1347 fn assert_snapshot(name: &str, actual: &str) {
1351 let path = snapshot_dir().join(format!("{name}.json"));
1352 if !path.exists() {
1353 std::fs::create_dir_all(path.parent().unwrap())
1354 .expect("failed to create snapshots dir");
1355 std::fs::write(&path, actual).expect("failed to write new snapshot");
1356 return;
1358 }
1359 let expected = std::fs::read_to_string(&path)
1360 .unwrap_or_else(|e| panic!("failed to read snapshot {}: {e}", path.display()));
1361 let expected = expected.replace("\r\n", "\n");
1364 if expected.trim_end() != actual.trim_end() {
1366 panic!(
1367 "snapshot mismatch for {name}\n\
1368 fixture: {}\n\
1369 -- expected --\n{expected}\n\
1370 -- actual --\n{actual}\n\
1371 -- end --\n\
1372 If the schema change is intentional, delete the fixture \
1373 and re-run to regenerate.",
1374 path.display()
1375 );
1376 }
1377 }
1378
1379 #[test]
1382 fn snapshot_show_status() {
1383 let node = build_test_node();
1384 assert_snapshot("show_status", &render(show_status(&node)));
1385 }
1386
1387 #[test]
1388 fn snapshot_show_acl() {
1389 let node = build_test_node();
1390 assert_snapshot("show_acl", &render(show_acl(&node)));
1391 }
1392
1393 #[test]
1394 fn snapshot_show_peers() {
1395 let node = build_test_node();
1396 assert_snapshot("show_peers", &render(show_peers(&node)));
1397 }
1398
1399 #[test]
1400 fn snapshot_show_links() {
1401 let node = build_test_node();
1402 assert_snapshot("show_links", &render(show_links(&node)));
1403 }
1404
1405 #[test]
1406 fn snapshot_show_tree() {
1407 let node = build_test_node();
1408 assert_snapshot("show_tree", &render(show_tree(&node)));
1409 }
1410
1411 #[test]
1412 fn snapshot_show_sessions() {
1413 let node = build_test_node();
1414 assert_snapshot("show_sessions", &render(show_sessions(&node)));
1415 }
1416
1417 #[test]
1418 fn snapshot_show_bloom() {
1419 let node = build_test_node();
1420 assert_snapshot("show_bloom", &render(show_bloom(&node)));
1421 }
1422
1423 #[test]
1424 fn snapshot_show_mmp() {
1425 let node = build_test_node();
1426 assert_snapshot("show_mmp", &render(show_mmp(&node)));
1427 }
1428
1429 #[test]
1430 fn snapshot_show_cache() {
1431 let node = build_test_node();
1432 assert_snapshot("show_cache", &render(show_cache(&node)));
1433 }
1434
1435 #[test]
1436 fn snapshot_show_connections() {
1437 let node = build_test_node();
1438 assert_snapshot("show_connections", &render(show_connections(&node)));
1439 }
1440
1441 #[test]
1442 fn snapshot_show_transports() {
1443 let node = build_test_node();
1444 assert_snapshot("show_transports", &render(show_transports(&node)));
1445 }
1446
1447 #[test]
1448 fn snapshot_show_routing() {
1449 let node = build_test_node();
1450 assert_snapshot("show_routing", &render(show_routing(&node)));
1451 }
1452
1453 #[test]
1454 fn snapshot_show_identity_cache() {
1455 let node = build_test_node();
1456 assert_snapshot("show_identity_cache", &render(show_identity_cache(&node)));
1457 }
1458
1459 #[test]
1460 fn snapshot_show_listening_sockets() {
1461 let node = build_test_node();
1462 assert_snapshot(
1463 "show_listening_sockets",
1464 &render(show_listening_sockets(&node)),
1465 );
1466 }
1467
1468 #[test]
1469 fn snapshot_show_stats_list() {
1470 assert_snapshot("show_stats_list", &render(show_stats_list()));
1472 }
1473
1474 #[test]
1475 fn snapshot_show_stats_history() {
1476 let node = build_test_node();
1477 let params = json!({ "metric": "mesh_size", "window": "10s", "granularity": "1s" });
1479 let resp = show_stats_history(&node, Some(¶ms));
1480 assert_snapshot("show_stats_history", &render_response(resp));
1481 }
1482
1483 #[test]
1484 fn snapshot_show_stats_all_history() {
1485 let node = build_test_node();
1486 let params = json!({ "window": "10s", "granularity": "1s" });
1489 let resp = show_stats_all_history(&node, Some(¶ms));
1490 assert_snapshot("show_stats_all_history", &render_response(resp));
1491 }
1492
1493 #[test]
1494 fn snapshot_show_stats_peers() {
1495 let node = build_test_node();
1496 assert_snapshot("show_stats_peers", &render(show_stats_peers(&node)));
1497 }
1498
1499 #[test]
1500 fn snapshot_show_stats_history_all_peers() {
1501 let node = build_test_node();
1502 let params = json!({ "metric": "srtt_ms", "window": "10s", "granularity": "1s" });
1506 let resp = show_stats_history_all_peers(&node, Some(¶ms));
1507 assert_snapshot("show_stats_history_all_peers", &render_response(resp));
1508 }
1509
1510 #[test]
1514 fn dispatch_covers_all_snapshotted_handlers() {
1515 let expected = [
1516 "show_status",
1517 "show_acl",
1518 "show_peers",
1519 "show_links",
1520 "show_tree",
1521 "show_sessions",
1522 "show_bloom",
1523 "show_mmp",
1524 "show_cache",
1525 "show_connections",
1526 "show_transports",
1527 "show_routing",
1528 "show_identity_cache",
1529 "show_listening_sockets",
1530 "show_stats_list",
1531 "show_stats_history",
1532 "show_stats_all_history",
1533 "show_stats_peers",
1534 "show_stats_history_all_peers",
1535 ];
1536 assert_eq!(expected.len(), 19, "expected exactly 19 query handlers");
1537 let node = build_test_node();
1538 for cmd in expected {
1539 let params = match cmd {
1542 "show_stats_history" => Some(json!({
1543 "metric": "mesh_size", "window": "10s", "granularity": "1s"
1544 })),
1545 "show_stats_all_history" => Some(json!({ "window": "10s", "granularity": "1s" })),
1546 "show_stats_history_all_peers" => Some(json!({
1547 "metric": "srtt_ms", "window": "10s", "granularity": "1s"
1548 })),
1549 _ => None,
1550 };
1551 let resp = dispatch(&node, cmd, params.as_ref());
1552 assert_eq!(
1553 resp.status, "ok",
1554 "dispatch({cmd}) returned status={} message={:?}",
1555 resp.status, resp.message
1556 );
1557 }
1558 }
1559}