Skip to main content

ant_quic/
path.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//! Read-only path handles for observing per-path connection state.
9
10use std::{
11    fmt,
12    net::SocketAddr,
13    sync::{Arc, Mutex},
14};
15
16use crate::{connection::PathStats, high_level::WeakConnectionHandle};
17
18/// Identifier for a QUIC connection path.
19///
20/// The current read-only skeleton exposes only the primary single-path route.
21/// Future multipath support will allocate additional IDs.
22#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
23pub struct PathId(u64);
24
25impl PathId {
26    /// The primary single-path route used before multipath is negotiated.
27    pub const PRIMARY: Self = Self(0);
28
29    /// Return the numeric path identifier.
30    pub const fn get(self) -> u64 {
31        self.0
32    }
33}
34
35impl From<u64> for PathId {
36    fn from(value: u64) -> Self {
37        Self(value)
38    }
39}
40
41impl From<PathId> for u64 {
42    fn from(value: PathId) -> Self {
43        value.0
44    }
45}
46
47impl fmt::Display for PathId {
48    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49        self.0.fmt(f)
50    }
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56    use crate::connection::PathStats;
57
58    // ── PathId tests ──
59
60    #[test]
61    fn path_id_primary_is_zero() {
62        assert_eq!(PathId::PRIMARY.get(), 0);
63    }
64
65    #[test]
66    fn path_id_from_u64() {
67        let id: PathId = 42u64.into();
68        assert_eq!(id.get(), 42);
69    }
70
71    #[test]
72    fn path_id_into_u64() {
73        let id = PathId::PRIMARY;
74        let val: u64 = id.into();
75        assert_eq!(val, 0);
76    }
77
78    #[test]
79    fn path_id_default_is_zero() {
80        let id = PathId::default();
81        assert_eq!(id.get(), 0);
82        assert_eq!(id, PathId::PRIMARY);
83    }
84
85    #[test]
86    fn path_id_equality() {
87        assert_eq!(PathId::from(1u64), PathId::from(1u64));
88        assert_ne!(PathId::from(1u64), PathId::from(2u64));
89    }
90
91    #[test]
92    fn path_id_ordering() {
93        assert!(PathId::from(0u64) < PathId::from(1u64));
94        assert!(PathId::from(1u64) > PathId::from(0u64));
95    }
96
97    #[test]
98    fn path_id_display() {
99        assert_eq!(format!("{}", PathId::PRIMARY), "0");
100        assert_eq!(format!("{}", PathId::from(42u64)), "42");
101    }
102
103    #[test]
104    fn path_id_debug() {
105        let debug = format!("{:?}", PathId::from(42u64));
106        assert!(debug.contains("42"));
107    }
108
109    #[test]
110    fn path_id_clone() {
111        let a = PathId::from(5u64);
112        let b = a;
113        assert_eq!(a, b);
114    }
115
116    #[test]
117    fn path_id_hash() {
118        use std::collections::hash_map::DefaultHasher;
119        use std::hash::{Hash, Hasher};
120
121        let a = PathId::from(10u64);
122        let b = PathId::from(10u64);
123        let mut ha = DefaultHasher::new();
124        let mut hb = DefaultHasher::new();
125        a.hash(&mut ha);
126        b.hash(&mut hb);
127        assert_eq!(ha.finish(), hb.finish());
128    }
129
130    // ── PathSnapshot tests ──
131
132    #[test]
133    fn path_snapshot_default() {
134        let stats = PathStats::default();
135        // PathSnapshot is pub(crate), test via construction
136        let snapshot = PathSnapshot {
137            stats,
138            remote_address: "127.0.0.1:9000".parse().unwrap(),
139            observed_external_addr: None,
140        };
141        assert_eq!(snapshot.remote_address.port(), 9000);
142        assert!(snapshot.observed_external_addr.is_none());
143    }
144
145    #[test]
146    fn path_snapshot_with_external_addr() {
147        let stats = PathStats::default();
148        let snapshot = PathSnapshot {
149            stats,
150            remote_address: "192.168.1.1:9000".parse().unwrap(),
151            observed_external_addr: Some("10.0.0.1:9001".parse().unwrap()),
152        };
153        assert!(snapshot.observed_external_addr.is_some());
154        assert_eq!(
155            snapshot.observed_external_addr.unwrap().to_string(),
156            "10.0.0.1:9001"
157        );
158    }
159
160    #[test]
161    fn path_snapshot_clone_copy() {
162        let stats = PathStats::default();
163        let s1 = PathSnapshot {
164            stats,
165            remote_address: "127.0.0.1:80".parse().unwrap(),
166            observed_external_addr: None,
167        };
168        let s2 = s1;
169        assert_eq!(s1.remote_address, s2.remote_address);
170    }
171
172    // ── RetainedPathSnapshot tests ──
173
174    #[test]
175    fn retained_path_snapshot_store_and_load() {
176        let stats = PathStats::default();
177        let snapshot = PathSnapshot {
178            stats,
179            remote_address: "127.0.0.1:9000".parse().unwrap(),
180            observed_external_addr: None,
181        };
182        let retained = RetainedPathSnapshot::new(snapshot);
183        let loaded = retained.load();
184        assert_eq!(loaded.remote_address.port(), 9000);
185    }
186
187    #[test]
188    fn retained_path_snapshot_overwrite() {
189        let stats = PathStats::default();
190        let snapshot1 = PathSnapshot {
191            stats,
192            remote_address: "127.0.0.1:1".parse().unwrap(),
193            observed_external_addr: None,
194        };
195        let retained = RetainedPathSnapshot::new(snapshot1);
196
197        let snapshot2 = PathSnapshot {
198            stats: PathStats::default(),
199            remote_address: "127.0.0.1:2".parse().unwrap(),
200            observed_external_addr: None,
201        };
202        retained.store(snapshot2);
203
204        let loaded = retained.load();
205        assert_eq!(loaded.remote_address.port(), 2);
206    }
207
208    #[test]
209    fn retained_path_snapshot_clone() {
210        let stats = PathStats::default();
211        let snapshot = PathSnapshot {
212            stats,
213            remote_address: "10.0.0.1:8000".parse().unwrap(),
214            observed_external_addr: None,
215        };
216        let retained = RetainedPathSnapshot::new(snapshot);
217        let cloned = retained.clone();
218        assert_eq!(retained.load().remote_address, cloned.load().remote_address);
219    }
220
221    #[test]
222    fn retained_path_snapshot_concurrent_independence() {
223        let stats = PathStats::default();
224        let snapshot = PathSnapshot {
225            stats,
226            remote_address: "127.0.0.1:9000".parse().unwrap(),
227            observed_external_addr: None,
228        };
229        let retained = RetainedPathSnapshot::new(snapshot);
230        let cloned = retained.clone();
231
232        // Update one, the other should still see the old value
233        let new_snapshot = PathSnapshot {
234            stats: PathStats::default(),
235            remote_address: "127.0.0.1:9999".parse().unwrap(),
236            observed_external_addr: None,
237        };
238        retained.store(new_snapshot);
239
240        assert_eq!(retained.load().remote_address.port(), 9999);
241        assert_eq!(cloned.load().remote_address.port(), 9999); // shared Arc
242    }
243}
244
245/// Snapshot of read-only path state.
246#[derive(Debug, Clone, Copy)]
247pub(crate) struct PathSnapshot {
248    pub(crate) stats: PathStats,
249    pub(crate) remote_address: SocketAddr,
250    pub(crate) observed_external_addr: Option<SocketAddr>,
251}
252
253#[derive(Debug, Clone)]
254struct RetainedPathSnapshot(Arc<Mutex<PathSnapshot>>);
255
256impl RetainedPathSnapshot {
257    fn new(snapshot: PathSnapshot) -> Self {
258        Self(Arc::new(Mutex::new(snapshot)))
259    }
260
261    fn load(&self) -> PathSnapshot {
262        match self.0.lock() {
263            Ok(snapshot) => *snapshot,
264            Err(poisoned) => *poisoned.into_inner(),
265        }
266    }
267
268    fn store(&self, snapshot: PathSnapshot) {
269        match self.0.lock() {
270            Ok(mut retained) => *retained = snapshot,
271            Err(poisoned) => *poisoned.into_inner() = snapshot,
272        }
273    }
274}
275
276/// Read-only handle to a QUIC connection path.
277///
278/// `Path` does not keep the underlying connection alive. Accessors read live
279/// state while the connection exists and fall back to the retained snapshot
280/// after the connection/path has gone away.
281#[derive(Debug, Clone)]
282pub struct Path {
283    conn_handle: WeakConnectionHandle,
284    id: PathId,
285    retained: RetainedPathSnapshot,
286}
287
288impl Path {
289    pub(crate) fn new(
290        conn_handle: WeakConnectionHandle,
291        id: PathId,
292        snapshot: PathSnapshot,
293    ) -> Self {
294        Self {
295            conn_handle,
296            id,
297            retained: RetainedPathSnapshot::new(snapshot),
298        }
299    }
300
301    fn live_snapshot(&self) -> Option<PathSnapshot> {
302        self.conn_handle
303            .upgrade()
304            .and_then(|conn| conn.path_snapshot(self.id))
305    }
306
307    fn snapshot(&self) -> PathSnapshot {
308        if let Some(snapshot) = self.live_snapshot() {
309            self.retained.store(snapshot);
310            snapshot
311        } else {
312            self.retained.load()
313        }
314    }
315
316    /// Return this path's identifier.
317    pub fn id(&self) -> PathId {
318        self.id
319    }
320
321    /// Return the latest readable statistics for this path.
322    pub fn stats(&self) -> PathStats {
323        self.snapshot().stats
324    }
325
326    /// Return the peer UDP address associated with this path.
327    pub fn remote_address(&self) -> SocketAddr {
328        self.snapshot().remote_address
329    }
330
331    /// Return the external/reflexive address observed for this path.
332    pub fn observed_external_addr(&self) -> Option<SocketAddr> {
333        self.snapshot().observed_external_addr
334    }
335
336    /// Downgrade this path to a weak handle.
337    pub fn weak_handle(&self) -> WeakPathHandle {
338        WeakPathHandle {
339            conn_handle: self.conn_handle.clone(),
340            id: self.id,
341            retained: self.retained.clone(),
342        }
343    }
344}
345
346impl Drop for Path {
347    fn drop(&mut self) {
348        if let Some(snapshot) = self.live_snapshot() {
349            self.retained.store(snapshot);
350        }
351    }
352}
353
354/// Weak read-only handle to a QUIC connection path.
355///
356/// The handle can expose retained path state after the connection/path has
357/// closed without keeping the connection alive.
358#[derive(Debug, Clone)]
359pub struct WeakPathHandle {
360    conn_handle: WeakConnectionHandle,
361    id: PathId,
362    retained: RetainedPathSnapshot,
363}
364
365impl WeakPathHandle {
366    fn live_snapshot(&self) -> Option<PathSnapshot> {
367        self.conn_handle
368            .upgrade()
369            .and_then(|conn| conn.path_snapshot(self.id))
370    }
371
372    fn snapshot(&self) -> PathSnapshot {
373        if let Some(snapshot) = self.live_snapshot() {
374            self.retained.store(snapshot);
375            snapshot
376        } else {
377            self.retained.load()
378        }
379    }
380
381    /// Return this path's identifier.
382    pub fn id(&self) -> PathId {
383        self.id
384    }
385
386    /// Upgrade to a live path handle if the path's connection is still alive.
387    pub fn upgrade(&self) -> Option<Path> {
388        if !self.conn_handle.is_alive() {
389            return None;
390        }
391
392        let snapshot = self.live_snapshot()?;
393        Some(Path {
394            conn_handle: self.conn_handle.clone(),
395            id: self.id,
396            retained: RetainedPathSnapshot::new(snapshot),
397        })
398    }
399
400    /// Return true while the underlying path's connection is still alive.
401    pub fn is_alive(&self) -> bool {
402        self.conn_handle.is_alive() && self.live_snapshot().is_some()
403    }
404
405    /// Return the latest readable statistics for this path.
406    pub fn stats(&self) -> PathStats {
407        self.snapshot().stats
408    }
409
410    /// Return the retained or live peer UDP address associated with this path.
411    pub fn remote_address(&self) -> SocketAddr {
412        self.snapshot().remote_address
413    }
414
415    /// Return the retained or live external/reflexive address for this path.
416    pub fn observed_external_addr(&self) -> Option<SocketAddr> {
417        self.snapshot().observed_external_addr
418    }
419}