Skip to main content

ant_quic/
node_status.rs

1// Copyright 2024 Saorsa Labs Ltd.
2//
3// This Saorsa Network Software is licensed under the General Public License (GPL), version 3.
4// Please see the file LICENSE-GPL, or visit <http://www.gnu.org/licenses/> for the full text.
5//
6// Full details available at https://saorsalabs.com/licenses
7
8//! Consolidated node status for observability
9//!
10//! This module provides [`NodeStatus`] - a consolidated best-effort snapshot
11//! of a node's current state, including a best-effort NAT behavior hint, connectivity,
12//! relay/coordinator hints, and performance metrics.
13//!
14//! # Example
15//!
16//! ```rust,ignore
17//! use ant_quic::Node;
18//!
19//! let node = Node::new().await?;
20//! let status = node.status();
21//!
22//! println!("NAT behavior hint: {:?}", status.nat_type);
23//! println!("Can receive direct: {}", status.can_receive_direct);
24//! println!("Relay service enabled: {}", status.relay_service_enabled);
25//! println!("Acting as relay: {}", status.is_relaying);
26//! println!("Relay sessions: {}", status.relay_sessions);
27//! ```
28
29use std::net::SocketAddr;
30use std::time::Duration;
31
32use crate::nat_traversal_api::PeerId;
33pub use crate::reachability::ReachabilityScope;
34
35/// Best-effort NAT behavior hint for the node.
36///
37/// In the current implementation this is derived from native QUIC
38/// reachability and address-mapping observations, not from classic
39/// STUN-based NAT behavior discovery. Treat it as a debug/telemetry hint,
40/// not authoritative NAT classification.
41///
42/// These labels are compatibility-oriented and should not be read as a full
43/// RFC 3489/RFC 5780 NAT classification unless explicitly backed by
44/// protocol-level behavior measurements.
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
46pub enum NatType {
47    /// Compatibility-oriented label for paths that appeared not to require NAT traversal.
48    ///
49    /// This indicates the observed path did not require NAT traversal. It does
50    /// not, by itself, prove current direct reachability to other peers.
51    None,
52
53    /// Compatibility-oriented label for direct-only native connectivity observations.
54    ///
55    /// The current implementation does not prove RFC-style full-cone mapping
56    /// or filtering behaviour before surfacing this value.
57    FullCone,
58
59    /// Compatibility-oriented label reserved for address-restricted behaviour.
60    ///
61    /// The current node-level heuristic does not distinguish this variant from
62    /// other cone-like behaviours.
63    AddressRestricted,
64
65    /// Compatibility-oriented label for mixed direct and relay-assisted outcomes.
66    ///
67    /// The current implementation does not prove RFC-style port-restricted
68    /// filtering behaviour before surfacing this value.
69    PortRestricted,
70
71    /// Compatibility-oriented label for likely endpoint-dependent mapping behaviour.
72    ///
73    /// This is derived from native QUIC observations and is used as a relay
74    /// optimization hint, not a complete NAT classification.
75    Symmetric,
76
77    /// NAT behavior hint not yet determined
78    ///
79    /// The node has not yet gathered enough native connectivity evidence.
80    #[default]
81    Unknown,
82}
83
84impl std::fmt::Display for NatType {
85    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86        match self {
87            Self::None => write!(f, "None (No NAT detected)"),
88            Self::FullCone => write!(f, "Full Cone"),
89            Self::AddressRestricted => write!(f, "Address Restricted"),
90            Self::PortRestricted => write!(f, "Port Restricted"),
91            Self::Symmetric => write!(f, "Symmetric"),
92            Self::Unknown => write!(f, "Unknown"),
93        }
94    }
95}
96
97/// Comprehensive node status snapshot
98///
99/// This struct provides a consolidated snapshot of the node's current state,
100/// including identity, connectivity, NAT status, relay/coordinator hints, and performance.
101///
102/// # Status Categories
103///
104/// - **Identity**: peer_id, local_addr, external_addrs
105/// - **NAT Status**: nat_type, can_receive_direct, direct_reachability_scope, has_global_address
106/// - **Connections**: connected_peers, active_connections, pending_connections
107/// - **NAT Traversal**: direct_connections, relayed_connections, hole_punch_success_rate
108/// - **Assist Services**: relay_service_enabled, coordinator_service_enabled, bootstrap_service_enabled
109/// - **Relay Activity**: is_relaying, relay_sessions, relay_bytes_forwarded
110/// - **Coordinator Activity**: is_coordinating, coordination_sessions
111/// - **Performance**: avg_rtt, uptime
112#[derive(Debug, Clone)]
113pub struct NodeStatus {
114    // --- Identity ---
115    /// This node's peer ID (derived from public key)
116    pub peer_id: PeerId,
117
118    /// Local bind address
119    pub local_addr: SocketAddr,
120
121    /// All discovered external addresses
122    ///
123    /// These are addresses as seen by other peers. Multiple addresses
124    /// may be discovered when behind NAT or with multiple interfaces.
125    pub external_addrs: Vec<SocketAddr>,
126
127    // --- NAT Status ---
128    /// Best-effort NAT behavior hint.
129    ///
130    /// This is observational telemetry derived from native QUIC reachability
131    /// outcomes and address observations, not authoritative NAT classification.
132    pub nat_type: NatType,
133
134    /// Whether this node can receive direct connections
135    ///
136    /// `true` only after this node has peer-verified evidence that another
137    /// node reached it directly without coordinator or relay assistance.
138    pub can_receive_direct: bool,
139
140    /// Broadest scope in which direct inbound reachability has been verified.
141    pub direct_reachability_scope: Option<ReachabilityScope>,
142
143    /// Whether this node has a globally routable address candidate.
144    ///
145    /// This is an address property, not proof of reachability.
146    pub has_global_address: bool,
147
148    /// Whether best-effort router port mapping is currently active.
149    pub port_mapping_active: bool,
150
151    /// The currently mapped public address, if router port mapping is active.
152    pub port_mapping_addr: Option<SocketAddr>,
153
154    /// Whether first-party mDNS browsing is currently active.
155    pub mdns_browsing: bool,
156
157    /// Whether first-party mDNS advertisement is currently active.
158    pub mdns_advertising: bool,
159
160    /// Number of currently eligible peers surfaced by first-party mDNS.
161    pub mdns_discovered_peers: usize,
162
163    // --- Assist Services ---
164    /// Whether this node offers relay service as a capability hint to peers.
165    ///
166    /// This is a local policy/configuration signal. Remote peers still decide
167    /// whether this node is actually useful for relay service.
168    pub relay_service_enabled: bool,
169
170    /// Whether this node offers coordinator capability as a hint to peers.
171    ///
172    /// This is a capability advertisement, not proof of current reachability
173    /// or performance.
174    pub coordinator_service_enabled: bool,
175
176    /// Whether this node offers bootstrap/known-peer assist capability.
177    ///
178    /// Peers may treat this node as one discovery/bootstrap input among many.
179    pub bootstrap_service_enabled: bool,
180
181    // --- Connections ---
182    /// Number of connected peers
183    pub connected_peers: usize,
184
185    /// Number of active connections (may differ from peers if multiplexed)
186    pub active_connections: usize,
187
188    /// Number of pending connection attempts
189    pub pending_connections: usize,
190
191    // --- NAT Traversal Stats ---
192    /// Total successful direct connections (no relay)
193    pub direct_connections: u64,
194
195    /// Total connections that required relay
196    pub relayed_connections: u64,
197
198    /// Hole punch success rate (0.0 - 1.0)
199    ///
200    /// Calculated from NAT traversal attempts vs successes.
201    pub hole_punch_success_rate: f64,
202
203    // --- Relay Status (NEW - key visibility) ---
204    /// Whether this node is currently acting as a relay for others.
205    ///
206    /// This tracks observed runtime activity, not merely whether the node is
207    /// willing to offer relay service.
208    pub is_relaying: bool,
209
210    /// Number of active relay sessions.
211    ///
212    /// Currently reported conservatively; this is not yet a complete runtime metric.
213    pub relay_sessions: usize,
214
215    /// Total bytes forwarded as relay
216    pub relay_bytes_forwarded: u64,
217
218    // --- Coordinator Status (NEW - key visibility) ---
219    /// Whether this node is coordinating NAT traversal.
220    ///
221    /// This is a best-effort signal derived from observed coordination
222    /// activity in the current process lifetime, not merely whether the node
223    /// advertises coordinator capability.
224    pub is_coordinating: bool,
225
226    /// Number of active coordination sessions.
227    ///
228    /// This is currently a best-effort cumulative proxy derived from runtime
229    /// coordination statistics rather than an authoritative live in-flight count.
230    pub coordination_sessions: usize,
231
232    // --- Performance ---
233    /// Average round-trip time across all connections
234    pub avg_rtt: Duration,
235
236    /// Time since node started
237    pub uptime: Duration,
238}
239
240impl Default for NodeStatus {
241    fn default() -> Self {
242        Self {
243            peer_id: PeerId([0u8; 32]),
244            local_addr: "0.0.0.0:0".parse().unwrap_or_else(|_| {
245                SocketAddr::new(std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED), 0)
246            }),
247            external_addrs: Vec::new(),
248            nat_type: NatType::Unknown,
249            can_receive_direct: false,
250            direct_reachability_scope: None,
251            has_global_address: false,
252            port_mapping_active: false,
253            port_mapping_addr: None,
254            mdns_browsing: false,
255            mdns_advertising: false,
256            mdns_discovered_peers: 0,
257            relay_service_enabled: false,
258            coordinator_service_enabled: false,
259            bootstrap_service_enabled: false,
260            connected_peers: 0,
261            active_connections: 0,
262            pending_connections: 0,
263            direct_connections: 0,
264            relayed_connections: 0,
265            hole_punch_success_rate: 0.0,
266            is_relaying: false,
267            relay_sessions: 0,
268            relay_bytes_forwarded: 0,
269            is_coordinating: false,
270            coordination_sessions: 0,
271            avg_rtt: Duration::ZERO,
272            uptime: Duration::ZERO,
273        }
274    }
275}
276
277impl NodeStatus {
278    /// Check if node has any connectivity
279    pub fn is_connected(&self) -> bool {
280        self.connected_peers > 0
281    }
282
283    /// Check if node can help with NAT traversal
284    ///
285    /// Returns true if the node currently participates in the assist plane or
286    /// has peer-verified direct reachability.
287    pub fn can_help_traversal(&self) -> bool {
288        self.relay_service_enabled || self.coordinator_service_enabled || self.can_receive_direct
289    }
290
291    /// Get the total number of connections (direct + relayed)
292    pub fn total_connections(&self) -> u64 {
293        self.direct_connections + self.relayed_connections
294    }
295
296    /// Get the direct connection rate (0.0 - 1.0)
297    ///
298    /// Higher is better - indicates more direct connections vs relayed.
299    pub fn direct_rate(&self) -> f64 {
300        let total = self.total_connections();
301        if total == 0 {
302            0.0
303        } else {
304            self.direct_connections as f64 / total as f64
305        }
306    }
307}
308
309#[cfg(test)]
310mod tests {
311    use super::*;
312
313    #[test]
314    fn test_nat_type_display() {
315        assert_eq!(format!("{}", NatType::None), "None (No NAT detected)");
316        assert_eq!(format!("{}", NatType::FullCone), "Full Cone");
317        assert_eq!(
318            format!("{}", NatType::AddressRestricted),
319            "Address Restricted"
320        );
321        assert_eq!(format!("{}", NatType::PortRestricted), "Port Restricted");
322        assert_eq!(format!("{}", NatType::Symmetric), "Symmetric");
323        assert_eq!(format!("{}", NatType::Unknown), "Unknown");
324    }
325
326    #[test]
327    fn test_nat_type_default() {
328        assert_eq!(NatType::default(), NatType::Unknown);
329    }
330
331    #[test]
332    fn test_node_status_default() {
333        let status = NodeStatus::default();
334        assert_eq!(status.nat_type, NatType::Unknown);
335        assert!(!status.can_receive_direct);
336        assert_eq!(status.direct_reachability_scope, None);
337        assert!(!status.has_global_address);
338        assert!(!status.port_mapping_active);
339        assert_eq!(status.port_mapping_addr, None);
340        assert!(!status.mdns_browsing);
341        assert!(!status.mdns_advertising);
342        assert_eq!(status.mdns_discovered_peers, 0);
343        assert!(!status.relay_service_enabled);
344        assert!(!status.coordinator_service_enabled);
345        assert!(!status.bootstrap_service_enabled);
346        assert_eq!(status.connected_peers, 0);
347        assert!(!status.is_relaying);
348        assert!(!status.is_coordinating);
349    }
350
351    #[test]
352    fn test_is_connected() {
353        let mut status = NodeStatus::default();
354        assert!(!status.is_connected());
355
356        status.connected_peers = 1;
357        assert!(status.is_connected());
358    }
359
360    #[test]
361    fn test_can_help_traversal() {
362        let mut status = NodeStatus::default();
363        assert!(!status.can_help_traversal());
364
365        status.has_global_address = true;
366        assert!(
367            !status.can_help_traversal(),
368            "Global address alone must not imply direct reachability"
369        );
370
371        status.relay_service_enabled = true;
372        assert!(status.can_help_traversal());
373
374        status.relay_service_enabled = false;
375        status.coordinator_service_enabled = true;
376        assert!(status.can_help_traversal());
377
378        status.coordinator_service_enabled = false;
379        status.can_receive_direct = true;
380        status.direct_reachability_scope = Some(ReachabilityScope::Global);
381        assert!(status.can_help_traversal());
382    }
383
384    #[test]
385    fn test_total_connections() {
386        let mut status = NodeStatus::default();
387        status.direct_connections = 5;
388        status.relayed_connections = 3;
389        assert_eq!(status.total_connections(), 8);
390    }
391
392    #[test]
393    fn test_direct_rate() {
394        let mut status = NodeStatus::default();
395        assert_eq!(status.direct_rate(), 0.0);
396
397        status.direct_connections = 8;
398        status.relayed_connections = 2;
399        assert!((status.direct_rate() - 0.8).abs() < 0.001);
400    }
401
402    #[test]
403    fn test_status_is_debug() {
404        let status = NodeStatus::default();
405        let debug_str = format!("{:?}", status);
406        assert!(debug_str.contains("NodeStatus"));
407        assert!(debug_str.contains("nat_type"));
408        assert!(debug_str.contains("is_relaying"));
409    }
410
411    #[test]
412    fn test_status_is_clone() {
413        let mut status = NodeStatus::default();
414        status.connected_peers = 5;
415        status.is_relaying = true;
416
417        let cloned = status.clone();
418        assert_eq!(status.connected_peers, cloned.connected_peers);
419        assert_eq!(status.is_relaying, cloned.is_relaying);
420    }
421
422    #[test]
423    fn test_nat_type_equality() {
424        assert_eq!(NatType::FullCone, NatType::FullCone);
425        assert_ne!(NatType::FullCone, NatType::Symmetric);
426    }
427
428    #[test]
429    fn test_status_with_relay() {
430        let mut status = NodeStatus::default();
431        status.is_relaying = true;
432        status.relay_sessions = 3;
433        status.relay_bytes_forwarded = 1024 * 1024; // 1 MB
434
435        assert!(status.is_relaying);
436        assert_eq!(status.relay_sessions, 3);
437        assert_eq!(status.relay_bytes_forwarded, 1024 * 1024);
438    }
439
440    #[test]
441    fn test_status_with_coordinator() {
442        let mut status = NodeStatus::default();
443        status.is_coordinating = true;
444        status.coordination_sessions = 5;
445
446        assert!(status.is_coordinating);
447        assert_eq!(status.coordination_sessions, 5);
448    }
449
450    #[test]
451    fn test_external_addrs() {
452        let mut status = NodeStatus::default();
453        let addr1: SocketAddr = "1.2.3.4:9000".parse().unwrap();
454        let addr2: SocketAddr = "5.6.7.8:9001".parse().unwrap();
455
456        status.external_addrs.push(addr1);
457        status.external_addrs.push(addr2);
458
459        assert_eq!(status.external_addrs.len(), 2);
460        assert!(status.external_addrs.contains(&addr1));
461        assert!(status.external_addrs.contains(&addr2));
462    }
463
464    #[test]
465    fn test_direct_reachability_scope_tracks_observer_scope() {
466        let mut status = NodeStatus::default();
467        status.can_receive_direct = true;
468        status.direct_reachability_scope = Some(ReachabilityScope::LocalNetwork);
469
470        assert_eq!(
471            status.direct_reachability_scope,
472            Some(ReachabilityScope::LocalNetwork)
473        );
474    }
475
476    #[test]
477    fn test_port_mapping_status_fields() {
478        let mut status = NodeStatus::default();
479        let mapped_addr: SocketAddr = "198.51.100.77:41000".parse().unwrap();
480
481        status.port_mapping_active = true;
482        status.port_mapping_addr = Some(mapped_addr);
483
484        assert!(status.port_mapping_active);
485        assert_eq!(status.port_mapping_addr, Some(mapped_addr));
486    }
487
488    #[test]
489    fn test_mdns_status_fields() {
490        let mut status = NodeStatus::default();
491        status.mdns_browsing = true;
492        status.mdns_advertising = true;
493        status.mdns_discovered_peers = 2;
494
495        assert!(status.mdns_browsing);
496        assert!(status.mdns_advertising);
497        assert_eq!(status.mdns_discovered_peers, 2);
498    }
499
500    #[test]
501    fn test_assist_service_status_fields() {
502        let mut status = NodeStatus::default();
503        status.relay_service_enabled = true;
504        status.coordinator_service_enabled = true;
505        status.bootstrap_service_enabled = true;
506
507        assert!(status.relay_service_enabled);
508        assert!(status.coordinator_service_enabled);
509        assert!(status.bootstrap_service_enabled);
510    }
511}