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