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