Skip to main content

ant_quic/bootstrap_cache/
entry.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//! Cached peer entry types.
9
10use crate::nat_traversal_api::PeerId;
11use serde::{Deserialize, Serialize};
12use std::collections::HashSet;
13use std::net::SocketAddr;
14use std::time::{Duration, SystemTime};
15
16/// A cached peer entry with quality metrics
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct CachedPeer {
19    /// Unique peer identifier (serialized as bytes)
20    #[serde(with = "peer_id_serde")]
21    pub peer_id: PeerId,
22
23    /// Known socket addresses for this peer
24    pub addresses: Vec<SocketAddr>,
25
26    /// Peer capabilities and features
27    pub capabilities: PeerCapabilities,
28
29    /// When we first discovered this peer
30    pub first_seen: SystemTime,
31
32    /// When we last successfully communicated with this peer
33    pub last_seen: SystemTime,
34
35    /// When we last attempted to connect (success or failure)
36    pub last_attempt: Option<SystemTime>,
37
38    /// Connection statistics
39    pub stats: ConnectionStats,
40
41    /// Computed quality score (0.0 to 1.0)
42    #[serde(default = "default_quality_score")]
43    pub quality_score: f64,
44
45    /// Source that added this peer
46    pub source: PeerSource,
47
48    /// Known relay paths for reaching this peer when direct connection fails
49    #[serde(default)]
50    pub relay_paths: Vec<RelayPathHint>,
51
52    /// Persistent QUIC address validation token
53    #[serde(default)]
54    pub token: Option<Vec<u8>>,
55}
56
57fn default_quality_score() -> f64 {
58    0.5
59}
60
61/// Peer capabilities and features
62#[derive(Debug, Clone, Default, Serialize, Deserialize)]
63pub struct PeerCapabilities {
64    /// Peer supports relay traffic (observed, not self-asserted)
65    pub supports_relay: bool,
66
67    /// Peer supports NAT traversal coordination (observed, not self-asserted)
68    pub supports_coordination: bool,
69
70    /// Protocol identifiers advertised by this peer (as hex strings for serialization)
71    #[serde(default)]
72    pub protocols: HashSet<String>,
73
74    /// Observed NAT type hint
75    pub nat_type: Option<NatType>,
76
77    /// External addresses reported by peer
78    #[serde(default)]
79    pub external_addresses: Vec<SocketAddr>,
80}
81
82impl PeerCapabilities {
83    /// Check if this peer has any IPv4 addresses
84    pub fn has_ipv4(&self) -> bool {
85        self.external_addresses.iter().any(|addr| addr.is_ipv4())
86    }
87
88    /// Check if this peer has any IPv6 addresses
89    pub fn has_ipv6(&self) -> bool {
90        self.external_addresses.iter().any(|addr| addr.is_ipv6())
91    }
92
93    /// Check if this peer supports dual-stack (both IPv4 and IPv6)
94    ///
95    /// A dual-stack peer can bridge traffic between IPv4 and IPv6 networks
96    /// when acting as a relay.
97    pub fn supports_dual_stack(&self) -> bool {
98        self.has_ipv4() && self.has_ipv6()
99    }
100
101    /// Get addresses filtered by IP version
102    pub fn addresses_by_version(&self, ipv4: bool) -> Vec<SocketAddr> {
103        self.external_addresses
104            .iter()
105            .filter(|addr| addr.is_ipv4() == ipv4)
106            .copied()
107            .collect()
108    }
109
110    /// Check if this peer can bridge between source and target IP versions
111    pub fn can_bridge(&self, source: &SocketAddr, target: &SocketAddr) -> bool {
112        let source_v4 = source.is_ipv4();
113        let target_v4 = target.is_ipv4();
114
115        // Same version - any peer can handle
116        if source_v4 == target_v4 {
117            return true;
118        }
119
120        // Different versions - need dual-stack
121        self.supports_dual_stack()
122    }
123}
124
125/// NAT type classification
126#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
127pub enum NatType {
128    /// No NAT (public IP)
129    None,
130    /// Full cone NAT (easiest to traverse)
131    FullCone,
132    /// Address-restricted cone NAT
133    AddressRestrictedCone,
134    /// Port-restricted cone NAT
135    PortRestrictedCone,
136    /// Symmetric NAT (hardest to traverse)
137    Symmetric,
138    /// Unknown NAT type
139    Unknown,
140}
141
142/// Connection statistics for quality scoring
143#[derive(Debug, Clone, Default, Serialize, Deserialize)]
144pub struct ConnectionStats {
145    /// Total successful connections
146    pub success_count: u32,
147
148    /// Total failed connection attempts
149    pub failure_count: u32,
150
151    /// Exponential moving average RTT in milliseconds
152    pub avg_rtt_ms: u32,
153
154    /// Minimum observed RTT
155    pub min_rtt_ms: u32,
156
157    /// Maximum observed RTT
158    pub max_rtt_ms: u32,
159
160    /// Total bytes relayed through this peer (if relay)
161    pub bytes_relayed: u64,
162
163    /// Number of NAT traversals coordinated (if coordinator)
164    pub coordinations_completed: u32,
165}
166
167/// How we discovered this peer
168#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
169pub enum PeerSource {
170    /// User-provided bootstrap seed
171    Seed,
172    /// Discovered via active connection
173    Connection,
174    /// Discovered via relay traffic
175    Relay,
176    /// Discovered via NAT coordination
177    Coordination,
178    /// Merged from another cache instance
179    Merge,
180    /// Unknown source (legacy entries)
181    #[default]
182    Unknown,
183}
184
185/// Result of a connection attempt
186#[derive(Debug, Clone)]
187pub struct ConnectionOutcome {
188    /// Whether the connection succeeded
189    pub success: bool,
190    /// RTT in milliseconds if available
191    pub rtt_ms: Option<u32>,
192    /// Capabilities discovered during connection
193    pub capabilities_discovered: Option<PeerCapabilities>,
194}
195
196/// A relay path hint for reaching a peer through an intermediary
197///
198/// When direct connections fail, relay paths provide alternative routes.
199/// This tracks known relays that can reach a given peer.
200#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct RelayPathHint {
202    /// EndpointId of the relay peer
203    #[serde(with = "peer_id_serde")]
204    pub relay_endpoint_id: PeerId,
205
206    /// Known socket addresses for the relay
207    pub relay_locators: Vec<SocketAddr>,
208
209    /// Observed round-trip latency through this relay in milliseconds
210    pub observed_latency_ms: Option<u32>,
211
212    /// When this relay path was last successfully used
213    pub last_used: SystemTime,
214}
215
216impl CachedPeer {
217    /// Create a new peer entry
218    pub fn new(peer_id: PeerId, addresses: Vec<SocketAddr>, source: PeerSource) -> Self {
219        let now = SystemTime::now();
220        Self {
221            peer_id,
222            addresses,
223            capabilities: PeerCapabilities::default(),
224            first_seen: now,
225            last_seen: now,
226            last_attempt: None,
227            stats: ConnectionStats::default(),
228            quality_score: 0.5, // Neutral starting score
229            source,
230            relay_paths: Vec::new(),
231            token: None,
232        }
233    }
234
235    /// Record a successful connection
236    pub fn record_success(&mut self, rtt_ms: u32, caps: Option<PeerCapabilities>) {
237        self.last_seen = SystemTime::now();
238        self.last_attempt = Some(SystemTime::now());
239        self.stats.success_count = self.stats.success_count.saturating_add(1);
240
241        // Update RTT with exponential moving average (alpha = 0.125)
242        if self.stats.avg_rtt_ms == 0 {
243            self.stats.avg_rtt_ms = rtt_ms;
244            self.stats.min_rtt_ms = rtt_ms;
245            self.stats.max_rtt_ms = rtt_ms;
246        } else {
247            self.stats.avg_rtt_ms = (self.stats.avg_rtt_ms * 7 + rtt_ms) / 8;
248            self.stats.min_rtt_ms = self.stats.min_rtt_ms.min(rtt_ms);
249            self.stats.max_rtt_ms = self.stats.max_rtt_ms.max(rtt_ms);
250        }
251
252        if let Some(caps) = caps {
253            self.capabilities = caps;
254        }
255    }
256
257    /// Record a failed connection attempt
258    pub fn record_failure(&mut self) {
259        self.last_attempt = Some(SystemTime::now());
260        self.stats.failure_count = self.stats.failure_count.saturating_add(1);
261    }
262
263    /// Calculate quality score based on metrics
264    pub fn calculate_quality(&mut self, weights: &super::config::QualityWeights) {
265        let total_attempts = self.stats.success_count + self.stats.failure_count;
266
267        // Success rate component (0.0 to 1.0)
268        let success_rate = if total_attempts > 0 {
269            self.stats.success_count as f64 / total_attempts as f64
270        } else {
271            0.5 // Neutral for untested peers
272        };
273
274        // RTT component (lower is better, normalized to 0.0-1.0)
275        // 50ms = 1.0, 500ms = 0.5, 1000ms+ = 0.0
276        let rtt_score = if self.stats.avg_rtt_ms > 0 {
277            1.0 - (self.stats.avg_rtt_ms as f64 / 1000.0).min(1.0)
278        } else {
279            0.5 // Neutral for unknown RTT
280        };
281
282        // Freshness component (exponential decay with 24-hour half-life)
283        let age_secs = self
284            .last_seen
285            .duration_since(SystemTime::UNIX_EPOCH)
286            .ok()
287            .and_then(|last_seen_epoch| {
288                SystemTime::now()
289                    .duration_since(SystemTime::UNIX_EPOCH)
290                    .ok()
291                    .map(|now_epoch| {
292                        now_epoch
293                            .as_secs()
294                            .saturating_sub(last_seen_epoch.as_secs())
295                    })
296            })
297            .unwrap_or(0) as f64;
298
299        // Half-life of 24 hours = decay constant ln(2)/86400
300        let freshness = (-age_secs * 0.693 / 86400.0).exp();
301
302        // Capability bonuses
303        let mut cap_bonus: f64 = 0.0;
304        if self.capabilities.supports_relay {
305            cap_bonus += 0.25;
306        }
307        if self.capabilities.supports_coordination {
308            cap_bonus += 0.25;
309        }
310        if self.capabilities.supports_dual_stack() {
311            cap_bonus += 0.2; // Dual-stack relays are valuable for bridging
312        }
313        if matches!(
314            self.capabilities.nat_type,
315            Some(NatType::None) | Some(NatType::FullCone)
316        ) {
317            cap_bonus += 0.3; // Easy to connect
318        }
319        let cap_score = cap_bonus.min(1.0);
320
321        // Weighted combination
322        self.quality_score = (success_rate * weights.success_rate
323            + rtt_score * weights.rtt
324            + freshness * weights.freshness
325            + cap_score * weights.capabilities)
326            .clamp(0.0, 1.0);
327    }
328
329    /// Check if this peer is stale
330    pub fn is_stale(&self, threshold: Duration) -> bool {
331        self.last_seen
332            .elapsed()
333            .map(|age| age > threshold)
334            .unwrap_or(true)
335    }
336
337    /// Get success rate
338    pub fn success_rate(&self) -> f64 {
339        let total = self.stats.success_count + self.stats.failure_count;
340        if total == 0 {
341            0.5
342        } else {
343            self.stats.success_count as f64 / total as f64
344        }
345    }
346
347    /// Merge addresses from another peer entry
348    pub fn merge_addresses(&mut self, other: &CachedPeer) {
349        for addr in &other.addresses {
350            if !self.addresses.contains(addr) {
351                self.addresses.push(*addr);
352            }
353        }
354        // Keep reasonable limit
355        if self.addresses.len() > 10 {
356            self.addresses.truncate(10);
357        }
358    }
359}
360
361/// Serde helper for PeerId serialization
362mod peer_id_serde {
363    use super::PeerId;
364    use serde::{Deserialize, Deserializer, Serialize, Serializer};
365
366    pub fn serialize<S>(peer_id: &PeerId, serializer: S) -> Result<S::Ok, S::Error>
367    where
368        S: Serializer,
369    {
370        hex::encode(peer_id.0).serialize(serializer)
371    }
372
373    pub fn deserialize<'de, D>(deserializer: D) -> Result<PeerId, D::Error>
374    where
375        D: Deserializer<'de>,
376    {
377        let s = String::deserialize(deserializer)?;
378        let bytes = hex::decode(&s).map_err(serde::de::Error::custom)?;
379        if bytes.len() != 32 {
380            return Err(serde::de::Error::custom("PeerId must be 32 bytes"));
381        }
382        let mut arr = [0u8; 32];
383        arr.copy_from_slice(&bytes);
384        Ok(PeerId(arr))
385    }
386}
387
388#[cfg(test)]
389mod tests {
390    use super::*;
391
392    #[test]
393    fn test_cached_peer_new() {
394        let peer_id = PeerId([1u8; 32]);
395        let peer = CachedPeer::new(
396            peer_id,
397            vec!["127.0.0.1:9000".parse().unwrap()],
398            PeerSource::Seed,
399        );
400
401        assert_eq!(peer.peer_id, peer_id);
402        assert_eq!(peer.addresses.len(), 1);
403        assert_eq!(peer.source, PeerSource::Seed);
404        assert!((peer.quality_score - 0.5).abs() < f64::EPSILON);
405    }
406
407    #[test]
408    fn test_record_success() {
409        let mut peer = CachedPeer::new(
410            PeerId([1u8; 32]),
411            vec!["127.0.0.1:9000".parse().unwrap()],
412            PeerSource::Seed,
413        );
414
415        peer.record_success(100, None);
416        assert_eq!(peer.stats.success_count, 1);
417        assert_eq!(peer.stats.avg_rtt_ms, 100);
418        assert_eq!(peer.stats.min_rtt_ms, 100);
419        assert_eq!(peer.stats.max_rtt_ms, 100);
420
421        peer.record_success(200, None);
422        assert_eq!(peer.stats.success_count, 2);
423        // EMA: (100*7 + 200) / 8 = 112
424        assert_eq!(peer.stats.avg_rtt_ms, 112);
425        assert_eq!(peer.stats.min_rtt_ms, 100);
426        assert_eq!(peer.stats.max_rtt_ms, 200);
427    }
428
429    #[test]
430    fn test_record_failure() {
431        let mut peer = CachedPeer::new(
432            PeerId([1u8; 32]),
433            vec!["127.0.0.1:9000".parse().unwrap()],
434            PeerSource::Seed,
435        );
436
437        peer.record_failure();
438        assert_eq!(peer.stats.failure_count, 1);
439        assert!(peer.last_attempt.is_some());
440    }
441
442    #[test]
443    fn test_success_rate() {
444        let mut peer = CachedPeer::new(
445            PeerId([1u8; 32]),
446            vec!["127.0.0.1:9000".parse().unwrap()],
447            PeerSource::Seed,
448        );
449
450        // No attempts = 0.5
451        assert!((peer.success_rate() - 0.5).abs() < f64::EPSILON);
452
453        peer.record_success(100, None);
454        assert!((peer.success_rate() - 1.0).abs() < f64::EPSILON);
455
456        peer.record_failure();
457        assert!((peer.success_rate() - 0.5).abs() < f64::EPSILON);
458    }
459
460    #[test]
461    fn test_quality_calculation() {
462        let weights = super::super::config::QualityWeights::default();
463        let mut peer = CachedPeer::new(
464            PeerId([1u8; 32]),
465            vec!["127.0.0.1:9000".parse().unwrap()],
466            PeerSource::Seed,
467        );
468
469        // Initial quality should be moderate (untested peer)
470        peer.calculate_quality(&weights);
471        assert!(peer.quality_score > 0.3 && peer.quality_score < 0.7);
472
473        // Good performance should increase quality
474        for _ in 0..5 {
475            peer.record_success(50, None); // Low RTT
476        }
477        peer.calculate_quality(&weights);
478        assert!(peer.quality_score > 0.6);
479    }
480
481    #[test]
482    fn test_peer_serialization() {
483        let peer = CachedPeer::new(
484            PeerId([0xab; 32]),
485            vec!["127.0.0.1:9000".parse().unwrap()],
486            PeerSource::Seed,
487        );
488
489        let json = serde_json::to_string(&peer).unwrap();
490        let deserialized: CachedPeer = serde_json::from_str(&json).unwrap();
491
492        assert_eq!(deserialized.peer_id, peer.peer_id);
493        assert_eq!(deserialized.addresses, peer.addresses);
494        assert_eq!(deserialized.source, peer.source);
495    }
496
497    #[test]
498    fn test_peer_capabilities_dual_stack() {
499        let mut caps = PeerCapabilities::default();
500
501        // Default - no addresses
502        assert!(!caps.supports_dual_stack());
503        assert!(!caps.has_ipv4());
504        assert!(!caps.has_ipv6());
505
506        // Add IPv4 only
507        caps.external_addresses
508            .push("127.0.0.1:9000".parse().unwrap());
509        assert!(!caps.supports_dual_stack());
510        assert!(caps.has_ipv4());
511        assert!(!caps.has_ipv6());
512
513        // Add IPv6 - now dual-stack
514        caps.external_addresses.push("[::1]:9001".parse().unwrap());
515        assert!(caps.supports_dual_stack());
516        assert!(caps.has_ipv4());
517        assert!(caps.has_ipv6());
518    }
519
520    #[test]
521    fn test_peer_capabilities_ipv6_only() {
522        let mut caps = PeerCapabilities::default();
523        caps.external_addresses.push("[::1]:9000".parse().unwrap());
524        caps.external_addresses.push("[::1]:9001".parse().unwrap());
525
526        assert!(!caps.supports_dual_stack());
527        assert!(!caps.has_ipv4());
528        assert!(caps.has_ipv6());
529    }
530
531    #[test]
532    fn test_peer_capabilities_can_bridge() {
533        let mut caps = PeerCapabilities::default();
534        caps.external_addresses
535            .push("127.0.0.1:9000".parse().unwrap());
536        caps.external_addresses.push("[::1]:9001".parse().unwrap());
537
538        let v4_src: SocketAddr = "192.168.1.1:1000".parse().unwrap();
539        let v4_dst: SocketAddr = "192.168.1.2:2000".parse().unwrap();
540        let v6_src: SocketAddr = "[2001:db8::1]:1000".parse().unwrap();
541        let v6_dst: SocketAddr = "[2001:db8::2]:2000".parse().unwrap();
542
543        // Same version - always OK
544        assert!(caps.can_bridge(&v4_src, &v4_dst));
545        assert!(caps.can_bridge(&v6_src, &v6_dst));
546
547        // Cross version - OK for dual-stack
548        assert!(caps.can_bridge(&v4_src, &v6_dst));
549        assert!(caps.can_bridge(&v6_src, &v4_dst));
550    }
551
552    #[test]
553    fn test_peer_capabilities_cannot_bridge_ipv4_only() {
554        let mut caps = PeerCapabilities::default();
555        caps.external_addresses
556            .push("127.0.0.1:9000".parse().unwrap());
557
558        let v4_addr: SocketAddr = "192.168.1.1:1000".parse().unwrap();
559        let v6_addr: SocketAddr = "[2001:db8::1]:1000".parse().unwrap();
560
561        // Same version - OK
562        assert!(caps.can_bridge(&v4_addr, &v4_addr));
563
564        // Cross version - NOT OK for IPv4-only
565        assert!(!caps.can_bridge(&v4_addr, &v6_addr));
566        assert!(!caps.can_bridge(&v6_addr, &v4_addr));
567    }
568
569    #[test]
570    fn test_addresses_by_version() {
571        let mut caps = PeerCapabilities::default();
572        caps.external_addresses
573            .push("127.0.0.1:9000".parse().unwrap());
574        caps.external_addresses
575            .push("10.0.0.1:9001".parse().unwrap());
576        caps.external_addresses.push("[::1]:9002".parse().unwrap());
577
578        let v4_addrs = caps.addresses_by_version(true);
579        assert_eq!(v4_addrs.len(), 2);
580
581        let v6_addrs = caps.addresses_by_version(false);
582        assert_eq!(v6_addrs.len(), 1);
583    }
584}