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