ant_quic/
path_selection.rs

1//! RTT-based path selection with hysteresis
2//!
3//! Path selection algorithm:
4//! - Lower RTT paths preferred
5//! - 5ms hysteresis to prevent flapping
6//! - 3ms advantage for IPv6
7//! - Direct paths strongly preferred over relay
8
9use std::net::SocketAddr;
10use std::time::Duration;
11
12/// Maximum number of candidates per peer
13pub const MAX_CANDIDATES_PER_PEER: usize = 30;
14
15/// Maximum number of inactive candidates to keep
16pub const MAX_INACTIVE_CANDIDATES: usize = 10;
17
18/// Minimum RTT improvement required to switch paths (prevents flapping)
19pub const RTT_SWITCHING_MIN: Duration = Duration::from_millis(5);
20
21/// RTT advantage given to IPv6 paths
22pub const IPV6_RTT_ADVANTAGE: Duration = Duration::from_millis(3);
23
24/// Type of path connection
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum PathType {
27    /// Direct UDP connection
28    Direct,
29    /// Via relay server
30    Relay,
31}
32
33/// A candidate path with measured RTT
34#[derive(Debug, Clone)]
35pub struct PathCandidate {
36    /// Socket address of the path
37    pub addr: SocketAddr,
38    /// Measured round-trip time
39    pub rtt: Duration,
40    /// Type of path (direct or relay)
41    pub path_type: PathType,
42}
43
44impl PathCandidate {
45    /// Create a new direct path candidate
46    pub fn new(addr: SocketAddr, rtt: Duration) -> Self {
47        Self {
48            addr,
49            rtt,
50            path_type: PathType::Direct,
51        }
52    }
53
54    /// Create a direct path candidate
55    pub fn direct(addr: SocketAddr, rtt: Duration) -> Self {
56        Self {
57            addr,
58            rtt,
59            path_type: PathType::Direct,
60        }
61    }
62
63    /// Create a relay path candidate
64    pub fn relay(addr: SocketAddr, rtt: Duration) -> Self {
65        Self {
66            addr,
67            rtt,
68            path_type: PathType::Relay,
69        }
70    }
71
72    /// Check if this is a direct path
73    pub fn is_direct(&self) -> bool {
74        self.path_type == PathType::Direct
75    }
76
77    /// Check if this is a relay path
78    pub fn is_relay(&self) -> bool {
79        self.path_type == PathType::Relay
80    }
81
82    /// Calculate effective RTT (with IPv6 advantage applied)
83    pub fn effective_rtt(&self) -> Duration {
84        if self.addr.is_ipv6() {
85            self.rtt.saturating_sub(IPV6_RTT_ADVANTAGE)
86        } else {
87            self.rtt
88        }
89    }
90}
91
92/// Select the best path from candidates
93///
94/// Algorithm:
95/// 1. Prefer direct paths over relay paths
96/// 2. Among same type, prefer lower RTT
97/// 3. Apply IPv6 advantage (3ms)
98/// 4. Apply hysteresis (5ms) when switching from current path
99pub fn select_best_path(
100    paths: &[PathCandidate],
101    current: Option<&PathCandidate>,
102) -> Option<PathCandidate> {
103    if paths.is_empty() {
104        return None;
105    }
106
107    // Separate direct and relay paths
108    let direct_paths: Vec<_> = paths.iter().filter(|p| p.is_direct()).collect();
109    let relay_paths: Vec<_> = paths.iter().filter(|p| p.is_relay()).collect();
110
111    // Find best direct path
112    let best_direct = find_best_by_rtt(&direct_paths);
113
114    // Find best relay path
115    let best_relay = find_best_by_rtt(&relay_paths);
116
117    // Determine the best new path (prefer direct)
118    let best_new = match (best_direct, best_relay) {
119        (Some(direct), _) => Some(direct),
120        (None, Some(relay)) => Some(relay),
121        (None, None) => None,
122    };
123
124    // Apply hysteresis if we have a current path
125    match (current, best_new) {
126        (None, best) => best.cloned(),
127        (Some(current), None) => Some(current.clone()),
128        (Some(current), Some(new)) => {
129            // Never switch from direct to relay
130            if current.is_direct() && new.is_relay() {
131                return Some(current.clone());
132            }
133
134            // Check if new path is significantly better
135            let current_eff = current.effective_rtt();
136            let new_eff = new.effective_rtt();
137
138            if current_eff > new_eff + RTT_SWITCHING_MIN {
139                // New path is significantly better
140                Some(new.clone())
141            } else {
142                // Keep current path (hysteresis)
143                Some(current.clone())
144            }
145        }
146    }
147}
148
149/// Find the path with lowest effective RTT
150fn find_best_by_rtt<'a>(paths: &[&'a PathCandidate]) -> Option<&'a PathCandidate> {
151    paths.iter().min_by_key(|p| p.effective_rtt()).copied()
152}
153
154/// Compare IPv4 and IPv6 paths, applying IPv6 advantage
155pub fn select_v4_v6(
156    v4_addr: SocketAddr,
157    v4_rtt: Duration,
158    v6_addr: SocketAddr,
159    v6_rtt: Duration,
160) -> (SocketAddr, Duration) {
161    // Apply IPv6 advantage
162    let v6_effective = v6_rtt.saturating_sub(IPV6_RTT_ADVANTAGE);
163
164    if v6_effective <= v4_rtt {
165        (v6_addr, v6_rtt)
166    } else {
167        (v4_addr, v4_rtt)
168    }
169}
170
171// ============================================================================
172// PathManager for tracking and closing redundant paths
173// ============================================================================
174
175use std::collections::HashMap;
176
177/// Minimum number of direct paths to keep open
178pub const MIN_DIRECT_PATHS: usize = 2;
179
180/// Information about a tracked path
181#[derive(Debug, Clone)]
182pub struct PathInfo {
183    /// Socket address of the path
184    pub addr: SocketAddr,
185    /// Type of path (direct or relay)
186    pub path_type: PathType,
187    /// Measured RTT if available
188    pub rtt: Option<Duration>,
189    /// Whether the path is currently open
190    pub is_open: bool,
191}
192
193impl PathInfo {
194    /// Create a new direct path info
195    pub fn direct(addr: SocketAddr) -> Self {
196        Self {
197            addr,
198            path_type: PathType::Direct,
199            rtt: None,
200            is_open: true,
201        }
202    }
203
204    /// Create a new relay path info
205    pub fn relay(addr: SocketAddr) -> Self {
206        Self {
207            addr,
208            path_type: PathType::Relay,
209            rtt: None,
210            is_open: true,
211        }
212    }
213
214    /// Create path info with RTT
215    pub fn with_rtt(mut self, rtt: Duration) -> Self {
216        self.rtt = Some(rtt);
217        self
218    }
219}
220
221/// Manager for tracking and closing redundant paths
222///
223/// Manages open paths and closes redundant ones when a best path is selected.
224/// Rules:
225/// 1. Never close relay paths (they're fallback)
226/// 2. Keep at least MIN_DIRECT_PATHS direct paths open
227/// 3. Never close the selected path
228#[derive(Debug, Default)]
229pub struct PathManager {
230    /// All tracked paths
231    paths: HashMap<SocketAddr, PathInfo>,
232    /// Currently selected best path
233    selected_path: Option<SocketAddr>,
234    /// Minimum number of direct paths to keep
235    min_direct_paths: usize,
236}
237
238impl PathManager {
239    /// Create a new path manager
240    pub fn new() -> Self {
241        Self {
242            paths: HashMap::new(),
243            selected_path: None,
244            min_direct_paths: MIN_DIRECT_PATHS,
245        }
246    }
247
248    /// Create a path manager with custom minimum direct paths
249    pub fn with_min_direct_paths(min_direct_paths: usize) -> Self {
250        Self {
251            paths: HashMap::new(),
252            selected_path: None,
253            min_direct_paths,
254        }
255    }
256
257    /// Add a path to track
258    pub fn add_path(&mut self, info: PathInfo) {
259        self.paths.insert(info.addr, info);
260    }
261
262    /// Remove a path
263    pub fn remove_path(&mut self, addr: &SocketAddr) {
264        self.paths.remove(addr);
265        if self.selected_path.as_ref() == Some(addr) {
266            self.selected_path = None;
267        }
268    }
269
270    /// Set the selected (best) path
271    pub fn set_selected_path(&mut self, addr: SocketAddr) {
272        self.selected_path = Some(addr);
273    }
274
275    /// Get the selected path
276    pub fn selected_path(&self) -> Option<SocketAddr> {
277        self.selected_path
278    }
279
280    /// Check if a path is tracked
281    pub fn contains(&self, addr: &SocketAddr) -> bool {
282        self.paths.contains_key(addr)
283    }
284
285    /// Check if a path is a relay path
286    pub fn is_relay_path(&self, addr: &SocketAddr) -> bool {
287        self.paths
288            .get(addr)
289            .map(|p| p.path_type == PathType::Relay)
290            .unwrap_or(false)
291    }
292
293    /// Count of open direct paths
294    pub fn direct_path_count(&self) -> usize {
295        self.paths
296            .values()
297            .filter(|p| p.path_type == PathType::Direct && p.is_open)
298            .count()
299    }
300
301    /// Count of open relay paths
302    pub fn relay_path_count(&self) -> usize {
303        self.paths
304            .values()
305            .filter(|p| p.path_type == PathType::Relay && p.is_open)
306            .count()
307    }
308
309    /// Get all open paths
310    pub fn open_paths(&self) -> Vec<&PathInfo> {
311        self.paths.values().filter(|p| p.is_open).collect()
312    }
313
314    /// Close redundant paths, returning list of closed addresses
315    ///
316    /// Rules:
317    /// 1. Only close direct paths (never relay - they're fallback)
318    /// 2. Don't close the selected path
319    /// 3. Keep at least min_direct_paths direct paths open
320    pub fn close_redundant_paths(&mut self) -> Vec<SocketAddr> {
321        let Some(selected) = self.selected_path else {
322            return Vec::new();
323        };
324
325        // Count open direct paths
326        let open_direct: Vec<_> = self
327            .paths
328            .iter()
329            .filter(|(_, p)| p.path_type == PathType::Direct && p.is_open)
330            .map(|(addr, _)| *addr)
331            .collect();
332
333        // Don't close if at or below minimum
334        if open_direct.len() <= self.min_direct_paths {
335            return Vec::new();
336        }
337
338        // Calculate how many we can close
339        let excess = open_direct.len() - self.min_direct_paths;
340
341        // Close excess direct paths (not selected)
342        let mut to_close = Vec::new();
343        for addr in open_direct {
344            if to_close.len() >= excess {
345                break;
346            }
347            if addr != selected {
348                to_close.push(addr);
349            }
350        }
351
352        // Mark as closed
353        for addr in &to_close {
354            if let Some(path) = self.paths.get_mut(addr) {
355                path.is_open = false;
356            }
357        }
358
359        tracing::debug!(
360            closed = to_close.len(),
361            remaining = self.direct_path_count(),
362            "Closed redundant paths"
363        );
364
365        to_close
366    }
367
368    /// Update RTT for a path
369    pub fn update_rtt(&mut self, addr: &SocketAddr, rtt: Duration) {
370        if let Some(path) = self.paths.get_mut(addr) {
371            path.rtt = Some(rtt);
372        }
373    }
374
375    /// Mark a path as open
376    pub fn mark_open(&mut self, addr: &SocketAddr) {
377        if let Some(path) = self.paths.get_mut(addr) {
378            path.is_open = true;
379        }
380    }
381
382    /// Mark a path as closed
383    pub fn mark_closed(&mut self, addr: &SocketAddr) {
384        if let Some(path) = self.paths.get_mut(addr) {
385            path.is_open = false;
386        }
387    }
388}
389
390#[cfg(test)]
391mod tests {
392    use super::*;
393    use std::net::{Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6};
394
395    fn v4_addr(port: u16) -> SocketAddr {
396        SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(192, 168, 1, 1), port))
397    }
398
399    fn v6_addr(port: u16) -> SocketAddr {
400        SocketAddr::V6(SocketAddrV6::new(
401            Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1),
402            port,
403            0,
404            0,
405        ))
406    }
407
408    #[test]
409    fn test_selects_lower_rtt_path() {
410        let paths = vec![
411            PathCandidate::new(v4_addr(5000), Duration::from_millis(50)),
412            PathCandidate::new(v4_addr(5001), Duration::from_millis(20)),
413            PathCandidate::new(v4_addr(5002), Duration::from_millis(100)),
414        ];
415
416        let selected = select_best_path(&paths, None);
417
418        assert_eq!(selected.as_ref().map(|p| p.addr.port()), Some(5001));
419    }
420
421    #[test]
422    fn test_hysteresis_prevents_flapping() {
423        let current = PathCandidate::new(v4_addr(5000), Duration::from_millis(50));
424
425        let paths = vec![
426            current.clone(),
427            // Only 2ms better - should NOT switch (needs 5ms improvement)
428            PathCandidate::new(v4_addr(5001), Duration::from_millis(48)),
429        ];
430
431        let selected = select_best_path(&paths, Some(&current));
432
433        // Should keep current path (hysteresis)
434        assert_eq!(selected.as_ref().map(|p| p.addr.port()), Some(5000));
435    }
436
437    #[test]
438    fn test_switches_when_significantly_better() {
439        let current = PathCandidate::new(v4_addr(5000), Duration::from_millis(50));
440
441        let paths = vec![
442            current.clone(),
443            // 10ms better - should switch (exceeds 5ms threshold)
444            PathCandidate::new(v4_addr(5001), Duration::from_millis(40)),
445        ];
446
447        let selected = select_best_path(&paths, Some(&current));
448
449        assert_eq!(selected.as_ref().map(|p| p.addr.port()), Some(5001));
450    }
451
452    #[test]
453    fn test_ipv6_preference() {
454        let paths = vec![
455            PathCandidate::new(v4_addr(5000), Duration::from_millis(50)),
456            // IPv6 with same RTT should win due to 3ms advantage
457            PathCandidate::new(v6_addr(5001), Duration::from_millis(50)),
458        ];
459
460        let selected = select_best_path(&paths, None);
461
462        assert!(selected.as_ref().map(|p| p.addr.is_ipv6()).unwrap_or(false));
463    }
464
465    #[test]
466    fn test_ipv6_advantage_applied_correctly() {
467        let paths = vec![
468            // IPv4 is 2ms faster, but IPv6 gets 3ms advantage
469            PathCandidate::new(v4_addr(5000), Duration::from_millis(48)),
470            PathCandidate::new(v6_addr(5001), Duration::from_millis(50)),
471        ];
472
473        let selected = select_best_path(&paths, None);
474
475        // IPv6 should win (50 - 3 = 47 effective RTT < 48)
476        assert!(selected.as_ref().map(|p| p.addr.is_ipv6()).unwrap_or(false));
477    }
478
479    #[test]
480    fn test_direct_preferred_over_relay() {
481        let paths = vec![
482            PathCandidate::direct(v4_addr(5000), Duration::from_millis(100)),
483            // Relay is faster but direct should be preferred
484            PathCandidate::relay(v4_addr(5001), Duration::from_millis(50)),
485        ];
486
487        let selected = select_best_path(&paths, None);
488
489        assert!(selected.as_ref().map(|p| p.is_direct()).unwrap_or(false));
490    }
491
492    #[test]
493    fn test_falls_back_to_relay_when_no_direct() {
494        let paths = vec![
495            PathCandidate::relay(v4_addr(5000), Duration::from_millis(100)),
496            PathCandidate::relay(v4_addr(5001), Duration::from_millis(50)),
497        ];
498
499        let selected = select_best_path(&paths, None);
500
501        // Should select faster relay
502        assert_eq!(selected.as_ref().map(|p| p.addr.port()), Some(5001));
503    }
504
505    #[test]
506    fn test_never_switches_from_direct_to_relay() {
507        let current = PathCandidate::direct(v4_addr(5000), Duration::from_millis(100));
508
509        let paths = vec![
510            current.clone(),
511            // Much faster relay should NOT cause switch
512            PathCandidate::relay(v4_addr(5001), Duration::from_millis(10)),
513        ];
514
515        let selected = select_best_path(&paths, Some(&current));
516
517        assert!(selected.as_ref().map(|p| p.is_direct()).unwrap_or(false));
518    }
519
520    #[test]
521    fn test_empty_paths_returns_none() {
522        let paths: Vec<PathCandidate> = vec![];
523        let selected = select_best_path(&paths, None);
524        assert!(selected.is_none());
525    }
526
527    #[test]
528    fn test_all_paths_same_rtt() {
529        let paths = vec![
530            PathCandidate::new(v4_addr(5000), Duration::from_millis(50)),
531            PathCandidate::new(v4_addr(5001), Duration::from_millis(50)),
532            PathCandidate::new(v4_addr(5002), Duration::from_millis(50)),
533        ];
534
535        // Should return one of them (first or deterministic choice)
536        let selected = select_best_path(&paths, None);
537        assert!(selected.is_some());
538    }
539
540    #[test]
541    fn test_select_v4_v6_prefers_faster() {
542        let (addr, rtt) = select_v4_v6(
543            v4_addr(5000),
544            Duration::from_millis(100),
545            v6_addr(5001),
546            Duration::from_millis(50),
547        );
548
549        // IPv6 is much faster, should be selected
550        assert!(addr.is_ipv6());
551        assert_eq!(rtt, Duration::from_millis(50));
552    }
553
554    #[test]
555    fn test_select_v4_v6_applies_ipv6_advantage() {
556        let (addr, _) = select_v4_v6(
557            v4_addr(5000),
558            Duration::from_millis(48),
559            v6_addr(5001),
560            Duration::from_millis(50),
561        );
562
563        // IPv6 effective RTT is 50-3=47 < 48, so IPv6 wins
564        assert!(addr.is_ipv6());
565    }
566
567    // ===== PathManager Tests =====
568
569    #[test]
570    fn test_path_manager_closes_redundant_direct_paths() {
571        let mut manager = PathManager::with_min_direct_paths(2);
572
573        // Add 5 direct paths
574        for port in 5000..5005 {
575            manager.add_path(PathInfo::direct(v4_addr(port)));
576        }
577
578        // Select one as best
579        manager.set_selected_path(v4_addr(5000));
580
581        // Close redundant paths
582        let closed = manager.close_redundant_paths();
583
584        // Should close 3 (5 - min 2 = 3 excess)
585        assert_eq!(closed.len(), 3);
586
587        // Selected path should NOT be closed
588        assert!(!closed.contains(&v4_addr(5000)));
589
590        // Should have exactly 2 open direct paths remaining
591        assert_eq!(manager.direct_path_count(), 2);
592    }
593
594    #[test]
595    fn test_path_manager_keeps_minimum_direct_paths() {
596        let mut manager = PathManager::with_min_direct_paths(2);
597
598        // Add exactly 2 direct paths
599        manager.add_path(PathInfo::direct(v4_addr(5000)));
600        manager.add_path(PathInfo::direct(v4_addr(5001)));
601
602        manager.set_selected_path(v4_addr(5000));
603
604        // Try to close redundant - should close none
605        let closed = manager.close_redundant_paths();
606        assert!(closed.is_empty());
607        assert_eq!(manager.direct_path_count(), 2);
608    }
609
610    #[test]
611    fn test_path_manager_never_closes_relay_paths() {
612        let mut manager = PathManager::with_min_direct_paths(1);
613
614        // Add direct and relay paths
615        manager.add_path(PathInfo::direct(v4_addr(5000)));
616        manager.add_path(PathInfo::direct(v4_addr(5001)));
617        manager.add_path(PathInfo::direct(v4_addr(5002)));
618        manager.add_path(PathInfo::relay(v4_addr(6000)));
619        manager.add_path(PathInfo::relay(v4_addr(6001)));
620
621        manager.set_selected_path(v4_addr(5000));
622
623        // Close redundant
624        let closed = manager.close_redundant_paths();
625
626        // Should only close direct paths, never relay
627        for addr in &closed {
628            assert!(!manager.is_relay_path(addr), "Closed a relay path!");
629        }
630
631        // Relay paths should still be open
632        assert_eq!(manager.relay_path_count(), 2);
633    }
634
635    #[test]
636    fn test_path_manager_does_not_close_selected_path() {
637        let mut manager = PathManager::with_min_direct_paths(1);
638
639        // Add 3 direct paths
640        manager.add_path(PathInfo::direct(v4_addr(5000)));
641        manager.add_path(PathInfo::direct(v4_addr(5001)));
642        manager.add_path(PathInfo::direct(v4_addr(5002)));
643
644        // Select the first one
645        manager.set_selected_path(v4_addr(5000));
646
647        let closed = manager.close_redundant_paths();
648
649        // Should have closed 2 paths (3 - min 1 = 2)
650        assert_eq!(closed.len(), 2);
651
652        // Selected path must NOT be in closed list
653        assert!(!closed.contains(&v4_addr(5000)));
654
655        // Selected path should still be tracked
656        assert!(manager.contains(&v4_addr(5000)));
657    }
658
659    #[test]
660    fn test_path_manager_no_close_without_selected() {
661        let mut manager = PathManager::with_min_direct_paths(1);
662
663        // Add paths but don't select any
664        manager.add_path(PathInfo::direct(v4_addr(5000)));
665        manager.add_path(PathInfo::direct(v4_addr(5001)));
666        manager.add_path(PathInfo::direct(v4_addr(5002)));
667
668        // Without a selected path, should not close anything
669        let closed = manager.close_redundant_paths();
670        assert!(closed.is_empty());
671    }
672
673    #[test]
674    fn test_path_manager_add_and_remove() {
675        let mut manager = PathManager::new();
676
677        let addr = v4_addr(5000);
678        manager.add_path(PathInfo::direct(addr));
679        assert!(manager.contains(&addr));
680
681        manager.remove_path(&addr);
682        assert!(!manager.contains(&addr));
683    }
684
685    #[test]
686    fn test_path_manager_update_rtt() {
687        let mut manager = PathManager::new();
688
689        let addr = v4_addr(5000);
690        manager.add_path(PathInfo::direct(addr));
691
692        manager.update_rtt(&addr, Duration::from_millis(50));
693
694        let paths = manager.open_paths();
695        assert_eq!(paths.len(), 1);
696        assert_eq!(paths[0].rtt, Some(Duration::from_millis(50)));
697    }
698
699    #[test]
700    fn test_path_manager_mark_open_closed() {
701        let mut manager = PathManager::new();
702
703        let addr = v4_addr(5000);
704        manager.add_path(PathInfo::direct(addr));
705
706        assert_eq!(manager.direct_path_count(), 1);
707
708        manager.mark_closed(&addr);
709        assert_eq!(manager.direct_path_count(), 0);
710
711        manager.mark_open(&addr);
712        assert_eq!(manager.direct_path_count(), 1);
713    }
714
715    #[test]
716    fn test_path_manager_selected_path_cleared_on_remove() {
717        let mut manager = PathManager::new();
718
719        let addr = v4_addr(5000);
720        manager.add_path(PathInfo::direct(addr));
721        manager.set_selected_path(addr);
722
723        assert_eq!(manager.selected_path(), Some(addr));
724
725        manager.remove_path(&addr);
726        assert_eq!(manager.selected_path(), None);
727    }
728}