1use std::collections::HashMap;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum NatType {
11 Open,
13 FullCone,
15 RestrictedCone,
17 PortRestricted,
19 Symmetric,
21 Unknown,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq)]
27pub enum TraversalStrategy {
28 Direct,
30 HolePunch,
32 PortPrediction,
34 Relay,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq)]
40pub enum HolePunchStatus {
41 Pending,
43 InProgress,
45 Success,
47 Failed,
49 TimedOut,
51}
52
53#[derive(Debug, Clone)]
55pub struct StunBinding {
56 pub local_addr: String,
58 pub mapped_addr: String,
60 pub nat_type: NatType,
62 pub latency_ms: u64,
64 pub timestamp: u64,
66}
67
68#[derive(Debug, Clone)]
70pub struct HolePunchAttempt {
71 pub peer_id: String,
73 pub local_port: u16,
75 pub remote_addr: String,
77 pub attempts: u32,
79 pub max_attempts: u32,
81 pub status: HolePunchStatus,
83 pub started_at: u64,
85}
86
87#[derive(Debug, Clone)]
89pub struct NatTraversalConfig {
90 pub stun_timeout_ms: u64,
92 pub max_punch_attempts: u32,
94 pub punch_interval_ms: u64,
96 pub port_prediction_range: u16,
98 pub enable_upnp: bool,
100 pub enable_relay_fallback: bool,
102}
103
104impl Default for NatTraversalConfig {
105 fn default() -> Self {
106 Self {
107 stun_timeout_ms: 3000,
108 max_punch_attempts: 5,
109 punch_interval_ms: 500,
110 port_prediction_range: 10,
111 enable_upnp: true,
112 enable_relay_fallback: true,
113 }
114 }
115}
116
117#[derive(Debug, Clone, Default)]
119pub struct NatTraversalStats {
120 pub total_attempts: u64,
122 pub successful: u64,
124 pub failed: u64,
126 pub timed_out: u64,
128 pub avg_punch_time_ms: f64,
130}
131
132pub struct NatTraversalManager {
134 config: NatTraversalConfig,
135 bindings: Vec<StunBinding>,
136 active_punches: HashMap<String, HolePunchAttempt>,
137 stats: NatTraversalStats,
138 detected_nat_type: NatType,
139}
140
141impl NatTraversalManager {
142 pub fn new(config: NatTraversalConfig) -> Self {
144 Self {
145 config,
146 bindings: Vec::new(),
147 active_punches: HashMap::new(),
148 stats: NatTraversalStats::default(),
149 detected_nat_type: NatType::Unknown,
150 }
151 }
152
153 pub fn detect_nat_type(bindings: &[StunBinding]) -> NatType {
161 if bindings.is_empty() {
162 return NatType::Unknown;
163 }
164
165 if bindings.len() == 1 {
166 let b = &bindings[0];
167 if b.local_addr == b.mapped_addr {
168 return NatType::Open;
169 }
170 return NatType::RestrictedCone;
172 }
173
174 let mapped_parts: Vec<(&str, &str)> = bindings
176 .iter()
177 .filter_map(|b| {
178 let parts: Vec<&str> = b.mapped_addr.rsplitn(2, ':').collect();
179 if parts.len() == 2 {
180 Some((parts[1], parts[0]))
181 } else {
182 None
183 }
184 })
185 .collect();
186
187 if mapped_parts.is_empty() {
188 return NatType::Unknown;
189 }
190
191 let all_open = bindings.iter().all(|b| b.local_addr == b.mapped_addr);
193 if all_open {
194 return NatType::Open;
195 }
196
197 let first_ip = mapped_parts[0].0;
198 let first_port = mapped_parts[0].1;
199
200 let all_same_ip = mapped_parts.iter().all(|(ip, _)| *ip == first_ip);
201 let all_same_port = mapped_parts.iter().all(|(_, port)| *port == first_port);
202
203 if all_same_ip && all_same_port {
204 NatType::FullCone
206 } else if all_same_ip && !all_same_port {
207 NatType::PortRestricted
209 } else {
210 NatType::Symmetric
212 }
213 }
214
215 pub fn add_binding(&mut self, binding: StunBinding) {
217 self.bindings.push(binding);
218 self.detected_nat_type = Self::detect_nat_type(&self.bindings);
219 }
220
221 pub fn initiate_punch(
225 &mut self,
226 peer_id: &str,
227 remote_addr: &str,
228 local_port: u16,
229 ) -> Result<(), String> {
230 if self.active_punches.contains_key(peer_id) {
231 return Err(format!("Hole-punch already active for peer {}", peer_id));
232 }
233
234 let attempt = HolePunchAttempt {
235 peer_id: peer_id.to_string(),
236 local_port,
237 remote_addr: remote_addr.to_string(),
238 attempts: 0,
239 max_attempts: self.config.max_punch_attempts,
240 status: HolePunchStatus::Pending,
241 started_at: current_timestamp(),
242 };
243
244 self.active_punches.insert(peer_id.to_string(), attempt);
245 self.stats.total_attempts += 1;
246
247 Ok(())
248 }
249
250 pub fn predict_port(&self, base_port: u16, sample_ports: &[u16]) -> Vec<u16> {
256 let range = self.config.port_prediction_range;
257
258 if sample_ports.len() >= 2 {
259 let deltas: Vec<i32> = sample_ports
261 .windows(2)
262 .map(|w| i32::from(w[1]) - i32::from(w[0]))
263 .collect();
264
265 let first_delta = deltas[0];
267 let is_linear = deltas.iter().all(|d| *d == first_delta);
268
269 if is_linear && first_delta != 0 {
270 let last = i32::from(*sample_ports.last().unwrap_or(&base_port));
271 let predicted = last + first_delta;
272 let center = predicted.clamp(0, 65535) as u16;
273 return port_range(center, range);
274 }
275 }
276
277 port_range(base_port, range)
279 }
280
281 pub fn update_attempt(&mut self, peer_id: &str, status: HolePunchStatus) -> Result<(), String> {
283 let attempt = self
284 .active_punches
285 .get_mut(peer_id)
286 .ok_or_else(|| format!("No active punch for peer {}", peer_id))?;
287
288 match &status {
289 HolePunchStatus::InProgress => {
290 attempt.attempts += 1;
291 if attempt.attempts > attempt.max_attempts {
292 attempt.status = HolePunchStatus::Failed;
293 self.stats.failed += 1;
294 return Ok(());
295 }
296 }
297 HolePunchStatus::Success => {
298 self.stats.successful += 1;
299 let elapsed = current_timestamp().saturating_sub(attempt.started_at);
300 let n = self.stats.successful as f64;
302 self.stats.avg_punch_time_ms =
303 self.stats.avg_punch_time_ms * ((n - 1.0) / n) + (elapsed as f64) / n;
304 }
305 HolePunchStatus::Failed => {
306 self.stats.failed += 1;
307 }
308 HolePunchStatus::TimedOut => {
309 self.stats.timed_out += 1;
310 }
311 HolePunchStatus::Pending => {}
312 }
313
314 attempt.status = status;
315 Ok(())
316 }
317
318 pub fn get_attempt(&self, peer_id: &str) -> Option<&HolePunchAttempt> {
320 self.active_punches.get(peer_id)
321 }
322
323 pub fn active_punch_count(&self) -> usize {
325 self.active_punches
326 .values()
327 .filter(|a| {
328 matches!(
329 a.status,
330 HolePunchStatus::Pending | HolePunchStatus::InProgress
331 )
332 })
333 .count()
334 }
335
336 pub fn cleanup_expired(&mut self, now: u64) -> usize {
339 let timeout = self.config.stun_timeout_ms;
340 let before = self.active_punches.len();
341
342 self.active_punches.retain(|_k, v| {
343 let age = now.saturating_sub(v.started_at);
344 let is_terminal = matches!(
345 v.status,
346 HolePunchStatus::Success | HolePunchStatus::Failed | HolePunchStatus::TimedOut
347 );
348 age < timeout || !is_terminal
350 });
351
352 before - self.active_punches.len()
353 }
354
355 pub fn success_rate(&self) -> f64 {
357 if self.stats.total_attempts == 0 {
358 return 0.0;
359 }
360 self.stats.successful as f64 / self.stats.total_attempts as f64
361 }
362
363 pub fn stats(&self) -> &NatTraversalStats {
365 &self.stats
366 }
367
368 pub fn detected_nat_type(&self) -> &NatType {
370 &self.detected_nat_type
371 }
372
373 pub fn best_strategy(&self) -> TraversalStrategy {
375 match &self.detected_nat_type {
376 NatType::Open => TraversalStrategy::Direct,
377 NatType::FullCone | NatType::RestrictedCone => TraversalStrategy::HolePunch,
378 NatType::PortRestricted => TraversalStrategy::PortPrediction,
379 NatType::Symmetric | NatType::Unknown => {
380 if self.config.enable_relay_fallback {
381 TraversalStrategy::Relay
382 } else {
383 TraversalStrategy::HolePunch
384 }
385 }
386 }
387 }
388}
389
390fn port_range(center: u16, half_range: u16) -> Vec<u16> {
396 let low = center.saturating_sub(half_range);
397 let high = center.saturating_add(half_range);
398 (low..=high).collect()
399}
400
401fn current_timestamp() -> u64 {
404 use std::time::{SystemTime, UNIX_EPOCH};
405 SystemTime::now()
406 .duration_since(UNIX_EPOCH)
407 .unwrap_or_default()
408 .as_millis() as u64
409}
410
411#[cfg(test)]
416mod tests {
417 use super::*;
418
419 fn default_config() -> NatTraversalConfig {
420 NatTraversalConfig::default()
421 }
422
423 fn make_binding(local: &str, mapped: &str) -> StunBinding {
424 StunBinding {
425 local_addr: local.to_string(),
426 mapped_addr: mapped.to_string(),
427 nat_type: NatType::Unknown,
428 latency_ms: 10,
429 timestamp: 1000,
430 }
431 }
432
433 #[test]
436 fn config_default_values() {
437 let c = NatTraversalConfig::default();
438 assert_eq!(c.stun_timeout_ms, 3000);
439 assert_eq!(c.max_punch_attempts, 5);
440 assert_eq!(c.punch_interval_ms, 500);
441 assert_eq!(c.port_prediction_range, 10);
442 assert!(c.enable_upnp);
443 assert!(c.enable_relay_fallback);
444 }
445
446 #[test]
447 fn config_custom_values() {
448 let c = NatTraversalConfig {
449 stun_timeout_ms: 5000,
450 max_punch_attempts: 10,
451 punch_interval_ms: 200,
452 port_prediction_range: 20,
453 enable_upnp: false,
454 enable_relay_fallback: false,
455 };
456 assert_eq!(c.stun_timeout_ms, 5000);
457 assert!(!c.enable_upnp);
458 }
459
460 #[test]
463 fn stun_binding_fields() {
464 let b = StunBinding {
465 local_addr: "192.168.1.1:5000".into(),
466 mapped_addr: "1.2.3.4:6000".into(),
467 nat_type: NatType::FullCone,
468 latency_ms: 42,
469 timestamp: 999,
470 };
471 assert_eq!(b.local_addr, "192.168.1.1:5000");
472 assert_eq!(b.mapped_addr, "1.2.3.4:6000");
473 assert_eq!(b.nat_type, NatType::FullCone);
474 assert_eq!(b.latency_ms, 42);
475 assert_eq!(b.timestamp, 999);
476 }
477
478 #[test]
479 fn stun_binding_clone() {
480 let b = make_binding("10.0.0.1:80", "1.2.3.4:80");
481 let b2 = b.clone();
482 assert_eq!(b.local_addr, b2.local_addr);
483 assert_eq!(b.mapped_addr, b2.mapped_addr);
484 }
485
486 #[test]
489 fn detect_nat_type_empty() {
490 assert_eq!(NatTraversalManager::detect_nat_type(&[]), NatType::Unknown);
491 }
492
493 #[test]
494 fn detect_nat_type_open_single() {
495 let bindings = [make_binding("1.2.3.4:5000", "1.2.3.4:5000")];
496 assert_eq!(
497 NatTraversalManager::detect_nat_type(&bindings),
498 NatType::Open
499 );
500 }
501
502 #[test]
503 fn detect_nat_type_open_multiple() {
504 let bindings = [
505 make_binding("1.2.3.4:5000", "1.2.3.4:5000"),
506 make_binding("1.2.3.4:5001", "1.2.3.4:5001"),
507 ];
508 assert_eq!(
509 NatTraversalManager::detect_nat_type(&bindings),
510 NatType::Open
511 );
512 }
513
514 #[test]
515 fn detect_nat_type_full_cone() {
516 let bindings = [
518 make_binding("192.168.1.1:5000", "1.2.3.4:6000"),
519 make_binding("192.168.1.1:5001", "1.2.3.4:6000"),
520 ];
521 assert_eq!(
522 NatTraversalManager::detect_nat_type(&bindings),
523 NatType::FullCone
524 );
525 }
526
527 #[test]
528 fn detect_nat_type_port_restricted() {
529 let bindings = [
531 make_binding("192.168.1.1:5000", "1.2.3.4:6000"),
532 make_binding("192.168.1.1:5001", "1.2.3.4:6001"),
533 ];
534 assert_eq!(
535 NatTraversalManager::detect_nat_type(&bindings),
536 NatType::PortRestricted
537 );
538 }
539
540 #[test]
541 fn detect_nat_type_symmetric() {
542 let bindings = [
544 make_binding("192.168.1.1:5000", "1.2.3.4:6000"),
545 make_binding("192.168.1.1:5001", "5.6.7.8:6001"),
546 ];
547 assert_eq!(
548 NatTraversalManager::detect_nat_type(&bindings),
549 NatType::Symmetric
550 );
551 }
552
553 #[test]
554 fn detect_nat_type_restricted_cone_single_behind_nat() {
555 let bindings = [make_binding("192.168.1.1:5000", "1.2.3.4:6000")];
557 assert_eq!(
558 NatTraversalManager::detect_nat_type(&bindings),
559 NatType::RestrictedCone
560 );
561 }
562
563 #[test]
564 fn detect_nat_type_unknown_bad_format() {
565 let bindings = [StunBinding {
567 local_addr: "192.168.1.1".into(),
568 mapped_addr: "no-port-here".into(),
569 nat_type: NatType::Unknown,
570 latency_ms: 0,
571 timestamp: 0,
572 }];
573 assert_eq!(
579 NatTraversalManager::detect_nat_type(&bindings),
580 NatType::RestrictedCone
581 );
582 }
583
584 #[test]
587 fn add_binding_updates_nat_type() {
588 let mut mgr = NatTraversalManager::new(default_config());
589 assert_eq!(*mgr.detected_nat_type(), NatType::Unknown);
590
591 mgr.add_binding(make_binding("192.168.1.1:5000", "1.2.3.4:6000"));
592 assert_eq!(*mgr.detected_nat_type(), NatType::RestrictedCone);
594
595 mgr.add_binding(make_binding("192.168.1.1:5001", "1.2.3.4:6000"));
596 assert_eq!(*mgr.detected_nat_type(), NatType::FullCone);
598 }
599
600 #[test]
603 fn predict_port_linear_pattern() {
604 let mgr = NatTraversalManager::new(default_config());
605 let predicted = mgr.predict_port(5000, &[5000, 5002, 5004]);
607 assert!(predicted.contains(&5006));
609 assert!(predicted.contains(&4996));
610 assert!(predicted.contains(&5016));
611 }
612
613 #[test]
614 fn predict_port_no_samples() {
615 let mgr = NatTraversalManager::new(default_config());
616 let predicted = mgr.predict_port(8000, &[]);
617 assert!(predicted.contains(&8000));
619 assert!(predicted.contains(&7990));
620 assert!(predicted.contains(&8010));
621 }
622
623 #[test]
624 fn predict_port_single_sample() {
625 let mgr = NatTraversalManager::new(default_config());
626 let predicted = mgr.predict_port(4000, &[4000]);
627 assert!(predicted.contains(&4000));
629 }
630
631 #[test]
632 fn predict_port_random_pattern() {
633 let mgr = NatTraversalManager::new(default_config());
634 let predicted = mgr.predict_port(3000, &[3000, 3005, 3003]);
636 assert!(predicted.contains(&3000));
638 }
639
640 #[test]
641 fn predict_port_near_upper_bound() {
642 let mgr = NatTraversalManager::new(default_config());
643 let predicted = mgr.predict_port(65530, &[65528, 65530, 65532]);
644 assert!(predicted.last().copied().unwrap_or(0) == 65535);
646 }
647
648 #[test]
649 fn predict_port_near_lower_bound() {
650 let mgr = NatTraversalManager::new(default_config());
651 let predicted = mgr.predict_port(5, &[10, 5, 0]);
652 assert!(predicted.contains(&0));
654 }
655
656 #[test]
659 fn initiate_punch_success() {
660 let mut mgr = NatTraversalManager::new(default_config());
661 let result = mgr.initiate_punch("peer-1", "1.2.3.4:8000", 5000);
662 assert!(result.is_ok());
663 assert_eq!(mgr.active_punch_count(), 1);
664 }
665
666 #[test]
667 fn initiate_punch_duplicate_error() {
668 let mut mgr = NatTraversalManager::new(default_config());
669 let _ = mgr.initiate_punch("peer-1", "1.2.3.4:8000", 5000);
670 let result = mgr.initiate_punch("peer-1", "1.2.3.4:9000", 6000);
671 assert!(result.is_err());
672 }
673
674 #[test]
675 fn initiate_punch_increments_total_attempts() {
676 let mut mgr = NatTraversalManager::new(default_config());
677 let _ = mgr.initiate_punch("p1", "1.2.3.4:80", 100);
678 let _ = mgr.initiate_punch("p2", "5.6.7.8:80", 200);
679 assert_eq!(mgr.stats().total_attempts, 2);
680 }
681
682 #[test]
685 fn update_attempt_success() {
686 let mut mgr = NatTraversalManager::new(default_config());
687 let _ = mgr.initiate_punch("peer-1", "1.2.3.4:8000", 5000);
688 let result = mgr.update_attempt("peer-1", HolePunchStatus::Success);
689 assert!(result.is_ok());
690 assert_eq!(mgr.stats().successful, 1);
691 }
692
693 #[test]
694 fn update_attempt_unknown_peer() {
695 let mut mgr = NatTraversalManager::new(default_config());
696 let result = mgr.update_attempt("ghost", HolePunchStatus::Success);
697 assert!(result.is_err());
698 }
699
700 #[test]
701 fn update_attempt_failed() {
702 let mut mgr = NatTraversalManager::new(default_config());
703 let _ = mgr.initiate_punch("peer-1", "1.2.3.4:8000", 5000);
704 let _ = mgr.update_attempt("peer-1", HolePunchStatus::Failed);
705 assert_eq!(mgr.stats().failed, 1);
706 }
707
708 #[test]
709 fn update_attempt_timed_out() {
710 let mut mgr = NatTraversalManager::new(default_config());
711 let _ = mgr.initiate_punch("peer-1", "1.2.3.4:8000", 5000);
712 let _ = mgr.update_attempt("peer-1", HolePunchStatus::TimedOut);
713 assert_eq!(mgr.stats().timed_out, 1);
714 }
715
716 #[test]
717 fn update_attempt_max_exceeded_auto_fails() {
718 let config = NatTraversalConfig {
719 max_punch_attempts: 2,
720 ..default_config()
721 };
722 let mut mgr = NatTraversalManager::new(config);
723 let _ = mgr.initiate_punch("peer-1", "1.2.3.4:8000", 5000);
724
725 let _ = mgr.update_attempt("peer-1", HolePunchStatus::InProgress);
727 let _ = mgr.update_attempt("peer-1", HolePunchStatus::InProgress);
728 let _ = mgr.update_attempt("peer-1", HolePunchStatus::InProgress);
729
730 let attempt = mgr.get_attempt("peer-1");
731 assert!(attempt.is_some());
732 assert_eq!(attempt.map(|a| &a.status), Some(&HolePunchStatus::Failed));
733 assert_eq!(mgr.stats().failed, 1);
734 }
735
736 #[test]
739 fn get_attempt_existing() {
740 let mut mgr = NatTraversalManager::new(default_config());
741 let _ = mgr.initiate_punch("peer-1", "1.2.3.4:80", 100);
742 let a = mgr.get_attempt("peer-1");
743 assert!(a.is_some());
744 assert_eq!(a.map(|x| &x.peer_id).unwrap_or(&String::new()), "peer-1");
745 }
746
747 #[test]
748 fn get_attempt_missing() {
749 let mgr = NatTraversalManager::new(default_config());
750 assert!(mgr.get_attempt("nobody").is_none());
751 }
752
753 #[test]
756 fn active_punch_count_excludes_terminal() {
757 let mut mgr = NatTraversalManager::new(default_config());
758 let _ = mgr.initiate_punch("p1", "1.2.3.4:80", 100);
759 let _ = mgr.initiate_punch("p2", "5.6.7.8:80", 200);
760 let _ = mgr.update_attempt("p1", HolePunchStatus::Success);
761 assert_eq!(mgr.active_punch_count(), 1);
763 }
764
765 #[test]
768 fn cleanup_expired_removes_old_terminal() {
769 let mut mgr = NatTraversalManager::new(default_config());
770 let _ = mgr.initiate_punch("p1", "1.2.3.4:80", 100);
771 let _ = mgr.update_attempt("p1", HolePunchStatus::Success);
772
773 let removed = mgr.cleanup_expired(u64::MAX);
775 assert_eq!(removed, 1);
776 assert!(mgr.get_attempt("p1").is_none());
777 }
778
779 #[test]
780 fn cleanup_expired_keeps_recent() {
781 let mut mgr = NatTraversalManager::new(default_config());
782 let _ = mgr.initiate_punch("p1", "1.2.3.4:80", 100);
783 let removed = mgr.cleanup_expired(u64::MAX);
785 assert_eq!(removed, 0);
786 }
787
788 #[test]
791 fn success_rate_no_attempts() {
792 let mgr = NatTraversalManager::new(default_config());
793 assert!((mgr.success_rate() - 0.0).abs() < f64::EPSILON);
794 }
795
796 #[test]
797 fn success_rate_half() {
798 let mut mgr = NatTraversalManager::new(default_config());
799 let _ = mgr.initiate_punch("p1", "1.2.3.4:80", 100);
800 let _ = mgr.initiate_punch("p2", "5.6.7.8:80", 200);
801 let _ = mgr.update_attempt("p1", HolePunchStatus::Success);
802 let _ = mgr.update_attempt("p2", HolePunchStatus::Failed);
803 assert!((mgr.success_rate() - 0.5).abs() < f64::EPSILON);
804 }
805
806 #[test]
809 fn strategy_open() {
810 let mut mgr = NatTraversalManager::new(default_config());
811 mgr.add_binding(make_binding("1.2.3.4:80", "1.2.3.4:80"));
812 assert_eq!(mgr.best_strategy(), TraversalStrategy::Direct);
813 }
814
815 #[test]
816 fn strategy_full_cone() {
817 let mut mgr = NatTraversalManager::new(default_config());
818 mgr.add_binding(make_binding("192.168.1.1:80", "1.2.3.4:6000"));
819 mgr.add_binding(make_binding("192.168.1.1:81", "1.2.3.4:6000"));
820 assert_eq!(mgr.best_strategy(), TraversalStrategy::HolePunch);
821 }
822
823 #[test]
824 fn strategy_port_restricted() {
825 let mut mgr = NatTraversalManager::new(default_config());
826 mgr.add_binding(make_binding("192.168.1.1:80", "1.2.3.4:6000"));
827 mgr.add_binding(make_binding("192.168.1.1:81", "1.2.3.4:6001"));
828 assert_eq!(mgr.best_strategy(), TraversalStrategy::PortPrediction);
829 }
830
831 #[test]
832 fn strategy_symmetric_with_relay() {
833 let mut mgr = NatTraversalManager::new(default_config());
834 mgr.add_binding(make_binding("192.168.1.1:80", "1.2.3.4:6000"));
835 mgr.add_binding(make_binding("192.168.1.1:81", "5.6.7.8:6001"));
836 assert_eq!(mgr.best_strategy(), TraversalStrategy::Relay);
837 }
838
839 #[test]
840 fn strategy_symmetric_no_relay() {
841 let config = NatTraversalConfig {
842 enable_relay_fallback: false,
843 ..default_config()
844 };
845 let mut mgr = NatTraversalManager::new(config);
846 mgr.add_binding(make_binding("192.168.1.1:80", "1.2.3.4:6000"));
847 mgr.add_binding(make_binding("192.168.1.1:81", "5.6.7.8:6001"));
848 assert_eq!(mgr.best_strategy(), TraversalStrategy::HolePunch);
849 }
850
851 #[test]
852 fn strategy_unknown_defaults_to_relay() {
853 let mgr = NatTraversalManager::new(default_config());
854 assert_eq!(mgr.best_strategy(), TraversalStrategy::Relay);
855 }
856
857 #[test]
860 fn stats_default() {
861 let s = NatTraversalStats::default();
862 assert_eq!(s.total_attempts, 0);
863 assert_eq!(s.successful, 0);
864 assert_eq!(s.failed, 0);
865 assert_eq!(s.timed_out, 0);
866 assert!((s.avg_punch_time_ms - 0.0).abs() < f64::EPSILON);
867 }
868
869 #[test]
870 fn stats_reference() {
871 let mgr = NatTraversalManager::new(default_config());
872 let s = mgr.stats();
873 assert_eq!(s.total_attempts, 0);
874 }
875
876 #[test]
879 fn hole_punch_attempt_clone() {
880 let a = HolePunchAttempt {
881 peer_id: "p1".into(),
882 local_port: 1234,
883 remote_addr: "1.2.3.4:80".into(),
884 attempts: 0,
885 max_attempts: 5,
886 status: HolePunchStatus::Pending,
887 started_at: 100,
888 };
889 let b = a.clone();
890 assert_eq!(a.peer_id, b.peer_id);
891 assert_eq!(a.status, b.status);
892 }
893
894 #[test]
897 fn nat_type_equality() {
898 assert_eq!(NatType::Open, NatType::Open);
899 assert_ne!(NatType::Open, NatType::FullCone);
900 assert_ne!(NatType::Symmetric, NatType::Unknown);
901 }
902
903 #[test]
906 fn port_range_basic() {
907 let r = port_range(100, 5);
908 assert_eq!(r.len(), 11); assert_eq!(r[0], 95);
910 assert_eq!(*r.last().unwrap_or(&0), 105);
911 }
912
913 #[test]
914 fn port_range_saturates_low() {
915 let r = port_range(3, 10);
916 assert_eq!(r[0], 0);
917 }
918
919 #[test]
920 fn port_range_saturates_high() {
921 let r = port_range(65530, 10);
922 assert_eq!(*r.last().unwrap_or(&0), 65535);
923 }
924}