Skip to main content

fips_core/control/
queries.rs

1//! Control query implementations.
2//!
3//! Each function takes `&Node` and returns a `serde_json::Value`.
4//! Query logic is kept separate from socket handling.
5
6use crate::identity::encode_npub;
7use crate::node::Node;
8use crate::node::stats_history::Metric;
9use serde_json::{Value, json};
10
11mod stats;
12
13pub use stats::{
14    show_stats_all_history, show_stats_history, show_stats_history_all_peers, show_stats_list,
15    show_stats_peers,
16};
17
18/// Helper: get current Unix time in milliseconds.
19fn now_ms() -> u64 {
20    std::time::SystemTime::now()
21        .duration_since(std::time::UNIX_EPOCH)
22        .map(|d| d.as_millis() as u64)
23        .unwrap_or(0)
24}
25
26/// Classify a DualEwma trend as "rising", "falling", or "stable".
27fn trend_label(short: f64, long: f64) -> &'static str {
28    if !short.is_finite() || !long.is_finite() || long == 0.0 {
29        return "stable";
30    }
31    let ratio = short / long;
32    if ratio > 1.05 {
33        "rising"
34    } else if ratio < 0.95 {
35        "falling"
36    } else {
37        "stable"
38    }
39}
40
41/// `show_status` — Node overview.
42pub fn show_status(node: &Node) -> Value {
43    let pid = std::process::id();
44    let exe_path = std::env::current_exe()
45        .map(|p| p.display().to_string())
46        .unwrap_or_else(|_| "-".into());
47    let uptime_secs = node.uptime().as_secs();
48    let fwd = node.stats().snapshot().forwarding;
49
50    // Inline last-N-second sparklines for dashboard rendering. Kept
51    // short so the status payload stays compact; longer windows use
52    // `show_stats_history`.
53    const SPARK_N: usize = 30;
54    let hist = node.stats_history();
55    let sparklines = json!({
56        "mesh_size": hist.recent(Metric::MeshSize, SPARK_N),
57        "tree_depth": hist.recent(Metric::TreeDepth, SPARK_N),
58        "peer_count": hist.recent(Metric::PeerCount, SPARK_N),
59        "bytes_in": hist.recent(Metric::BytesIn, SPARK_N),
60        "bytes_out": hist.recent(Metric::BytesOut, SPARK_N),
61        "loss_rate": hist.recent(Metric::LossRate, SPARK_N),
62    });
63
64    json!({
65        "version": crate::version::short_version(),
66        "npub": node.npub(),
67        "node_addr": hex::encode(node.node_addr().as_bytes()),
68        "ipv6_addr": format!("{}", node.identity().address()),
69        "state": format!("{}", node.state()),
70        "is_leaf_only": node.is_leaf_only(),
71        "peer_count": node.peer_count(),
72        "session_count": node.session_count(),
73        "link_count": node.link_count(),
74        "transport_count": node.transport_count(),
75        "connection_count": node.connection_count(),
76        "tun_state": format!("{}", node.tun_state()),
77        "tun_name": node.tun_name().unwrap_or("-"),
78        "effective_ipv6_mtu": node.effective_ipv6_mtu(),
79        "control_socket": &node.config().node.control.socket_path,
80        "pid": pid,
81        "exe_path": exe_path,
82        "uptime_secs": uptime_secs,
83        "estimated_mesh_size": node.estimated_mesh_size(),
84        "forwarding": serde_json::to_value(&fwd).unwrap_or_default(),
85        "sparklines": sparklines,
86    })
87}
88
89/// `show_acl` — Loaded peer ACL state.
90pub fn show_acl(node: &Node) -> Value {
91    let status = node.peer_acl_status();
92
93    json!({
94        "allow_file": status.allow_file,
95        "deny_file": status.deny_file,
96        "enforcement_active": status.enforcement_active,
97        "effective_mode": status.effective_mode,
98        "default_decision": status.default_decision,
99        "allow_all": status.allow_all,
100        "deny_all": status.deny_all,
101        "allow_file_entries": status.allow_file_entries,
102        "deny_file_entries": status.deny_file_entries,
103        "allow_entries": status.allow_entries,
104        "deny_entries": status.deny_entries,
105    })
106}
107
108/// `show_peers` — Authenticated peers.
109pub fn show_peers(node: &Node) -> Value {
110    let tree = node.tree_state();
111    let my_addr = *tree.my_node_addr();
112    let parent_id = *tree.my_declaration().parent_id();
113    let is_root = tree.is_root();
114
115    // Per-npub Nostr-traversal failure-state snapshot, indexed by npub
116    // for O(1) per-peer lookup. Empty if Nostr discovery is disabled.
117    let nostr_state: std::collections::HashMap<String, _> = node
118        .nostr_discovery_handle()
119        .map(|d| {
120            d.failure_state_snapshot()
121                .into_iter()
122                .map(|view| (view.npub.clone(), view))
123                .collect()
124        })
125        .unwrap_or_default();
126    let retry_state: std::collections::HashMap<_, _> = node
127        .retry_state_iter()
128        .map(|(addr, state)| (*addr, state.retry_after_ms))
129        .collect();
130
131    let peers: Vec<Value> = node
132        .peers()
133        .map(|peer| {
134            let node_addr = *peer.node_addr();
135            let addr_hex = hex::encode(node_addr.as_bytes());
136
137            // Determine tree relationship
138            let is_parent = !is_root && node_addr == parent_id;
139            let is_child = tree
140                .peer_declaration(&node_addr)
141                .is_some_and(|decl| *decl.parent_id() == my_addr);
142
143            let mut peer_json = json!({
144                "node_addr": addr_hex,
145                "npub": peer.npub(),
146                "display_name": node.peer_display_name(&node_addr),
147                "ipv6_addr": format!("{}", peer.address()),
148                "connectivity": format!("{}", peer.connectivity()),
149                "link_id": peer.link_id().as_u64(),
150                "authenticated_at_ms": peer.authenticated_at(),
151                "last_seen_ms": peer.last_seen(),
152                "has_tree_position": peer.has_tree_position(),
153                "has_bloom_filter": peer.filter_sequence() > 0,
154                "filter_sequence": peer.filter_sequence(),
155                "is_parent": is_parent,
156                "is_child": is_child,
157            });
158
159            // Add transport address if available
160            if let Some(addr) = peer.current_addr() {
161                peer_json["transport_addr"] = json!(format!("{}", addr));
162            }
163
164            // Add link info (direction, transport type)
165            let link_id = peer.link_id();
166            if let Some(link) = node.get_link(&link_id) {
167                peer_json["direction"] = json!(format!("{}", link.direction()));
168                let transport_id = link.transport_id();
169                if let Some(handle) = node.get_transport(&transport_id) {
170                    peer_json["transport_type"] = json!(handle.transport_type().name);
171                }
172            }
173
174            // Add tree depth if available
175            if let Some(coords) = peer.coords() {
176                peer_json["tree_depth"] = json!(coords.depth());
177            }
178
179            // Add link stats
180            let stats = peer.link_stats();
181            peer_json["stats"] = json!({
182                "packets_sent": stats.packets_sent,
183                "packets_recv": stats.packets_recv,
184                "bytes_sent": stats.bytes_sent,
185                "bytes_recv": stats.bytes_recv,
186            });
187
188            // Security signals
189            peer_json["replay_suppressed"] = json!(peer.replay_suppressed_count());
190            peer_json["consecutive_decrypt_failures"] = json!(peer.consecutive_decrypt_failures());
191
192            // Nostr-traversal state if this peer's npub appears in
193            // failure-state. Always emitted (even null) so the schema
194            // stays stable; values populated only when Nostr discovery
195            // is enabled and the npub has been seen.
196            let npub = peer.npub();
197            let mut nostr_obj = json!({
198                "consecutive_failures": 0,
199                "in_cooldown": false,
200                "cooldown_until_ms": Value::Null,
201                "last_observed_skew_ms": Value::Null,
202                "direct_probe_pending": retry_state.contains_key(&node_addr),
203                "direct_probe_after_ms": retry_state
204                    .get(&node_addr)
205                    .map(|t| json!(t))
206                    .unwrap_or(Value::Null),
207            });
208            if let Some(state) = nostr_state.get(&npub) {
209                nostr_obj["consecutive_failures"] = json!(state.consecutive_failures);
210                nostr_obj["in_cooldown"] = json!(state.cooldown_until_ms.is_some());
211                nostr_obj["cooldown_until_ms"] = state
212                    .cooldown_until_ms
213                    .map(|t| json!(t))
214                    .unwrap_or(Value::Null);
215                nostr_obj["last_observed_skew_ms"] = state
216                    .last_observed_skew_ms
217                    .map(|s| json!(s))
218                    .unwrap_or(Value::Null);
219            }
220            peer_json["nostr_traversal"] = nostr_obj;
221
222            // Noise session counters (rekey urgency, replay window state)
223            if let Some(session) = peer.noise_session() {
224                peer_json["noise"] = json!({
225                    "send_counter": session.current_send_counter(),
226                    "highest_recv_counter": session.highest_received_counter(),
227                });
228            }
229
230            // Session indices (hijack detection)
231            if let Some(idx) = peer.our_index() {
232                peer_json["our_session_index"] = json!(format!("{:08x}", idx.as_u32()));
233            }
234
235            // Rekey state
236            if peer.rekey_in_progress() {
237                peer_json["rekey_in_progress"] = json!(true);
238            }
239            if peer.is_draining() {
240                peer_json["rekey_draining"] = json!(true);
241            }
242            peer_json["current_k_bit"] = json!(peer.current_k_bit());
243
244            // Add MMP metrics if available
245            if let Some(mmp) = peer.mmp() {
246                let mut mmp_json = json!({
247                    "mode": format!("{}", mmp.mode()),
248                });
249                if let Some(srtt) = mmp.metrics.srtt_ms() {
250                    mmp_json["srtt_ms"] = json!(srtt);
251                }
252                mmp_json["loss_rate"] = json!(mmp.metrics.loss_rate());
253                mmp_json["etx"] = json!(mmp.metrics.etx);
254                mmp_json["goodput_bps"] = json!(mmp.metrics.goodput_bps);
255                mmp_json["delivery_ratio_forward"] = json!(mmp.metrics.delivery_ratio_forward);
256                mmp_json["delivery_ratio_reverse"] = json!(mmp.metrics.delivery_ratio_reverse);
257                if let Some(smoothed_loss) = mmp.metrics.smoothed_loss() {
258                    mmp_json["smoothed_loss"] = json!(smoothed_loss);
259                }
260                if let Some(smoothed_etx) = mmp.metrics.smoothed_etx() {
261                    mmp_json["smoothed_etx"] = json!(smoothed_etx);
262                }
263                if let Some(srtt) = mmp.metrics.srtt_ms()
264                    && let Some(setx) = mmp.metrics.smoothed_etx()
265                {
266                    mmp_json["lqi"] = json!(setx * (1.0 + srtt / 100.0));
267                }
268                peer_json["mmp"] = mmp_json;
269            }
270
271            peer_json
272        })
273        .collect();
274
275    json!({ "peers": peers })
276}
277
278/// `show_links` — Active links.
279pub fn show_links(node: &Node) -> Value {
280    let links: Vec<Value> = node
281        .links()
282        .map(|link| {
283            let stats = link.stats();
284            json!({
285                "link_id": link.link_id().as_u64(),
286                "transport_id": link.transport_id().as_u32(),
287                "remote_addr": format!("{}", link.remote_addr()),
288                "direction": format!("{}", link.direction()),
289                "state": format!("{}", link.state()),
290                "created_at_ms": link.created_at(),
291                "stats": {
292                    "packets_sent": stats.packets_sent,
293                    "packets_recv": stats.packets_recv,
294                    "bytes_sent": stats.bytes_sent,
295                    "bytes_recv": stats.bytes_recv,
296                    "last_recv_ms": stats.last_recv_ms,
297                },
298            })
299        })
300        .collect();
301
302    json!({ "links": links })
303}
304
305/// `show_tree` — Spanning tree state.
306pub fn show_tree(node: &Node) -> Value {
307    let tree = node.tree_state();
308    let my_coords = tree.my_coords();
309    let decl = tree.my_declaration();
310
311    // Build coords array as hex strings
312    let coords: Vec<String> = my_coords
313        .entries()
314        .iter()
315        .map(|e| hex::encode(e.node_addr.as_bytes()))
316        .collect();
317
318    // Build peer tree data
319    let peers: Vec<Value> = tree
320        .peer_ids()
321        .map(|peer_id| {
322            let mut peer_json = json!({
323                "node_addr": hex::encode(peer_id.as_bytes()),
324                "display_name": node.peer_display_name(peer_id),
325            });
326            if let Some(coords) = tree.peer_coords(peer_id) {
327                let coord_path: Vec<String> = coords
328                    .entries()
329                    .iter()
330                    .map(|e| hex::encode(e.node_addr.as_bytes()))
331                    .collect();
332                peer_json["depth"] = json!(coords.depth());
333                peer_json["root"] = json!(hex::encode(coords.root_id().as_bytes()));
334                peer_json["coords"] = json!(coord_path);
335                peer_json["distance_to_us"] = json!(my_coords.distance_to(coords));
336            }
337            peer_json
338        })
339        .collect();
340
341    // Determine parent display name
342    let parent_addr = my_coords.parent_id();
343    let parent_hex = hex::encode(parent_addr.as_bytes());
344    let parent_display = node.peer_display_name(parent_addr);
345
346    let tree_stats = node.stats().snapshot().tree;
347
348    json!({
349        "my_node_addr": hex::encode(tree.my_node_addr().as_bytes()),
350        "root": hex::encode(tree.root().as_bytes()),
351        "is_root": tree.is_root(),
352        "depth": my_coords.depth(),
353        "my_coords": coords,
354        "parent": parent_hex,
355        "parent_display_name": parent_display,
356        "declaration_sequence": decl.sequence(),
357        "declaration_signed": decl.is_signed(),
358        "peer_tree_count": tree.peer_count(),
359        "peers": peers,
360        "stats": serde_json::to_value(&tree_stats).unwrap_or_default(),
361    })
362}
363
364/// `show_sessions` — End-to-end sessions.
365pub fn show_sessions(node: &Node) -> Value {
366    let sessions: Vec<Value> = node
367        .session_entries()
368        .map(|(addr, entry)| {
369            let state_str = if entry.is_established() {
370                "established"
371            } else if entry.is_initiating() {
372                "initiating"
373            } else if entry.is_awaiting_msg3() {
374                "awaiting_msg3"
375            } else {
376                "unknown"
377            };
378
379            let mut session_json = json!({
380                "remote_addr": hex::encode(addr.as_bytes()),
381                "display_name": node.peer_display_name(addr),
382                "state": state_str,
383                "is_initiator": entry.is_initiator(),
384                "last_activity_ms": entry.last_activity(),
385            });
386
387            // Derive npub from session's remote public key
388            let (xonly, _parity) = entry.remote_pubkey().x_only_public_key();
389            session_json["npub"] = json!(encode_npub(&xonly));
390
391            // Traffic counters
392            let (pkts_tx, pkts_rx, bytes_tx, bytes_rx) = entry.traffic_counters();
393            session_json["stats"] = json!({
394                "packets_sent": pkts_tx,
395                "packets_recv": pkts_rx,
396                "bytes_sent": bytes_tx,
397                "bytes_recv": bytes_rx,
398            });
399
400            // Handshake health (visible during initiating/awaiting_msg3)
401            if !entry.is_established() {
402                session_json["resend_count"] = json!(entry.resend_count());
403            }
404
405            // Rekey and session health (visible when established)
406            if entry.is_established() {
407                session_json["session_start_ms"] = json!(entry.session_start_ms());
408                session_json["current_k_bit"] = json!(entry.current_k_bit());
409                session_json["coords_warmup_remaining"] = json!(entry.coords_warmup_remaining());
410                session_json["is_draining"] = json!(entry.is_draining());
411            }
412
413            // Add session MMP if available
414            if let Some(mmp) = entry.mmp() {
415                let mut mmp_json = json!({
416                    "mode": format!("{}", mmp.mode()),
417                    "loss_rate": mmp.metrics.loss_rate(),
418                    "etx": mmp.metrics.etx,
419                    "goodput_bps": mmp.metrics.goodput_bps,
420                    "delivery_ratio_forward": mmp.metrics.delivery_ratio_forward,
421                    "delivery_ratio_reverse": mmp.metrics.delivery_ratio_reverse,
422                    "path_mtu": mmp.path_mtu.current_mtu(),
423                });
424                if let Some(srtt) = mmp.metrics.srtt_ms() {
425                    mmp_json["srtt_ms"] = json!(srtt);
426                }
427                if let Some(smoothed_loss) = mmp.metrics.smoothed_loss() {
428                    mmp_json["smoothed_loss"] = json!(smoothed_loss);
429                }
430                if let Some(smoothed_etx) = mmp.metrics.smoothed_etx() {
431                    mmp_json["smoothed_etx"] = json!(smoothed_etx);
432                }
433                if let Some(srtt) = mmp.metrics.srtt_ms()
434                    && let Some(setx) = mmp.metrics.smoothed_etx()
435                {
436                    mmp_json["sqi"] = json!(setx * (1.0 + srtt / 100.0));
437                }
438                session_json["mmp"] = mmp_json;
439            }
440
441            session_json
442        })
443        .collect();
444
445    json!({ "sessions": sessions })
446}
447
448/// `show_bloom` — Bloom filter state.
449pub fn show_bloom(node: &Node) -> Value {
450    let bloom = node.bloom_state();
451
452    let leaf_deps: Vec<String> = bloom
453        .leaf_dependents()
454        .iter()
455        .map(|addr| hex::encode(addr.as_bytes()))
456        .collect();
457
458    // Build per-peer filter info
459    let peer_filters: Vec<Value> = node
460        .peers()
461        .map(|peer| {
462            let addr = *peer.node_addr();
463            let mut pf = json!({
464                "peer": hex::encode(addr.as_bytes()),
465                "display_name": node.peer_display_name(&addr),
466                "has_filter": peer.filter_sequence() > 0,
467                "filter_sequence": peer.filter_sequence(),
468            });
469            if let Some(filter) = peer.inbound_filter() {
470                let max_fpr = node.config().node.bloom.max_inbound_fpr;
471                pf["estimated_count"] = json!(filter.estimated_count(max_fpr));
472                pf["set_bits"] = json!(filter.count_ones());
473                pf["fill_ratio"] = json!(filter.fill_ratio());
474            }
475            pf
476        })
477        .collect();
478
479    let bloom_stats = node.stats().snapshot().bloom;
480
481    json!({
482        "own_node_addr": hex::encode(node.node_addr().as_bytes()),
483        "is_leaf_only": node.is_leaf_only(),
484        "sequence": bloom.sequence(),
485        "leaf_dependent_count": bloom.leaf_dependents().len(),
486        "leaf_dependents": leaf_deps,
487        "peer_filters": peer_filters,
488        "stats": serde_json::to_value(&bloom_stats).unwrap_or_default(),
489    })
490}
491
492/// `show_mmp` — MMP metrics summary.
493pub fn show_mmp(node: &Node) -> Value {
494    // Link-layer MMP per peer
495    let peers: Vec<Value> = node.peers().filter_map(|peer| {
496        let mmp = peer.mmp()?;
497        let addr = *peer.node_addr();
498        let metrics = &mmp.metrics;
499
500        let mut link_layer = json!({
501            "loss_rate": metrics.loss_rate(),
502            "etx": metrics.etx,
503            "goodput_bps": metrics.goodput_bps,
504            "spin_bit_role": if mmp.spin_bit.is_initiator() { "initiator" } else { "responder" },
505        });
506
507        if let Some(smoothed_loss) = metrics.smoothed_loss() {
508            link_layer["smoothed_loss"] = json!(smoothed_loss);
509        }
510        if let Some(smoothed_etx) = metrics.smoothed_etx() {
511            link_layer["smoothed_etx"] = json!(smoothed_etx);
512        }
513        if let Some(srtt) = metrics.srtt_ms() {
514            link_layer["srtt_ms"] = json!(srtt);
515            if let Some(setx) = metrics.smoothed_etx() {
516                link_layer["lqi"] = json!(setx * (1.0 + srtt / 100.0));
517            }
518        }
519
520        // Trend indicators
521        if metrics.rtt_trend.initialized() {
522            link_layer["rtt_trend"] = json!(trend_label(metrics.rtt_trend.short(), metrics.rtt_trend.long()));
523        }
524        if metrics.loss_trend.initialized() {
525            link_layer["loss_trend"] = json!(trend_label(metrics.loss_trend.short(), metrics.loss_trend.long()));
526        }
527        if metrics.goodput_trend.initialized() {
528            link_layer["goodput_trend"] = json!(trend_label(metrics.goodput_trend.short(), metrics.goodput_trend.long()));
529        }
530        if metrics.jitter_trend.initialized() {
531            link_layer["jitter_trend"] = json!(trend_label(metrics.jitter_trend.short(), metrics.jitter_trend.long()));
532        }
533
534        link_layer["delivery_ratio_forward"] = json!(metrics.delivery_ratio_forward);
535        link_layer["delivery_ratio_reverse"] = json!(metrics.delivery_ratio_reverse);
536        link_layer["ecn_ce_count"] = json!(metrics.last_ecn_ce_count());
537
538        Some(json!({
539            "peer": hex::encode(addr.as_bytes()),
540            "display_name": node.peer_display_name(&addr),
541            "mode": format!("{}", mmp.mode()),
542            "link_layer": link_layer,
543        }))
544    }).collect();
545
546    // Session-layer MMP
547    let sessions: Vec<Value> = node
548        .session_entries()
549        .filter_map(|(addr, entry)| {
550            let mmp = entry.mmp()?;
551            let metrics = &mmp.metrics;
552
553            let mut session_layer = json!({
554                "loss_rate": metrics.loss_rate(),
555                "etx": metrics.etx,
556                "path_mtu": mmp.path_mtu.current_mtu(),
557            });
558
559            if let Some(smoothed_loss) = metrics.smoothed_loss() {
560                session_layer["smoothed_loss"] = json!(smoothed_loss);
561            }
562            if let Some(smoothed_etx) = metrics.smoothed_etx() {
563                session_layer["smoothed_etx"] = json!(smoothed_etx);
564            }
565            if let Some(srtt) = metrics.srtt_ms() {
566                session_layer["srtt_ms"] = json!(srtt);
567                if let Some(setx) = metrics.smoothed_etx() {
568                    session_layer["sqi"] = json!(setx * (1.0 + srtt / 100.0));
569                }
570            }
571
572            Some(json!({
573                "remote": hex::encode(addr.as_bytes()),
574                "display_name": node.peer_display_name(addr),
575                "mode": format!("{}", mmp.mode()),
576                "session_layer": session_layer,
577            }))
578        })
579        .collect();
580
581    json!({
582        "peers": peers,
583        "sessions": sessions,
584    })
585}
586
587/// `show_cache` — Coordinate cache stats and entries.
588pub fn show_cache(node: &Node) -> Value {
589    let cache = node.coord_cache();
590    let now = now_ms();
591    let stats = cache.stats(now);
592
593    // Include individual entries for route debugging
594    let entries: Vec<Value> = cache
595        .iter(now)
596        .map(|(addr, entry)| {
597            let fips_addr = crate::identity::FipsAddress::from_node_addr(addr);
598            let coord_path: Vec<String> = entry
599                .coords()
600                .entries()
601                .iter()
602                .map(|e| hex::encode(e.node_addr.as_bytes()))
603                .collect();
604            let mut entry_json = json!({
605                "node_addr": hex::encode(addr.as_bytes()),
606                "display_name": node.peer_display_name(addr),
607                "ipv6_addr": format!("{}", fips_addr),
608                "depth": entry.coords().depth(),
609                "coords": coord_path,
610                "age_ms": now.saturating_sub(entry.created_at()),
611                "last_used_ms": entry.last_used(),
612            });
613            if let Some(mtu) = entry.path_mtu() {
614                entry_json["path_mtu"] = json!(mtu);
615            }
616            entry_json
617        })
618        .collect();
619
620    json!({
621        "count": stats.entries,
622        "max_entries": stats.max_entries,
623        "fill_ratio": stats.fill_ratio(),
624        "default_ttl_ms": cache.default_ttl_ms(),
625        "expired": stats.expired,
626        "avg_age_ms": stats.avg_age_ms,
627        "entries": entries,
628    })
629}
630
631/// `show_connections` — Pending handshakes.
632pub fn show_connections(node: &Node) -> Value {
633    let now = now_ms();
634    let connections: Vec<Value> = node
635        .connections()
636        .map(|conn| {
637            let mut conn_json = json!({
638                "link_id": conn.link_id().as_u64(),
639                "direction": format!("{}", conn.direction()),
640                "handshake_state": format!("{}", conn.handshake_state()),
641                "started_at_ms": conn.started_at(),
642                "idle_ms": now.saturating_sub(conn.last_activity()),
643                "resend_count": conn.resend_count(),
644            });
645
646            if let Some(identity) = conn.expected_identity() {
647                conn_json["expected_peer"] = json!(identity.npub());
648            }
649
650            conn_json
651        })
652        .collect();
653
654    json!({ "connections": connections })
655}
656
657/// `show_transports` — Transport instances.
658pub fn show_transports(node: &Node) -> Value {
659    let transports: Vec<Value> = node
660        .transport_ids()
661        .map(|id| {
662            let handle = node.get_transport(id).unwrap();
663            let mut t_json = json!({
664                "transport_id": id.as_u32(),
665                "type": handle.transport_type().name,
666                "state": format!("{}", handle.state()),
667                "mtu": handle.mtu(),
668            });
669
670            if let Some(name) = handle.name() {
671                t_json["name"] = json!(name);
672            }
673            if let Some(addr) = handle.local_addr() {
674                t_json["local_addr"] = json!(format!("{}", addr));
675            }
676
677            // Tor-specific fields
678            if let Some(mode) = handle.tor_mode() {
679                t_json["tor_mode"] = json!(mode);
680            }
681            if let Some(onion) = handle.onion_address() {
682                t_json["onion_address"] = json!(onion);
683            }
684            if let Some(monitoring) = handle.tor_monitoring() {
685                t_json["tor_monitoring"] = serde_json::to_value(&monitoring).unwrap_or_default();
686            }
687
688            t_json["stats"] = handle.transport_stats();
689
690            t_json
691        })
692        .collect();
693
694    json!({ "transports": transports })
695}
696
697/// `show_routing` — Routing table summary and node statistics.
698pub fn show_routing(node: &Node) -> Value {
699    let cache = node.coord_cache();
700    let now = now_ms();
701    let cache_stats = cache.stats(now);
702    let node_stats = node.stats().snapshot();
703    let learned_routes = node.learned_route_table_snapshot(now);
704
705    // Pending discovery lookups (individual targets)
706    let lookups: Vec<Value> = node
707        .pending_lookups_iter()
708        .map(|(addr, lookup)| {
709            json!({
710                "target": hex::encode(addr.as_bytes()),
711                "display_name": node.peer_display_name(addr),
712                "initiated_ms": lookup.initiated_ms,
713                "last_sent_ms": lookup.last_sent_ms,
714                "attempt": lookup.attempt,
715                "age_ms": now.saturating_sub(lookup.initiated_ms),
716            })
717        })
718        .collect();
719
720    // Connection retry state
721    let retries: Vec<Value> = node
722        .retry_state_iter()
723        .map(|(addr, state)| {
724            json!({
725                "node_addr": hex::encode(addr.as_bytes()),
726                "display_name": node.peer_display_name(addr),
727                "retry_count": state.retry_count,
728                "retry_after_ms": state.retry_after_ms,
729                "auto_reconnect": state.reconnect,
730            })
731        })
732        .collect();
733
734    json!({
735        "coord_cache_entries": cache_stats.entries,
736        "routing_mode": node.config().node.routing.mode.to_string(),
737        "learned_routes": serde_json::to_value(&learned_routes).unwrap_or_default(),
738        "identity_cache_entries": node.identity_cache_len(),
739        "pending_lookups": lookups,
740        "pending_tun_destinations": node.pending_tun_destinations(),
741        "pending_tun_packets": node.pending_tun_total_packets(),
742        "recent_requests": node.recent_request_count(),
743        "retries": retries,
744        "forwarding": serde_json::to_value(&node_stats.forwarding).unwrap_or_default(),
745        "discovery": serde_json::to_value(&node_stats.discovery).unwrap_or_default(),
746        "error_signals": serde_json::to_value(&node_stats.errors).unwrap_or_default(),
747        "congestion": serde_json::to_value(&node_stats.congestion).unwrap_or_default(),
748    })
749}
750
751/// `show_identity_cache` — Known node identities.
752///
753/// Lists every node whose public key has been cached by this daemon.
754/// Identities are learned from DNS resolution, peer handshakes, session
755/// establishment, and configured peer npubs.  The cache uses LRU eviction
756/// bounded by `node.cache.identity_size`.
757pub fn show_identity_cache(node: &Node) -> Value {
758    let now = now_ms();
759    let entries: Vec<Value> = node
760        .identity_cache_iter()
761        .map(|(node_addr, pubkey, last_seen_ms)| {
762            let (xonly, _parity) = pubkey.x_only_public_key();
763            let fips_addr = crate::identity::FipsAddress::from_node_addr(node_addr);
764            json!({
765                "node_addr": hex::encode(node_addr.as_bytes()),
766                "npub": encode_npub(&xonly),
767                "display_name": node.peer_display_name(node_addr),
768                "ipv6_addr": format!("{}", fips_addr),
769                "last_seen_ms": last_seen_ms,
770                "age_ms": now.saturating_sub(last_seen_ms),
771            })
772        })
773        .collect();
774    let count = entries.len();
775
776    json!({
777        "entries": entries,
778        "count": count,
779        "max_entries": node.identity_cache_max(),
780    })
781}
782
783/// `show_listening_sockets` - IPv6 listeners reachable from fips0, annotated
784/// with the current `inet fips` filter classification.
785pub fn show_listening_sockets(node: &Node) -> Value {
786    let fips0 = crate::FipsAddress::from_node_addr(node.identity().node_addr()).to_ipv6();
787    #[cfg(test)]
788    let sockets: Vec<super::listening::ListeningSocket> = Vec::new();
789    #[cfg(not(test))]
790    let sockets = super::listening::enumerate(fips0);
791    #[cfg(test)]
792    let classifier = super::firewall_state::FilterClassifier::no_firewall();
793    #[cfg(not(test))]
794    let classifier = super::firewall_state::FilterClassifier::query();
795
796    let rows: Vec<Value> = sockets
797        .iter()
798        .map(|socket| {
799            let filter = classifier.classify(socket.proto, socket.port);
800            json!({
801                "proto": socket.proto.as_str(),
802                "local_addr": socket.local_addr.to_string(),
803                "port": socket.port,
804                "pid": socket.pid,
805                "process": socket.process,
806                "filter": filter.as_str(),
807                "wildcard_bind": socket.wildcard_bind,
808            })
809        })
810        .collect();
811
812    json!({
813        "fips0_addr": fips0.to_string(),
814        "firewall_active": classifier.is_active(),
815        "sockets": rows,
816    })
817}
818
819/// Dispatch a command string to the appropriate query function.
820pub fn dispatch(node: &Node, command: &str, params: Option<&Value>) -> super::protocol::Response {
821    match command {
822        "show_acl" => super::protocol::Response::ok(show_acl(node)),
823        "show_status" => super::protocol::Response::ok(show_status(node)),
824        "show_peers" => super::protocol::Response::ok(show_peers(node)),
825        "show_links" => super::protocol::Response::ok(show_links(node)),
826        "show_tree" => super::protocol::Response::ok(show_tree(node)),
827        "show_sessions" => super::protocol::Response::ok(show_sessions(node)),
828        "show_bloom" => super::protocol::Response::ok(show_bloom(node)),
829        "show_mmp" => super::protocol::Response::ok(show_mmp(node)),
830        "show_cache" => super::protocol::Response::ok(show_cache(node)),
831        "show_connections" => super::protocol::Response::ok(show_connections(node)),
832        "show_transports" => super::protocol::Response::ok(show_transports(node)),
833        "show_routing" => super::protocol::Response::ok(show_routing(node)),
834        "show_identity_cache" => super::protocol::Response::ok(show_identity_cache(node)),
835        "show_listening_sockets" => super::protocol::Response::ok(show_listening_sockets(node)),
836        "show_stats_list" => super::protocol::Response::ok(show_stats_list()),
837        "show_stats_history" => show_stats_history(node, params),
838        "show_stats_all_history" => show_stats_all_history(node, params),
839        "show_stats_peers" => super::protocol::Response::ok(show_stats_peers(node)),
840        "show_stats_history_all_peers" => show_stats_history_all_peers(node, params),
841        _ => super::protocol::Response::error(format!("unknown command: {}", command)),
842    }
843}
844
845#[cfg(test)]
846mod tests;