Skip to main content

ant_quic/
node_event.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//! Unified events for P2P nodes
9//!
10//! This module provides [`NodeEvent`] - a single event type that covers
11//! all significant node activities including connections, best-effort NAT
12//! behavior hints,
13//! relay sessions, and data transfer.
14//!
15//! # Example
16//!
17//! ```rust,ignore
18//! use ant_quic::Node;
19//!
20//! let node = Node::new().await?;
21//! let mut events = node.subscribe();
22//!
23//! tokio::spawn(async move {
24//!     while let Ok(event) = events.recv().await {
25//!         match event {
26//!             NodeEvent::PeerConnected { peer_id, .. } => {
27//!                 println!("Connected to: {:?}", peer_id);
28//!             }
29//!             NodeEvent::NatTypeDetected { nat_type } => {
30//!                 println!("NAT behavior hint: {:?}", nat_type);
31//!             }
32//!             _ => {}
33//!         }
34//!     }
35//! });
36//! ```
37
38use std::net::SocketAddr;
39
40use crate::mdns::MdnsPeerRecord;
41use crate::nat_traversal_api::PeerId;
42use crate::node_status::NatType;
43pub use crate::p2p_endpoint::{DirectPathStatus, DirectPathUnavailableReason};
44pub use crate::reachability::TraversalMethod;
45use crate::transport::TransportAddr;
46
47/// Reason for peer disconnection
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub enum DisconnectReason {
50    /// Normal graceful shutdown
51    Graceful,
52    /// Connection timeout
53    Timeout,
54    /// Connection reset by peer
55    Reset,
56    /// Application-level close
57    ApplicationClose,
58    /// Idle timeout
59    Idle,
60    /// Transport error
61    TransportError(String),
62    /// Unknown reason
63    Unknown,
64}
65
66impl std::fmt::Display for DisconnectReason {
67    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68        match self {
69            Self::Graceful => write!(f, "graceful shutdown"),
70            Self::Timeout => write!(f, "connection timeout"),
71            Self::Reset => write!(f, "connection reset"),
72            Self::ApplicationClose => write!(f, "application close"),
73            Self::Idle => write!(f, "idle timeout"),
74            Self::TransportError(e) => write!(f, "transport error: {}", e),
75            Self::Unknown => write!(f, "unknown reason"),
76        }
77    }
78}
79
80/// Unified event type for all node activities
81///
82/// Subscribe to these events via `node.subscribe()` to monitor
83/// all significant node activities in real-time.
84#[derive(Debug, Clone)]
85pub enum NodeEvent {
86    // --- Peer Events ---
87    /// A peer connected successfully
88    PeerConnected {
89        /// The connected peer's ID
90        peer_id: PeerId,
91        /// The peer's address (supports all transport types)
92        addr: TransportAddr,
93        /// How the connection was established.
94        method: TraversalMethod,
95        /// Whether this is a direct connection (vs relayed or assisted)
96        direct: bool,
97    },
98
99    /// A peer disconnected
100    PeerDisconnected {
101        /// The disconnected peer's ID
102        peer_id: PeerId,
103        /// Reason for disconnection
104        reason: DisconnectReason,
105    },
106
107    /// Connection attempt failed
108    ConnectionFailed {
109        /// Target address that failed
110        addr: SocketAddr,
111        /// Error message
112        error: String,
113    },
114
115    // --- NAT Events ---
116    /// External address discovered
117    ///
118    /// This is the address as seen by other peers.
119    ExternalAddressDiscovered {
120        /// The discovered external address (supports all transport types)
121        addr: TransportAddr,
122    },
123
124    /// Best-effort router port mapping was established.
125    PortMappingEstablished {
126        /// The mapped external address.
127        external_addr: SocketAddr,
128    },
129
130    /// Best-effort router port mapping was renewed.
131    PortMappingRenewed {
132        /// The mapped external address.
133        external_addr: SocketAddr,
134    },
135
136    /// Best-effort router port mapping changed to a different public address.
137    PortMappingAddressChanged {
138        /// Previous mapped public address.
139        previous_addr: SocketAddr,
140        /// Current mapped public address.
141        external_addr: SocketAddr,
142    },
143
144    /// Best-effort router port mapping failed.
145    PortMappingFailed {
146        /// Human-readable failure detail.
147        error: String,
148    },
149
150    /// Best-effort router port mapping was removed or became inactive.
151    PortMappingRemoved {
152        /// The last mapped external address, when known.
153        external_addr: Option<SocketAddr>,
154    },
155
156    /// Best-effort NAT behavior hint updated.
157    NatTypeDetected {
158        /// Compatibility-oriented NAT behavior hint derived from native QUIC
159        /// observations rather than STUN-style NAT classification.
160        nat_type: NatType,
161    },
162
163    /// NAT traversal completed
164    NatTraversalComplete {
165        /// The peer we traversed to
166        peer_id: PeerId,
167        /// Whether traversal was successful
168        success: bool,
169        /// Connection method used
170        method: TraversalMethod,
171    },
172
173    /// Best-effort direct-path status for a peer.
174    DirectPathStatus {
175        /// Authenticated peer identity.
176        peer_id: PeerId,
177        /// Current direct-path status.
178        status: DirectPathStatus,
179    },
180
181    // --- Relay Events ---
182    /// Started relaying for a peer
183    RelaySessionStarted {
184        /// The peer we're relaying for
185        peer_id: PeerId,
186    },
187
188    /// Stopped relaying for a peer
189    RelaySessionEnded {
190        /// The peer we were relaying for
191        peer_id: PeerId,
192        /// Total bytes forwarded during session
193        bytes_forwarded: u64,
194    },
195
196    // --- Coordination Events ---
197    /// Started coordinating NAT traversal for peers
198    CoordinationStarted {
199        /// Peer A in the coordination
200        peer_a: PeerId,
201        /// Peer B in the coordination
202        peer_b: PeerId,
203    },
204
205    /// NAT traversal coordination completed
206    CoordinationComplete {
207        /// Peer A in the coordination
208        peer_a: PeerId,
209        /// Peer B in the coordination
210        peer_b: PeerId,
211        /// Whether coordination was successful
212        success: bool,
213    },
214
215    // --- mDNS Events ---
216    /// The local endpoint is advertising itself via first-party mDNS.
217    MdnsServiceAdvertised {
218        /// Service/application scope being advertised.
219        service: String,
220        /// Namespace/workspace scope, if configured.
221        namespace: Option<String>,
222        /// Full DNS-SD instance name being advertised.
223        instance_fullname: String,
224    },
225
226    /// A peer was discovered via first-party mDNS.
227    MdnsPeerDiscovered {
228        /// Structured mDNS discovery record.
229        peer: MdnsPeerRecord,
230    },
231
232    /// A previously discovered mDNS peer was updated.
233    MdnsPeerUpdated {
234        /// Structured mDNS discovery record.
235        peer: MdnsPeerRecord,
236    },
237
238    /// A previously discovered mDNS peer was removed.
239    MdnsPeerRemoved {
240        /// Structured mDNS discovery record.
241        peer: MdnsPeerRecord,
242    },
243
244    /// A discovered mDNS peer passed local eligibility checks.
245    MdnsPeerEligible {
246        /// Structured mDNS discovery record.
247        peer: MdnsPeerRecord,
248    },
249
250    /// A discovered mDNS peer was rejected by local eligibility checks.
251    MdnsPeerIneligible {
252        /// Structured mDNS discovery record.
253        peer: MdnsPeerRecord,
254        /// Human-readable reason for rejection.
255        reason: String,
256    },
257
258    /// A discovered mDNS peer requires explicit approval before auto-connect.
259    MdnsPeerApprovalRequired {
260        /// Structured mDNS discovery record.
261        peer: MdnsPeerRecord,
262        /// Human-readable policy reason.
263        reason: String,
264    },
265
266    /// An mDNS-driven auto-connect attempt was scheduled.
267    MdnsAutoConnectAttempted {
268        /// Structured mDNS discovery record.
269        peer: MdnsPeerRecord,
270        /// Candidate addresses routed through the unified connect path.
271        addresses: Vec<SocketAddr>,
272    },
273
274    /// An mDNS-driven auto-connect attempt succeeded.
275    MdnsAutoConnectSucceeded {
276        /// Structured mDNS discovery record.
277        peer: MdnsPeerRecord,
278        /// Authenticated peer identity learned from QUIC.
279        authenticated_peer_id: PeerId,
280        /// Connected remote transport address.
281        remote_addr: TransportAddr,
282    },
283
284    /// An mDNS-driven auto-connect attempt failed.
285    MdnsAutoConnectFailed {
286        /// Structured mDNS discovery record.
287        peer: MdnsPeerRecord,
288        /// Candidate addresses routed through the unified connect path.
289        addresses: Vec<SocketAddr>,
290        /// Human-readable failure detail.
291        error: String,
292    },
293
294    // --- Data Events ---
295    /// Data received from a peer
296    DataReceived {
297        /// The peer that sent data
298        peer_id: PeerId,
299        /// Stream ID (for multiplexed connections)
300        stream_id: u64,
301        /// Number of bytes received
302        bytes: usize,
303    },
304
305    /// Data sent to a peer
306    DataSent {
307        /// The peer we sent data to
308        peer_id: PeerId,
309        /// Stream ID
310        stream_id: u64,
311        /// Number of bytes sent
312        bytes: usize,
313    },
314}
315
316impl NodeEvent {
317    /// Check if this is a connection event
318    pub fn is_connection_event(&self) -> bool {
319        matches!(
320            self,
321            Self::PeerConnected { .. }
322                | Self::PeerDisconnected { .. }
323                | Self::ConnectionFailed { .. }
324        )
325    }
326
327    /// Check if this is a NAT-related event
328    pub fn is_nat_event(&self) -> bool {
329        matches!(
330            self,
331            Self::ExternalAddressDiscovered { .. }
332                | Self::PortMappingEstablished { .. }
333                | Self::PortMappingRenewed { .. }
334                | Self::PortMappingAddressChanged { .. }
335                | Self::PortMappingFailed { .. }
336                | Self::PortMappingRemoved { .. }
337                | Self::NatTypeDetected { .. }
338                | Self::NatTraversalComplete { .. }
339                | Self::DirectPathStatus { .. }
340        )
341    }
342
343    /// Check if this is a relay event
344    pub fn is_relay_event(&self) -> bool {
345        matches!(
346            self,
347            Self::RelaySessionStarted { .. } | Self::RelaySessionEnded { .. }
348        )
349    }
350
351    /// Check if this is a coordination event
352    pub fn is_coordination_event(&self) -> bool {
353        matches!(
354            self,
355            Self::CoordinationStarted { .. } | Self::CoordinationComplete { .. }
356        )
357    }
358
359    /// Check if this is a data event
360    pub fn is_data_event(&self) -> bool {
361        matches!(self, Self::DataReceived { .. } | Self::DataSent { .. })
362    }
363
364    /// Get the peer ID associated with this event (if any)
365    pub fn peer_id(&self) -> Option<&PeerId> {
366        match self {
367            Self::PeerConnected { peer_id, .. } => Some(peer_id),
368            Self::PeerDisconnected { peer_id, .. } => Some(peer_id),
369            Self::NatTraversalComplete { peer_id, .. } => Some(peer_id),
370            Self::DirectPathStatus { peer_id, .. } => Some(peer_id),
371            Self::RelaySessionStarted { peer_id } => Some(peer_id),
372            Self::RelaySessionEnded { peer_id, .. } => Some(peer_id),
373            Self::DataReceived { peer_id, .. } => Some(peer_id),
374            Self::DataSent { peer_id, .. } => Some(peer_id),
375            Self::MdnsAutoConnectSucceeded {
376                authenticated_peer_id,
377                ..
378            } => Some(authenticated_peer_id),
379            _ => None,
380        }
381    }
382}
383
384// Import P2pDisconnectReason for the From implementation
385use crate::p2p_endpoint::DisconnectReason as P2pDisconnectReason;
386
387/// Convert P2pDisconnectReason to NodeDisconnectReason (DisconnectReason in node_event)
388///
389/// This provides an idiomatic conversion between the two disconnect reason types
390/// used at different API layers.
391impl From<P2pDisconnectReason> for DisconnectReason {
392    fn from(reason: P2pDisconnectReason) -> Self {
393        match reason {
394            P2pDisconnectReason::Normal => Self::Graceful,
395            P2pDisconnectReason::Timeout => Self::Timeout,
396            P2pDisconnectReason::ProtocolError(e) => Self::TransportError(e),
397            P2pDisconnectReason::AuthenticationFailed => {
398                Self::TransportError("authentication failed".to_string())
399            }
400            P2pDisconnectReason::ConnectionLost => Self::Reset,
401            P2pDisconnectReason::RemoteClosed => Self::ApplicationClose,
402            // X0X-0062: a liveness-timeout disconnect maps to Timeout from
403            // the user-facing event surface — same shape as a transport
404            // idle-timeout. The LivenessTimeout-specific close reason is
405            // preserved at the lifecycle-event layer for observers that need
406            // to distinguish the two.
407            P2pDisconnectReason::LivenessTimeout => Self::Timeout,
408        }
409    }
410}
411
412#[cfg(test)]
413mod tests {
414    use super::*;
415
416    fn test_peer_id() -> PeerId {
417        PeerId([1u8; 32])
418    }
419
420    fn test_addr() -> SocketAddr {
421        "127.0.0.1:9000".parse().unwrap()
422    }
423
424    #[test]
425    fn test_peer_connected_event() {
426        let event = NodeEvent::PeerConnected {
427            peer_id: test_peer_id(),
428            addr: TransportAddr::Udp(test_addr()),
429            method: TraversalMethod::Direct,
430            direct: true,
431        };
432
433        assert!(event.is_connection_event());
434        assert!(!event.is_nat_event());
435        assert_eq!(event.peer_id(), Some(&test_peer_id()));
436    }
437
438    #[test]
439    fn test_peer_disconnected_event() {
440        let event = NodeEvent::PeerDisconnected {
441            peer_id: test_peer_id(),
442            reason: DisconnectReason::Graceful,
443        };
444
445        assert!(event.is_connection_event());
446        assert_eq!(event.peer_id(), Some(&test_peer_id()));
447    }
448
449    #[test]
450    fn test_nat_type_detected_event() {
451        let event = NodeEvent::NatTypeDetected {
452            nat_type: NatType::FullCone,
453        };
454
455        assert!(event.is_nat_event());
456        assert!(!event.is_connection_event());
457        assert!(event.peer_id().is_none());
458    }
459
460    #[test]
461    fn test_direct_path_status_event() {
462        let event = NodeEvent::DirectPathStatus {
463            peer_id: test_peer_id(),
464            status: DirectPathStatus::BestEffortUnavailable {
465                reason: DirectPathUnavailableReason::NatUnreachable,
466            },
467        };
468
469        assert!(event.is_nat_event());
470        assert_eq!(event.peer_id(), Some(&test_peer_id()));
471    }
472
473    #[test]
474    fn test_relay_session_events() {
475        let start = NodeEvent::RelaySessionStarted {
476            peer_id: test_peer_id(),
477        };
478
479        let end = NodeEvent::RelaySessionEnded {
480            peer_id: test_peer_id(),
481            bytes_forwarded: 1024,
482        };
483
484        assert!(start.is_relay_event());
485        assert!(end.is_relay_event());
486        assert!(!start.is_connection_event());
487    }
488
489    #[test]
490    fn test_coordination_events() {
491        let peer_a = PeerId([1u8; 32]);
492        let peer_b = PeerId([2u8; 32]);
493
494        let start = NodeEvent::CoordinationStarted { peer_a, peer_b };
495
496        let complete = NodeEvent::CoordinationComplete {
497            peer_a,
498            peer_b,
499            success: true,
500        };
501
502        assert!(start.is_coordination_event());
503        assert!(complete.is_coordination_event());
504    }
505
506    #[test]
507    fn test_data_events() {
508        let recv = NodeEvent::DataReceived {
509            peer_id: test_peer_id(),
510            stream_id: 1,
511            bytes: 1024,
512        };
513
514        let send = NodeEvent::DataSent {
515            peer_id: test_peer_id(),
516            stream_id: 1,
517            bytes: 512,
518        };
519
520        assert!(recv.is_data_event());
521        assert!(send.is_data_event());
522        assert!(!recv.is_connection_event());
523    }
524
525    #[test]
526    fn test_disconnect_reason_display() {
527        assert_eq!(
528            format!("{}", DisconnectReason::Graceful),
529            "graceful shutdown"
530        );
531        assert_eq!(
532            format!("{}", DisconnectReason::Timeout),
533            "connection timeout"
534        );
535        assert_eq!(
536            format!("{}", DisconnectReason::TransportError("test".to_string())),
537            "transport error: test"
538        );
539    }
540
541    #[test]
542    fn test_traversal_method_display() {
543        assert_eq!(format!("{}", TraversalMethod::Direct), "direct");
544        assert_eq!(format!("{}", TraversalMethod::HolePunch), "hole punch");
545        assert_eq!(format!("{}", TraversalMethod::Relay), "relay");
546        assert_eq!(
547            format!("{}", TraversalMethod::PortPrediction),
548            "port prediction"
549        );
550    }
551
552    #[test]
553    fn test_events_are_clone() {
554        let event = NodeEvent::PeerConnected {
555            peer_id: test_peer_id(),
556            addr: TransportAddr::Udp(test_addr()),
557            method: TraversalMethod::Direct,
558            direct: true,
559        };
560
561        let cloned = event.clone();
562        assert!(cloned.is_connection_event());
563    }
564
565    #[test]
566    fn test_events_are_debug() {
567        let event = NodeEvent::NatTypeDetected {
568            nat_type: NatType::Symmetric,
569        };
570
571        let debug_str = format!("{:?}", event);
572        assert!(debug_str.contains("NatTypeDetected"));
573        assert!(debug_str.contains("Symmetric"));
574    }
575
576    #[test]
577    fn test_connection_failed_event() {
578        let event = NodeEvent::ConnectionFailed {
579            addr: test_addr(),
580            error: "connection refused".to_string(),
581        };
582
583        assert!(event.is_connection_event());
584        assert!(event.peer_id().is_none());
585    }
586
587    #[test]
588    fn test_external_address_discovered() {
589        let event = NodeEvent::ExternalAddressDiscovered {
590            addr: TransportAddr::Udp("1.2.3.4:9000".parse().unwrap()),
591        };
592
593        assert!(event.is_nat_event());
594        assert!(event.peer_id().is_none());
595    }
596}