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 single snapshot of everything
11//! about a node's current state, including NAT type, connectivity,
12//! relay status, 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 type: {:?}", status.nat_type);
23//! println!("Can receive direct: {}", status.can_receive_direct);
24//! println!("Acting as relay: {}", status.is_relaying);
25//! println!("Relay sessions: {}", status.relay_sessions);
26//! ```
27
28use std::net::SocketAddr;
29use std::time::Duration;
30
31use crate::nat_traversal_api::PeerId;
32
33/// Detected NAT type for the node
34///
35/// NAT type affects connectivity - some types are easier to traverse than others.
36/// The node automatically detects its NAT type and adjusts traversal strategies.
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
38pub enum NatType {
39    /// No NAT detected - direct public connectivity
40    ///
41    /// The node has a public IP address and can accept connections directly.
42    None,
43
44    /// Full cone NAT - easiest to traverse
45    ///
46    /// Any external host can send packets to the internal IP:port once
47    /// the internal host has sent a packet to any external host.
48    FullCone,
49
50    /// Address-restricted cone NAT
51    ///
52    /// External hosts can send packets only if the internal host
53    /// has previously sent to that specific external IP.
54    AddressRestricted,
55
56    /// Port-restricted cone NAT
57    ///
58    /// External hosts can send packets only if the internal host
59    /// has previously sent to that specific external IP:port.
60    PortRestricted,
61
62    /// Symmetric NAT - hardest to traverse
63    ///
64    /// Each outgoing connection gets a different external port.
65    /// Requires prediction algorithms or relay fallback.
66    Symmetric,
67
68    /// NAT type not yet determined
69    ///
70    /// The node hasn't completed NAT detection yet.
71    #[default]
72    Unknown,
73}
74
75impl std::fmt::Display for NatType {
76    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77        match self {
78            Self::None => write!(f, "None (Public IP)"),
79            Self::FullCone => write!(f, "Full Cone"),
80            Self::AddressRestricted => write!(f, "Address Restricted"),
81            Self::PortRestricted => write!(f, "Port Restricted"),
82            Self::Symmetric => write!(f, "Symmetric"),
83            Self::Unknown => write!(f, "Unknown"),
84        }
85    }
86}
87
88/// Comprehensive node status snapshot
89///
90/// This struct provides a complete view of the node's current state,
91/// including identity, connectivity, NAT status, relay status, and performance.
92///
93/// # Status Categories
94///
95/// - **Identity**: peer_id, local_addr, external_addrs
96/// - **NAT Status**: nat_type, can_receive_direct, has_public_ip
97/// - **Connections**: connected_peers, active_connections, pending_connections
98/// - **NAT Traversal**: direct_connections, relayed_connections, hole_punch_success_rate
99/// - **Relay**: is_relaying, relay_sessions, relay_bytes_forwarded
100/// - **Coordinator**: is_coordinating, coordination_sessions
101/// - **Performance**: avg_rtt, uptime
102#[derive(Debug, Clone)]
103pub struct NodeStatus {
104    // --- Identity ---
105    /// This node's peer ID (derived from public key)
106    pub peer_id: PeerId,
107
108    /// Local bind address
109    pub local_addr: SocketAddr,
110
111    /// All discovered external addresses
112    ///
113    /// These are addresses as seen by other peers. Multiple addresses
114    /// may be discovered when behind NAT or with multiple interfaces.
115    pub external_addrs: Vec<SocketAddr>,
116
117    // --- NAT Status ---
118    /// Detected NAT type
119    pub nat_type: NatType,
120
121    /// Whether this node can receive direct connections
122    ///
123    /// `true` if the node has a public IP or is behind a traversable NAT.
124    pub can_receive_direct: bool,
125
126    /// Whether this node has a public IP
127    ///
128    /// `true` if local_addr matches an external_addr (no NAT).
129    pub has_public_ip: bool,
130
131    // --- Connections ---
132    /// Number of connected peers
133    pub connected_peers: usize,
134
135    /// Number of active connections (may differ from peers if multiplexed)
136    pub active_connections: usize,
137
138    /// Number of pending connection attempts
139    pub pending_connections: usize,
140
141    // --- NAT Traversal Stats ---
142    /// Total successful direct connections (no relay)
143    pub direct_connections: u64,
144
145    /// Total connections that required relay
146    pub relayed_connections: u64,
147
148    /// Hole punch success rate (0.0 - 1.0)
149    ///
150    /// Calculated from NAT traversal attempts vs successes.
151    pub hole_punch_success_rate: f64,
152
153    // --- Relay Status (NEW - key visibility) ---
154    /// Whether this node is currently acting as a relay for others
155    ///
156    /// `true` if this node has public connectivity and is forwarding
157    /// traffic for peers behind restrictive NATs.
158    pub is_relaying: bool,
159
160    /// Number of active relay sessions
161    pub relay_sessions: usize,
162
163    /// Total bytes forwarded as relay
164    pub relay_bytes_forwarded: u64,
165
166    // --- Coordinator Status (NEW - key visibility) ---
167    /// Whether this node is coordinating NAT traversal
168    ///
169    /// `true` if this node is helping peers coordinate hole punching.
170    /// All nodes with public connectivity act as coordinators.
171    pub is_coordinating: bool,
172
173    /// Number of active coordination sessions
174    pub coordination_sessions: usize,
175
176    // --- Performance ---
177    /// Average round-trip time across all connections
178    pub avg_rtt: Duration,
179
180    /// Time since node started
181    pub uptime: Duration,
182}
183
184impl Default for NodeStatus {
185    fn default() -> Self {
186        Self {
187            peer_id: PeerId([0u8; 32]),
188            local_addr: "0.0.0.0:0".parse().unwrap_or_else(|_| {
189                SocketAddr::new(std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED), 0)
190            }),
191            external_addrs: Vec::new(),
192            nat_type: NatType::Unknown,
193            can_receive_direct: false,
194            has_public_ip: false,
195            connected_peers: 0,
196            active_connections: 0,
197            pending_connections: 0,
198            direct_connections: 0,
199            relayed_connections: 0,
200            hole_punch_success_rate: 0.0,
201            is_relaying: false,
202            relay_sessions: 0,
203            relay_bytes_forwarded: 0,
204            is_coordinating: false,
205            coordination_sessions: 0,
206            avg_rtt: Duration::ZERO,
207            uptime: Duration::ZERO,
208        }
209    }
210}
211
212impl NodeStatus {
213    /// Check if node has any connectivity
214    pub fn is_connected(&self) -> bool {
215        self.connected_peers > 0
216    }
217
218    /// Check if node can help with NAT traversal
219    ///
220    /// Returns true if the node has public connectivity and can
221    /// act as coordinator/relay for other peers.
222    pub fn can_help_traversal(&self) -> bool {
223        self.has_public_ip || self.can_receive_direct
224    }
225
226    /// Get the total number of connections (direct + relayed)
227    pub fn total_connections(&self) -> u64 {
228        self.direct_connections + self.relayed_connections
229    }
230
231    /// Get the direct connection rate (0.0 - 1.0)
232    ///
233    /// Higher is better - indicates more direct connections vs relayed.
234    pub fn direct_rate(&self) -> f64 {
235        let total = self.total_connections();
236        if total == 0 {
237            0.0
238        } else {
239            self.direct_connections as f64 / total as f64
240        }
241    }
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247
248    #[test]
249    fn test_nat_type_display() {
250        assert_eq!(format!("{}", NatType::None), "None (Public IP)");
251        assert_eq!(format!("{}", NatType::FullCone), "Full Cone");
252        assert_eq!(
253            format!("{}", NatType::AddressRestricted),
254            "Address Restricted"
255        );
256        assert_eq!(format!("{}", NatType::PortRestricted), "Port Restricted");
257        assert_eq!(format!("{}", NatType::Symmetric), "Symmetric");
258        assert_eq!(format!("{}", NatType::Unknown), "Unknown");
259    }
260
261    #[test]
262    fn test_nat_type_default() {
263        assert_eq!(NatType::default(), NatType::Unknown);
264    }
265
266    #[test]
267    fn test_node_status_default() {
268        let status = NodeStatus::default();
269        assert_eq!(status.nat_type, NatType::Unknown);
270        assert!(!status.can_receive_direct);
271        assert!(!status.has_public_ip);
272        assert_eq!(status.connected_peers, 0);
273        assert!(!status.is_relaying);
274        assert!(!status.is_coordinating);
275    }
276
277    #[test]
278    fn test_is_connected() {
279        let mut status = NodeStatus::default();
280        assert!(!status.is_connected());
281
282        status.connected_peers = 1;
283        assert!(status.is_connected());
284    }
285
286    #[test]
287    fn test_can_help_traversal() {
288        let mut status = NodeStatus::default();
289        assert!(!status.can_help_traversal());
290
291        status.has_public_ip = true;
292        assert!(status.can_help_traversal());
293
294        status.has_public_ip = false;
295        status.can_receive_direct = true;
296        assert!(status.can_help_traversal());
297    }
298
299    #[test]
300    fn test_total_connections() {
301        let mut status = NodeStatus::default();
302        status.direct_connections = 5;
303        status.relayed_connections = 3;
304        assert_eq!(status.total_connections(), 8);
305    }
306
307    #[test]
308    fn test_direct_rate() {
309        let mut status = NodeStatus::default();
310        assert_eq!(status.direct_rate(), 0.0);
311
312        status.direct_connections = 8;
313        status.relayed_connections = 2;
314        assert!((status.direct_rate() - 0.8).abs() < 0.001);
315    }
316
317    #[test]
318    fn test_status_is_debug() {
319        let status = NodeStatus::default();
320        let debug_str = format!("{:?}", status);
321        assert!(debug_str.contains("NodeStatus"));
322        assert!(debug_str.contains("nat_type"));
323        assert!(debug_str.contains("is_relaying"));
324    }
325
326    #[test]
327    fn test_status_is_clone() {
328        let mut status = NodeStatus::default();
329        status.connected_peers = 5;
330        status.is_relaying = true;
331
332        let cloned = status.clone();
333        assert_eq!(status.connected_peers, cloned.connected_peers);
334        assert_eq!(status.is_relaying, cloned.is_relaying);
335    }
336
337    #[test]
338    fn test_nat_type_equality() {
339        assert_eq!(NatType::FullCone, NatType::FullCone);
340        assert_ne!(NatType::FullCone, NatType::Symmetric);
341    }
342
343    #[test]
344    fn test_status_with_relay() {
345        let mut status = NodeStatus::default();
346        status.is_relaying = true;
347        status.relay_sessions = 3;
348        status.relay_bytes_forwarded = 1024 * 1024; // 1 MB
349
350        assert!(status.is_relaying);
351        assert_eq!(status.relay_sessions, 3);
352        assert_eq!(status.relay_bytes_forwarded, 1024 * 1024);
353    }
354
355    #[test]
356    fn test_status_with_coordinator() {
357        let mut status = NodeStatus::default();
358        status.is_coordinating = true;
359        status.coordination_sessions = 5;
360
361        assert!(status.is_coordinating);
362        assert_eq!(status.coordination_sessions, 5);
363    }
364
365    #[test]
366    fn test_external_addrs() {
367        let mut status = NodeStatus::default();
368        let addr1: SocketAddr = "1.2.3.4:9000".parse().unwrap();
369        let addr2: SocketAddr = "5.6.7.8:9001".parse().unwrap();
370
371        status.external_addrs.push(addr1);
372        status.external_addrs.push(addr2);
373
374        assert_eq!(status.external_addrs.len(), 2);
375        assert!(status.external_addrs.contains(&addr1));
376        assert!(status.external_addrs.contains(&addr2));
377    }
378}