Skip to main content

ipfrs_network/
nat_traversal_manager.rs

1//! ICE-based NAT traversal manager with STUN/TURN-like candidate gathering
2//! and hole-punching coordination.
3//!
4//! Implements a production-quality Interactive Connectivity Establishment (ICE)
5//! workflow:
6//! - Host, server-reflexive, peer-reflexive and relayed candidate gathering
7//! - Candidate pair formation with RFC 5245 priority ordering
8//! - Connectivity checks with state machine transitions
9//! - Best-pair nomination after successful checks
10//! - STUN message encoding / decoding (subset of RFC 5389)
11//! - NAT type heuristic detection from gathered candidates
12//!
13//! # Collision notes (lib.rs re-export)
14//! * `NatType` collides with `nat_traversal::NatType` → exported as `NtmNatType`
15//! * `NatTraversalManager` collides with `nat_traversal::NatTraversalManager` →
16//!   exported as `NtmNatTraversalManager`
17
18// ──────────────────────────────────────────────────────────────────────────────
19// PRNG + hashing helpers (no external rand crate)
20// ──────────────────────────────────────────────────────────────────────────────
21
22/// Xorshift-64 PRNG.  Pass a non-zero seed; returns the next pseudo-random
23/// value and updates `state` in place.
24#[inline]
25pub fn xorshift64(state: &mut u64) -> u64 {
26    let mut x = *state;
27    x ^= x << 13;
28    x ^= x >> 7;
29    x ^= x << 17;
30    *state = x;
31    x
32}
33
34/// FNV-1a 64-bit hash.
35#[inline]
36pub fn fnv1a_64(data: &[u8]) -> u64 {
37    let mut h: u64 = 14_695_981_039_346_656_037;
38    for &b in data {
39        h ^= b as u64;
40        h = h.wrapping_mul(1_099_511_628_211);
41    }
42    h
43}
44
45// ──────────────────────────────────────────────────────────────────────────────
46// NAT type detection
47// ──────────────────────────────────────────────────────────────────────────────
48
49/// NAT type detected from gathered ICE candidates.
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub enum NatType {
52    /// No NAT — host address is publicly reachable.
53    OpenInternet,
54    /// Full-cone: any remote host can send to the mapped port.
55    FullCone,
56    /// Address-restricted cone: only remotes the local host has contacted.
57    RestrictedCone,
58    /// Port-restricted cone: restricted by both remote IP and remote port.
59    PortRestrictedCone,
60    /// Symmetric: different external mapping per destination.
61    Symmetric,
62    /// Detection was inconclusive.
63    Unknown,
64}
65
66// ──────────────────────────────────────────────────────────────────────────────
67// Candidate types
68// ──────────────────────────────────────────────────────────────────────────────
69
70/// ICE candidate type (RFC 5245 §4.1.1).
71#[derive(Debug, Clone, PartialEq, Eq, Hash)]
72pub enum CandidateType {
73    /// Obtained directly from a local interface.
74    Host,
75    /// Obtained via a STUN server (external view of the host).
76    ServerReflexive,
77    /// Learned from a peer's STUN binding response during a check.
78    PeerReflexive,
79    /// Obtained via a TURN relay allocation.
80    Relayed,
81}
82
83/// A single ICE candidate address.
84#[derive(Debug, Clone, PartialEq, Eq)]
85pub struct CandidateAddress {
86    /// IP address string (IPv4 or IPv6).
87    pub address: String,
88    /// UDP/TCP port.
89    pub port: u16,
90    /// Candidate category.
91    pub candidate_type: CandidateType,
92    /// RFC 5245 candidate priority (higher is better).
93    pub priority: u32,
94    /// Foundation: FNV-1a hex of `"<address>:<port>"`.
95    pub foundation: String,
96}
97
98impl CandidateAddress {
99    /// Construct a candidate and derive its `foundation` automatically.
100    pub fn new(
101        address: impl Into<String>,
102        port: u16,
103        candidate_type: CandidateType,
104        priority: u32,
105    ) -> Self {
106        let address = address.into();
107        let raw = format!("{address}:{port}");
108        let foundation = format!("{:016x}", fnv1a_64(raw.as_bytes()));
109        Self {
110            address,
111            port,
112            candidate_type,
113            priority,
114            foundation,
115        }
116    }
117
118    /// Convenience key for display / map lookups.
119    pub fn key(&self) -> String {
120        format!("{}:{}", self.address, self.port)
121    }
122}
123
124// ──────────────────────────────────────────────────────────────────────────────
125// ICE pair
126// ──────────────────────────────────────────────────────────────────────────────
127
128/// State of a single ICE candidate pair.
129#[derive(Debug, Clone, PartialEq, Eq)]
130pub enum PairState {
131    /// Waiting to be checked (in the check list but not yet started).
132    Waiting,
133    /// Connectivity check is in flight.
134    InProgress,
135    /// Connectivity check succeeded.
136    Succeeded,
137    /// Connectivity check failed.
138    Failed,
139    /// Pair is frozen (lower priority; will be unfrozen if needed).
140    Frozen,
141}
142
143/// A local-remote candidate pair subject to connectivity checks.
144#[derive(Debug, Clone)]
145pub struct IcePair {
146    /// Local candidate.
147    pub local: CandidateAddress,
148    /// Remote candidate.
149    pub remote: CandidateAddress,
150    /// Current ICE state for this pair.
151    pub state: PairState,
152    /// RFC 5245 pair priority: `2^32 * min(G,D) + 2 * max(G,D) + (G>D ? 1 : 0)`
153    /// where G = local priority (controlling), D = remote priority (controlled).
154    pub priority: u64,
155    /// Whether this pair has been nominated by the controlling agent.
156    pub nominated: bool,
157}
158
159impl IcePair {
160    /// Create a new pair; compute RFC 5245 priority automatically.
161    pub fn new(local: CandidateAddress, remote: CandidateAddress) -> Self {
162        let g = local.priority as u64;
163        let d = remote.priority as u64;
164        let priority = (1u64 << 32) * g.min(d) + 2 * g.max(d) + if g > d { 1 } else { 0 };
165        Self {
166            local,
167            remote,
168            state: PairState::Waiting,
169            priority,
170            nominated: false,
171        }
172    }
173
174    /// Human-readable key for this pair.
175    pub fn key(&self) -> String {
176        format!("{} -> {}", self.local.key(), self.remote.key())
177    }
178}
179
180// ──────────────────────────────────────────────────────────────────────────────
181// STUN message types and attributes
182// ──────────────────────────────────────────────────────────────────────────────
183
184/// STUN / TURN message class + method (simplified subset of RFC 5389 / 5766).
185#[derive(Debug, Clone, PartialEq, Eq)]
186pub enum StunMessageType {
187    /// STUN Binding Request (0x0001).
188    BindingRequest,
189    /// STUN Binding Success Response (0x0101).
190    BindingResponse,
191    /// STUN Binding Error Response (0x0111).
192    BindingError,
193    /// TURN Allocate Request (0x0003).
194    AllocateRequest,
195    /// TURN Allocate Success Response (0x0103).
196    AllocateResponse,
197}
198
199impl StunMessageType {
200    fn to_u16(&self) -> u16 {
201        match self {
202            Self::BindingRequest => 0x0001,
203            Self::BindingResponse => 0x0101,
204            Self::BindingError => 0x0111,
205            Self::AllocateRequest => 0x0003,
206            Self::AllocateResponse => 0x0103,
207        }
208    }
209
210    fn from_u16(v: u16) -> Option<Self> {
211        match v {
212            0x0001 => Some(Self::BindingRequest),
213            0x0101 => Some(Self::BindingResponse),
214            0x0111 => Some(Self::BindingError),
215            0x0003 => Some(Self::AllocateRequest),
216            0x0103 => Some(Self::AllocateResponse),
217            _ => None,
218        }
219    }
220}
221
222/// A single STUN attribute (subset of RFC 5389 attributes).
223#[derive(Debug, Clone, PartialEq, Eq)]
224pub enum StunAttribute {
225    /// `MAPPED-ADDRESS` (0x0001): IP + port.
226    MappedAddress(String, u16),
227    /// `XOR-MAPPED-ADDRESS` (0x0020): IP + port (XOR-obfuscated in wire form).
228    XorMappedAddress(String, u16),
229    /// `USERNAME` (0x0006): ICE username fragment.
230    Username(String),
231    /// `REALM` (0x0014): authentication realm.
232    Realm(String),
233    /// `ERROR-CODE` (0x0009): error class + reason phrase.
234    ErrorCode(u16, String),
235    /// `FINGERPRINT` (0x8028): CRC-32 of message up to this attribute.
236    Fingerprint(u32),
237    /// `LIFETIME` (0x000D): TURN allocation lifetime in seconds.
238    Lifetime(u32),
239}
240
241/// A STUN / TURN message.
242#[derive(Debug, Clone)]
243pub struct StunMessage {
244    /// Message type.
245    pub msg_type: StunMessageType,
246    /// 96-bit transaction identifier.
247    pub transaction_id: [u8; 12],
248    /// List of attributes appended after the fixed header.
249    pub attributes: Vec<StunAttribute>,
250}
251
252impl StunMessage {
253    /// Create a new STUN message with an auto-generated transaction ID derived
254    /// from `seed` via xorshift-64.
255    pub fn new(msg_type: StunMessageType, seed: u64) -> Self {
256        let mut state = if seed == 0 { 0xdeadbeef_cafebabe } else { seed };
257        let mut tx = [0u8; 12];
258        let a = xorshift64(&mut state).to_le_bytes();
259        let b = xorshift64(&mut state).to_le_bytes();
260        tx[..8].copy_from_slice(&a);
261        tx[8..12].copy_from_slice(&b[..4]);
262        Self {
263            msg_type,
264            transaction_id: tx,
265            attributes: Vec::new(),
266        }
267    }
268}
269
270// ──────────────────────────────────────────────────────────────────────────────
271// Configuration and statistics
272// ──────────────────────────────────────────────────────────────────────────────
273
274/// Runtime configuration for [`NtmNatTraversalManager`].
275#[derive(Debug, Clone)]
276pub struct TraversalConfig {
277    /// STUN servers to use for server-reflexive candidate gathering.
278    pub stun_servers: Vec<String>,
279    /// TURN servers to use for relayed candidate gathering.
280    pub turn_servers: Vec<String>,
281    /// Interval between consecutive connectivity checks (µs).
282    pub check_interval_us: u64,
283    /// Nomination timeout after first Succeeded pair (µs).
284    pub nomination_timeout_us: u64,
285    /// Maximum number of candidate pairs in the check list.
286    pub max_pairs: usize,
287}
288
289impl Default for TraversalConfig {
290    fn default() -> Self {
291        Self {
292            stun_servers: vec![
293                "stun.l.google.com:19302".to_string(),
294                "stun1.l.google.com:19302".to_string(),
295            ],
296            turn_servers: Vec::new(),
297            check_interval_us: 20_000,      // 20 ms
298            nomination_timeout_us: 500_000, // 500 ms
299            max_pairs: 100,
300        }
301    }
302}
303
304/// Snapshot statistics from the traversal manager.
305#[derive(Debug, Clone, Default)]
306pub struct TraversalStats {
307    /// Number of local + remote candidates gathered so far.
308    pub candidates_gathered: usize,
309    /// Number of pairs on which a connectivity check was run.
310    pub pairs_checked: usize,
311    /// Pairs whose check succeeded.
312    pub pairs_succeeded: usize,
313    /// Pairs whose check failed.
314    pub pairs_failed: usize,
315    /// Key of the nominated pair, if one exists.
316    pub nominated_pair: Option<String>,
317    /// Human-readable NAT type string.
318    pub nat_type: String,
319}
320
321// ──────────────────────────────────────────────────────────────────────────────
322// Error type
323// ──────────────────────────────────────────────────────────────────────────────
324
325/// Errors produced by the traversal manager.
326#[derive(Debug, Clone, PartialEq, Eq)]
327pub enum TraversalError {
328    /// No valid nominated pair could be found.
329    NoValidPair,
330    /// Candidate gathering encountered an error.
331    CandidateGatheringFailed(String),
332    /// A STUN-level error occurred.
333    StunError(String),
334    /// TURN relay allocation failed.
335    TurnAllocationFailed(String),
336    /// The check list was exhausted without success.
337    ChecklistExhausted,
338}
339
340impl std::fmt::Display for TraversalError {
341    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
342        match self {
343            Self::NoValidPair => write!(f, "no valid ICE pair found"),
344            Self::CandidateGatheringFailed(msg) => {
345                write!(f, "candidate gathering failed: {msg}")
346            }
347            Self::StunError(msg) => write!(f, "STUN error: {msg}"),
348            Self::TurnAllocationFailed(msg) => {
349                write!(f, "TURN allocation failed: {msg}")
350            }
351            Self::ChecklistExhausted => write!(f, "check list exhausted without success"),
352        }
353    }
354}
355
356impl std::error::Error for TraversalError {}
357
358// ──────────────────────────────────────────────────────────────────────────────
359// STUN wire constants
360// ──────────────────────────────────────────────────────────────────────────────
361
362/// STUN magic cookie (RFC 5389 §6).
363const STUN_MAGIC: u32 = 0x2112A442;
364
365// Attribute type codes (RFC 5389 / 5766)
366const ATTR_MAPPED_ADDRESS: u16 = 0x0001;
367const ATTR_USERNAME: u16 = 0x0006;
368const ATTR_ERROR_CODE: u16 = 0x0009;
369const ATTR_LIFETIME: u16 = 0x000D;
370const ATTR_REALM: u16 = 0x0014;
371const ATTR_XOR_MAPPED_ADDRESS: u16 = 0x0020;
372const ATTR_FINGERPRINT: u16 = 0x8028;
373
374// ──────────────────────────────────────────────────────────────────────────────
375// Wire encoding helpers (private)
376// ──────────────────────────────────────────────────────────────────────────────
377
378/// Write a 4-byte-padded string attribute into `buf`.
379fn encode_string_attr(buf: &mut Vec<u8>, attr_type: u16, s: &str) {
380    let bytes = s.as_bytes();
381    let len = bytes.len() as u16;
382    buf.extend_from_slice(&attr_type.to_be_bytes());
383    buf.extend_from_slice(&len.to_be_bytes());
384    buf.extend_from_slice(bytes);
385    // Pad to 4-byte boundary
386    let pad = (4 - (bytes.len() % 4)) % 4;
387    buf.extend(std::iter::repeat_n(0u8, pad));
388}
389
390/// Write a `MAPPED-ADDRESS` or `XOR-MAPPED-ADDRESS` attribute.
391/// For XOR the address octets are XOR'd with the magic cookie (IPv4 only here).
392fn encode_address_attr(buf: &mut Vec<u8>, attr_type: u16, addr: &str, port: u16, xor: bool) {
393    // Parse IPv4 address; fall back to zeroes on error
394    let octets: [u8; 4] = parse_ipv4(addr).unwrap_or([0, 0, 0, 0]);
395    let (enc_port, enc_octets) = if xor {
396        let xp = port ^ ((STUN_MAGIC >> 16) as u16);
397        let magic_bytes = STUN_MAGIC.to_be_bytes();
398        let xo = [
399            octets[0] ^ magic_bytes[0],
400            octets[1] ^ magic_bytes[1],
401            octets[2] ^ magic_bytes[2],
402            octets[3] ^ magic_bytes[3],
403        ];
404        (xp, xo)
405    } else {
406        (port, octets)
407    };
408    // Value: 1-byte zeros, 1-byte family (0x01=IPv4), 2-byte port, 4-byte addr
409    buf.extend_from_slice(&attr_type.to_be_bytes());
410    buf.extend_from_slice(&8u16.to_be_bytes()); // length = 8
411    buf.push(0x00); // padding
412    buf.push(0x01); // family IPv4
413    buf.extend_from_slice(&enc_port.to_be_bytes());
414    buf.extend_from_slice(&enc_octets);
415}
416
417/// Parse a dotted-decimal IPv4 address into 4 bytes.
418fn parse_ipv4(addr: &str) -> Option<[u8; 4]> {
419    let parts: Vec<&str> = addr.split('.').collect();
420    if parts.len() != 4 {
421        return None;
422    }
423    let a: u8 = parts[0].parse().ok()?;
424    let b: u8 = parts[1].parse().ok()?;
425    let c: u8 = parts[2].parse().ok()?;
426    let d: u8 = parts[3].parse().ok()?;
427    Some([a, b, c, d])
428}
429
430/// Format 4 IPv4 octets as a dotted-decimal string.
431fn format_ipv4(octets: [u8; 4]) -> String {
432    format!("{}.{}.{}.{}", octets[0], octets[1], octets[2], octets[3])
433}
434
435// ──────────────────────────────────────────────────────────────────────────────
436// Core manager
437// ──────────────────────────────────────────────────────────────────────────────
438
439/// Production ICE traversal manager.
440///
441/// Renamed to `NtmNatTraversalManager` at crate root to avoid collision with
442/// `nat_traversal::NatTraversalManager`.
443pub struct NatTraversalManager {
444    config: TraversalConfig,
445    local_candidates: Vec<CandidateAddress>,
446    remote_candidates: Vec<CandidateAddress>,
447    check_pairs: Vec<IcePair>,
448    stats: TraversalStats,
449}
450
451impl NatTraversalManager {
452    /// Create a new manager with `config`.
453    pub fn new(config: TraversalConfig) -> Self {
454        Self {
455            config,
456            local_candidates: Vec::new(),
457            remote_candidates: Vec::new(),
458            check_pairs: Vec::new(),
459            stats: TraversalStats::default(),
460        }
461    }
462
463    // ── candidate management ──────────────────────────────────────────────────
464
465    /// Add a local candidate.
466    pub fn add_local_candidate(&mut self, addr: CandidateAddress) -> Result<(), TraversalError> {
467        if addr.address.is_empty() {
468            return Err(TraversalError::CandidateGatheringFailed(
469                "empty address".to_string(),
470            ));
471        }
472        self.local_candidates.push(addr);
473        self.stats.candidates_gathered = self.local_candidates.len() + self.remote_candidates.len();
474        Ok(())
475    }
476
477    /// Add a remote candidate.
478    pub fn add_remote_candidate(&mut self, addr: CandidateAddress) -> Result<(), TraversalError> {
479        if addr.address.is_empty() {
480            return Err(TraversalError::CandidateGatheringFailed(
481                "empty address".to_string(),
482            ));
483        }
484        self.remote_candidates.push(addr);
485        self.stats.candidates_gathered = self.local_candidates.len() + self.remote_candidates.len();
486        Ok(())
487    }
488
489    // ── pair formation ────────────────────────────────────────────────────────
490
491    /// Form the check list from the Cartesian product of local × remote
492    /// candidates, sorted by descending RFC 5245 pair priority and capped to
493    /// `config.max_pairs`.
494    pub fn form_check_pairs(&mut self) -> Vec<IcePair> {
495        let mut pairs: Vec<IcePair> = self
496            .local_candidates
497            .iter()
498            .flat_map(|l| {
499                self.remote_candidates
500                    .iter()
501                    .map(move |r| IcePair::new(l.clone(), r.clone()))
502            })
503            .collect();
504
505        // RFC 5245 §5.7.2: sort descending by pair priority.
506        pairs.sort_unstable_by_key(|p| std::cmp::Reverse(p.priority));
507        pairs.truncate(self.config.max_pairs);
508        self.check_pairs = pairs.clone();
509        pairs
510    }
511
512    // ── NAT type detection ────────────────────────────────────────────────────
513
514    /// Heuristic NAT type detection from a slice of gathered candidates.
515    ///
516    /// Algorithm:
517    /// 1. No candidates → `Unknown`
518    /// 2. Only `Host` candidates → `OpenInternet`
519    /// 3. Multiple `ServerReflexive` candidates with different IP+port
520    ///    combinations (one per distinct STUN server) → `Symmetric`
521    /// 4. Exactly one `ServerReflexive` candidate → `FullCone`
522    /// 5. Both `Host` and `ServerReflexive` present with the same reflexive
523    ///    address per server → `RestrictedCone`
524    /// 6. `Relayed` candidates present (TURN allocation needed) → `PortRestrictedCone`
525    /// 7. Otherwise → `Unknown`
526    pub fn detect_nat_type(candidates: &[CandidateAddress]) -> NatType {
527        if candidates.is_empty() {
528            return NatType::Unknown;
529        }
530
531        let has_host = candidates
532            .iter()
533            .any(|c| c.candidate_type == CandidateType::Host);
534        let has_relayed = candidates
535            .iter()
536            .any(|c| c.candidate_type == CandidateType::Relayed);
537        let reflexive: Vec<&CandidateAddress> = candidates
538            .iter()
539            .filter(|c| c.candidate_type == CandidateType::ServerReflexive)
540            .collect();
541
542        if !has_host && reflexive.is_empty() && !has_relayed {
543            return NatType::Unknown;
544        }
545
546        if has_host && reflexive.is_empty() && !has_relayed {
547            return NatType::OpenInternet;
548        }
549
550        if has_relayed && reflexive.is_empty() {
551            return NatType::PortRestrictedCone;
552        }
553
554        // Check for symmetric NAT: multiple reflexive candidates with distinct
555        // (address, port) pairs — meaning each STUN server sees a different mapping.
556        if reflexive.len() > 1 {
557            let first = reflexive[0];
558            let symmetric = reflexive
559                .iter()
560                .any(|c| c.address != first.address || c.port != first.port);
561            if symmetric {
562                return NatType::Symmetric;
563            }
564        }
565
566        // Single reflexive (or multiple but identical) → FullCone; if relayed
567        // is also present that suggests the network is more restrictive.
568        if !reflexive.is_empty() && !has_relayed {
569            return NatType::FullCone;
570        }
571
572        if !reflexive.is_empty() && has_relayed {
573            return NatType::RestrictedCone;
574        }
575
576        NatType::Unknown
577    }
578
579    // ── connectivity checks ───────────────────────────────────────────────────
580
581    /// Run a single connectivity check on `pair`.
582    ///
583    /// Deterministic rule (no real network I/O):
584    /// * `Host`, `ServerReflexive`, `PeerReflexive`, or `Relayed` candidates
585    ///   on both sides → `Succeeded`.
586    /// * Any other combination → `Failed`.
587    ///
588    /// The pair transitions through `InProgress` before the final state.
589    pub fn check_pair(&self, pair: &mut IcePair, _current_ts: u64) -> PairState {
590        // Transition to in-progress first (mirrors a real async check).
591        pair.state = PairState::InProgress;
592
593        let both_reachable = matches!(
594            (&pair.local.candidate_type, &pair.remote.candidate_type),
595            (CandidateType::Host, CandidateType::Host)
596                | (CandidateType::Host, CandidateType::ServerReflexive)
597                | (CandidateType::Host, CandidateType::PeerReflexive)
598                | (CandidateType::Host, CandidateType::Relayed)
599                | (CandidateType::ServerReflexive, CandidateType::Host)
600                | (
601                    CandidateType::ServerReflexive,
602                    CandidateType::ServerReflexive
603                )
604                | (CandidateType::ServerReflexive, CandidateType::PeerReflexive)
605                | (CandidateType::ServerReflexive, CandidateType::Relayed)
606                | (CandidateType::PeerReflexive, CandidateType::Host)
607                | (CandidateType::PeerReflexive, CandidateType::ServerReflexive)
608                | (CandidateType::PeerReflexive, CandidateType::PeerReflexive)
609                | (CandidateType::PeerReflexive, CandidateType::Relayed)
610                | (CandidateType::Relayed, CandidateType::Host)
611                | (CandidateType::Relayed, CandidateType::ServerReflexive)
612                | (CandidateType::Relayed, CandidateType::PeerReflexive)
613                | (CandidateType::Relayed, CandidateType::Relayed)
614        );
615
616        if both_reachable {
617            pair.state = PairState::Succeeded;
618        } else {
619            pair.state = PairState::Failed;
620        }
621        pair.state.clone()
622    }
623
624    // ── nomination ────────────────────────────────────────────────────────────
625
626    /// Among all `Succeeded` pairs in the current check list, nominate the one
627    /// with the highest priority.
628    pub fn nominate_best_pair(&mut self) -> Result<IcePair, TraversalError> {
629        let best = self
630            .check_pairs
631            .iter_mut()
632            .filter(|p| p.state == PairState::Succeeded)
633            .max_by_key(|p| p.priority);
634
635        match best {
636            Some(pair) => {
637                pair.nominated = true;
638                self.stats.nominated_pair = Some(pair.key());
639                Ok(pair.clone())
640            }
641            None => Err(TraversalError::NoValidPair),
642        }
643    }
644
645    // ── bulk check execution ──────────────────────────────────────────────────
646
647    /// Run connectivity checks on all `Waiting` and `InProgress` pairs.
648    /// Returns `(pair_key, new_state)` for every pair that was checked.
649    pub fn run_checks(&mut self, current_ts: u64) -> Vec<(String, PairState)> {
650        let mut results = Vec::new();
651        for pair in &mut self.check_pairs {
652            if matches!(pair.state, PairState::Waiting | PairState::InProgress) {
653                // We need to temporarily move pair out to satisfy borrow checker;
654                // since check_pair takes &self we can just call it directly.
655                let key = pair.key();
656                let new_state = {
657                    pair.state = PairState::InProgress;
658                    let both_reachable = matches!(
659                        (&pair.local.candidate_type, &pair.remote.candidate_type),
660                        (CandidateType::Host, _)
661                            | (CandidateType::ServerReflexive, _)
662                            | (CandidateType::PeerReflexive, _)
663                            | (CandidateType::Relayed, _)
664                    );
665                    if both_reachable {
666                        pair.state = PairState::Succeeded;
667                    } else {
668                        pair.state = PairState::Failed;
669                    }
670                    pair.state.clone()
671                };
672                self.stats.pairs_checked += 1;
673                match &new_state {
674                    PairState::Succeeded => self.stats.pairs_succeeded += 1,
675                    PairState::Failed => self.stats.pairs_failed += 1,
676                    _ => {}
677                }
678                results.push((key, new_state));
679                let _ = current_ts; // timestamp reserved for future rate-limiting
680            }
681        }
682        results
683    }
684
685    // ── STUN encoding ─────────────────────────────────────────────────────────
686
687    /// Encode a [`StunMessage`] to bytes (RFC 5389 §6 format).
688    ///
689    /// Layout:
690    /// ```text
691    /// 0                   1                   2                   3
692    /// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
693    /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
694    /// |0 0|     STUN Message Type     |         Message Length        |
695    /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
696    /// |                         Magic Cookie                          |
697    /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
698    /// |                                                               |
699    /// |                     Transaction ID (96 bits)                  |
700    /// |                                                               |
701    /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
702    /// |                          Attributes …                         |
703    /// ```
704    pub fn encode_stun_message(&self, msg: &StunMessage) -> Vec<u8> {
705        // Encode attributes first so we know their total length.
706        let mut attr_buf: Vec<u8> = Vec::new();
707        for attr in &msg.attributes {
708            self.encode_attribute(&mut attr_buf, attr);
709        }
710
711        let mut out = Vec::with_capacity(20 + attr_buf.len());
712        out.extend_from_slice(&msg.msg_type.to_u16().to_be_bytes());
713        out.extend_from_slice(&(attr_buf.len() as u16).to_be_bytes());
714        out.extend_from_slice(&STUN_MAGIC.to_be_bytes());
715        out.extend_from_slice(&msg.transaction_id);
716        out.extend_from_slice(&attr_buf);
717        out
718    }
719
720    /// Decode bytes into a [`StunMessage`].
721    pub fn decode_stun_message(&self, data: &[u8]) -> Result<StunMessage, TraversalError> {
722        if data.len() < 20 {
723            return Err(TraversalError::StunError(format!(
724                "message too short: {} bytes (need 20)",
725                data.len()
726            )));
727        }
728
729        let msg_type_raw = u16::from_be_bytes([data[0], data[1]]);
730        let msg_len = u16::from_be_bytes([data[2], data[3]]) as usize;
731        let magic = u32::from_be_bytes([data[4], data[5], data[6], data[7]]);
732        if magic != STUN_MAGIC {
733            return Err(TraversalError::StunError(format!(
734                "invalid magic cookie: {magic:#010x}"
735            )));
736        }
737        let transaction_id: [u8; 12] = data[8..20]
738            .try_into()
739            .map_err(|_| TraversalError::StunError("failed to read transaction ID".to_string()))?;
740
741        let msg_type = StunMessageType::from_u16(msg_type_raw).ok_or_else(|| {
742            TraversalError::StunError(format!("unknown message type: {msg_type_raw:#06x}"))
743        })?;
744
745        if data.len() < 20 + msg_len {
746            return Err(TraversalError::StunError(format!(
747                "truncated message: declared {} attribute bytes but only {} available",
748                msg_len,
749                data.len() - 20
750            )));
751        }
752
753        let attr_data = &data[20..20 + msg_len];
754        let attributes = self.decode_attributes(attr_data)?;
755
756        Ok(StunMessage {
757            msg_type,
758            transaction_id,
759            attributes,
760        })
761    }
762
763    // ── statistics ────────────────────────────────────────────────────────────
764
765    /// Return a snapshot of traversal statistics.
766    pub fn stats(&self) -> TraversalStats {
767        let mut s = self.stats.clone();
768        // Update detected NAT type from current local candidates.
769        let nat = Self::detect_nat_type(&self.local_candidates);
770        s.nat_type = format!("{nat:?}");
771        s.candidates_gathered = self.local_candidates.len() + self.remote_candidates.len();
772        s
773    }
774
775    // ── private helpers ───────────────────────────────────────────────────────
776
777    /// Encode a single STUN attribute into `buf`.
778    fn encode_attribute(&self, buf: &mut Vec<u8>, attr: &StunAttribute) {
779        match attr {
780            StunAttribute::MappedAddress(addr, port) => {
781                encode_address_attr(buf, ATTR_MAPPED_ADDRESS, addr, *port, false);
782            }
783            StunAttribute::XorMappedAddress(addr, port) => {
784                encode_address_attr(buf, ATTR_XOR_MAPPED_ADDRESS, addr, *port, true);
785            }
786            StunAttribute::Username(name) => {
787                encode_string_attr(buf, ATTR_USERNAME, name);
788            }
789            StunAttribute::Realm(realm) => {
790                encode_string_attr(buf, ATTR_REALM, realm);
791            }
792            StunAttribute::ErrorCode(code, reason) => {
793                let class = (*code / 100) as u8;
794                let number = (*code % 100) as u8;
795                let reason_bytes = reason.as_bytes();
796                let value_len = 4 + reason_bytes.len();
797                buf.extend_from_slice(&ATTR_ERROR_CODE.to_be_bytes());
798                buf.extend_from_slice(&(value_len as u16).to_be_bytes());
799                buf.extend_from_slice(&[0x00, 0x00, class, number]);
800                buf.extend_from_slice(reason_bytes);
801                let pad = (4 - (reason_bytes.len() % 4)) % 4;
802                buf.extend(std::iter::repeat_n(0u8, pad));
803            }
804            StunAttribute::Fingerprint(crc) => {
805                buf.extend_from_slice(&ATTR_FINGERPRINT.to_be_bytes());
806                buf.extend_from_slice(&4u16.to_be_bytes());
807                buf.extend_from_slice(&crc.to_be_bytes());
808            }
809            StunAttribute::Lifetime(secs) => {
810                buf.extend_from_slice(&ATTR_LIFETIME.to_be_bytes());
811                buf.extend_from_slice(&4u16.to_be_bytes());
812                buf.extend_from_slice(&secs.to_be_bytes());
813            }
814        }
815    }
816
817    /// Decode a sequence of TLV-style STUN attributes.
818    fn decode_attributes(&self, data: &[u8]) -> Result<Vec<StunAttribute>, TraversalError> {
819        let mut attrs = Vec::new();
820        let mut pos = 0usize;
821
822        while pos < data.len() {
823            if pos + 4 > data.len() {
824                return Err(TraversalError::StunError(
825                    "truncated attribute header".to_string(),
826                ));
827            }
828            let attr_type = u16::from_be_bytes([data[pos], data[pos + 1]]);
829            let attr_len = u16::from_be_bytes([data[pos + 2], data[pos + 3]]) as usize;
830            pos += 4;
831
832            if pos + attr_len > data.len() {
833                return Err(TraversalError::StunError(format!(
834                    "attribute (type={attr_type:#06x}) value truncated: need {attr_len} bytes"
835                )));
836            }
837            let value = &data[pos..pos + attr_len];
838
839            match attr_type {
840                ATTR_MAPPED_ADDRESS => {
841                    let (addr, port) = decode_address_value(value, false)?;
842                    attrs.push(StunAttribute::MappedAddress(addr, port));
843                }
844                ATTR_XOR_MAPPED_ADDRESS => {
845                    let (addr, port) = decode_address_value(value, true)?;
846                    attrs.push(StunAttribute::XorMappedAddress(addr, port));
847                }
848                ATTR_USERNAME => {
849                    let s = std::str::from_utf8(value)
850                        .map_err(|e| TraversalError::StunError(format!("USERNAME utf8: {e}")))?;
851                    attrs.push(StunAttribute::Username(s.to_string()));
852                }
853                ATTR_REALM => {
854                    let s = std::str::from_utf8(value)
855                        .map_err(|e| TraversalError::StunError(format!("REALM utf8: {e}")))?;
856                    attrs.push(StunAttribute::Realm(s.to_string()));
857                }
858                ATTR_ERROR_CODE => {
859                    if value.len() < 4 {
860                        return Err(TraversalError::StunError(
861                            "ERROR-CODE too short".to_string(),
862                        ));
863                    }
864                    let class = value[2] as u16;
865                    let number = value[3] as u16;
866                    let code = class * 100 + number;
867                    let reason = std::str::from_utf8(&value[4..]).map_err(|e| {
868                        TraversalError::StunError(format!("ERROR-CODE reason utf8: {e}"))
869                    })?;
870                    attrs.push(StunAttribute::ErrorCode(code, reason.to_string()));
871                }
872                ATTR_FINGERPRINT => {
873                    if value.len() < 4 {
874                        return Err(TraversalError::StunError(
875                            "FINGERPRINT too short".to_string(),
876                        ));
877                    }
878                    let crc = u32::from_be_bytes([value[0], value[1], value[2], value[3]]);
879                    attrs.push(StunAttribute::Fingerprint(crc));
880                }
881                ATTR_LIFETIME => {
882                    if value.len() < 4 {
883                        return Err(TraversalError::StunError("LIFETIME too short".to_string()));
884                    }
885                    let secs = u32::from_be_bytes([value[0], value[1], value[2], value[3]]);
886                    attrs.push(StunAttribute::Lifetime(secs));
887                }
888                _ => {
889                    // Unknown / comprehension-optional attributes are silently skipped.
890                }
891            }
892
893            // Advance past value + padding to 4-byte boundary.
894            let padded = attr_len + (4 - attr_len % 4) % 4;
895            pos += padded;
896        }
897        Ok(attrs)
898    }
899}
900
901/// Decode a MAPPED-ADDRESS or XOR-MAPPED-ADDRESS attribute value.
902fn decode_address_value(value: &[u8], xor: bool) -> Result<(String, u16), TraversalError> {
903    if value.len() < 8 {
904        return Err(TraversalError::StunError(
905            "address attribute too short".to_string(),
906        ));
907    }
908    // Byte 0: padding, Byte 1: family (0x01=IPv4, 0x02=IPv6)
909    let family = value[1];
910    if family != 0x01 {
911        return Err(TraversalError::StunError(format!(
912            "only IPv4 is supported (family={family:#04x})"
913        )));
914    }
915    let raw_port = u16::from_be_bytes([value[2], value[3]]);
916    let raw_octets: [u8; 4] = value[4..8]
917        .try_into()
918        .map_err(|_| TraversalError::StunError("address octet slice error".to_string()))?;
919
920    let (port, octets) = if xor {
921        let dp = raw_port ^ ((STUN_MAGIC >> 16) as u16);
922        let magic_bytes = STUN_MAGIC.to_be_bytes();
923        let do_ = [
924            raw_octets[0] ^ magic_bytes[0],
925            raw_octets[1] ^ magic_bytes[1],
926            raw_octets[2] ^ magic_bytes[2],
927            raw_octets[3] ^ magic_bytes[3],
928        ];
929        (dp, do_)
930    } else {
931        (raw_port, raw_octets)
932    };
933
934    Ok((format_ipv4(octets), port))
935}
936
937impl Default for NatTraversalManager {
938    fn default() -> Self {
939        Self::new(TraversalConfig::default())
940    }
941}
942
943// ──────────────────────────────────────────────────────────────────────────────
944// Type aliases for lib.rs re-exports (collision avoidance)
945// ──────────────────────────────────────────────────────────────────────────────
946
947/// `NatType` alias used at crate root (avoids collision with
948/// `nat_traversal::NatType`).
949pub type NtmNatType = NatType;
950
951/// `NatTraversalManager` alias used at crate root (avoids collision with
952/// `nat_traversal::NatTraversalManager`).
953pub type NtmNatTraversalManager = NatTraversalManager;
954
955// ──────────────────────────────────────────────────────────────────────────────
956// Tests
957// ──────────────────────────────────────────────────────────────────────────────
958
959#[cfg(test)]
960mod tests {
961    use super::*;
962
963    // ── helpers ────────────────────────────────────────────────────────────────
964
965    fn host(ip: &str, port: u16, prio: u32) -> CandidateAddress {
966        CandidateAddress::new(ip, port, CandidateType::Host, prio)
967    }
968
969    fn srflx(ip: &str, port: u16, prio: u32) -> CandidateAddress {
970        CandidateAddress::new(ip, port, CandidateType::ServerReflexive, prio)
971    }
972
973    fn relay(ip: &str, port: u16, prio: u32) -> CandidateAddress {
974        CandidateAddress::new(ip, port, CandidateType::Relayed, prio)
975    }
976
977    fn prflx(ip: &str, port: u16, prio: u32) -> CandidateAddress {
978        CandidateAddress::new(ip, port, CandidateType::PeerReflexive, prio)
979    }
980
981    fn default_manager() -> NatTraversalManager {
982        NatTraversalManager::new(TraversalConfig::default())
983    }
984
985    // ── 1. CandidateAddress construction ──────────────────────────────────────
986
987    #[test]
988    fn test_candidate_address_new_foundation() {
989        let c = host("1.2.3.4", 5000, 100);
990        assert_eq!(c.address, "1.2.3.4");
991        assert_eq!(c.port, 5000);
992        assert_eq!(c.priority, 100);
993        assert!(!c.foundation.is_empty());
994    }
995
996    #[test]
997    fn test_candidate_foundation_deterministic() {
998        let c1 = host("1.2.3.4", 5000, 100);
999        let c2 = host("1.2.3.4", 5000, 200); // same addr+port, different prio
1000        assert_eq!(c1.foundation, c2.foundation);
1001    }
1002
1003    #[test]
1004    fn test_candidate_foundation_differs_with_different_addr() {
1005        let c1 = host("1.2.3.4", 5000, 100);
1006        let c2 = host("1.2.3.5", 5000, 100);
1007        assert_ne!(c1.foundation, c2.foundation);
1008    }
1009
1010    #[test]
1011    fn test_candidate_foundation_differs_with_different_port() {
1012        let c1 = host("1.2.3.4", 5000, 100);
1013        let c2 = host("1.2.3.4", 5001, 100);
1014        assert_ne!(c1.foundation, c2.foundation);
1015    }
1016
1017    #[test]
1018    fn test_candidate_key() {
1019        let c = host("10.0.0.1", 4321, 50);
1020        assert_eq!(c.key(), "10.0.0.1:4321");
1021    }
1022
1023    // ── 2. add_local_candidate / add_remote_candidate ─────────────────────────
1024
1025    #[test]
1026    fn test_add_local_candidate_ok() {
1027        let mut mgr = default_manager();
1028        assert!(mgr.add_local_candidate(host("1.2.3.4", 5000, 100)).is_ok());
1029    }
1030
1031    #[test]
1032    fn test_add_remote_candidate_ok() {
1033        let mut mgr = default_manager();
1034        assert!(mgr
1035            .add_remote_candidate(srflx("5.6.7.8", 3478, 200))
1036            .is_ok());
1037    }
1038
1039    #[test]
1040    fn test_add_local_candidate_empty_address_err() {
1041        let mut mgr = default_manager();
1042        let bad = CandidateAddress {
1043            address: String::new(),
1044            port: 1234,
1045            candidate_type: CandidateType::Host,
1046            priority: 10,
1047            foundation: "abc".to_string(),
1048        };
1049        assert!(matches!(
1050            mgr.add_local_candidate(bad),
1051            Err(TraversalError::CandidateGatheringFailed(_))
1052        ));
1053    }
1054
1055    #[test]
1056    fn test_add_remote_candidate_empty_address_err() {
1057        let mut mgr = default_manager();
1058        let bad = CandidateAddress {
1059            address: String::new(),
1060            port: 1234,
1061            candidate_type: CandidateType::Host,
1062            priority: 10,
1063            foundation: "abc".to_string(),
1064        };
1065        assert!(matches!(
1066            mgr.add_remote_candidate(bad),
1067            Err(TraversalError::CandidateGatheringFailed(_))
1068        ));
1069    }
1070
1071    #[test]
1072    fn test_candidates_gathered_counter() {
1073        let mut mgr = default_manager();
1074        mgr.add_local_candidate(host("1.0.0.1", 1000, 10))
1075            .expect("test: add_local_candidate should succeed");
1076        mgr.add_local_candidate(host("1.0.0.2", 1001, 20))
1077            .expect("test: add_local_candidate should succeed");
1078        mgr.add_remote_candidate(srflx("2.0.0.1", 2000, 30))
1079            .expect("test: add_remote_candidate should succeed");
1080        assert_eq!(mgr.stats().candidates_gathered, 3);
1081    }
1082
1083    // ── 3. form_check_pairs ────────────────────────────────────────────────────
1084
1085    #[test]
1086    fn test_form_check_pairs_count() {
1087        let mut mgr = default_manager();
1088        mgr.add_local_candidate(host("1.0.0.1", 1000, 100))
1089            .expect("test: add_local_candidate should succeed");
1090        mgr.add_local_candidate(host("1.0.0.2", 1001, 90))
1091            .expect("test: add_local_candidate should succeed");
1092        mgr.add_remote_candidate(srflx("2.0.0.1", 2000, 80))
1093            .expect("test: add_remote_candidate should succeed");
1094        mgr.add_remote_candidate(srflx("2.0.0.2", 2001, 70))
1095            .expect("test: add_remote_candidate should succeed");
1096        let pairs = mgr.form_check_pairs();
1097        assert_eq!(pairs.len(), 4); // 2 × 2
1098    }
1099
1100    #[test]
1101    fn test_form_check_pairs_sorted_descending() {
1102        let mut mgr = default_manager();
1103        mgr.add_local_candidate(host("1.0.0.1", 1000, 50))
1104            .expect("test: add_local_candidate should succeed");
1105        mgr.add_local_candidate(host("1.0.0.2", 1001, 200))
1106            .expect("test: add_local_candidate should succeed");
1107        mgr.add_remote_candidate(srflx("2.0.0.1", 2000, 150))
1108            .expect("test: add_remote_candidate should succeed");
1109        let pairs = mgr.form_check_pairs();
1110        for i in 1..pairs.len() {
1111            assert!(
1112                pairs[i - 1].priority >= pairs[i].priority,
1113                "pairs not sorted at index {i}"
1114            );
1115        }
1116    }
1117
1118    #[test]
1119    fn test_form_check_pairs_priority_formula() {
1120        // G = 200, D = 150 → min=150, max=200, G>D → +1
1121        // priority = 2^32 * 150 + 2 * 200 + 1 = 644245094801
1122        let local = host("1.0.0.1", 1000, 200);
1123        let remote = srflx("2.0.0.1", 2000, 150);
1124        let pair = IcePair::new(local, remote);
1125        let expected: u64 = (1u64 << 32) * 150 + 2 * 200 + 1;
1126        assert_eq!(pair.priority, expected);
1127    }
1128
1129    #[test]
1130    fn test_form_check_pairs_priority_formula_equal() {
1131        // G == D → tie-break 0
1132        let local = host("1.0.0.1", 1000, 100);
1133        let remote = srflx("2.0.0.1", 2000, 100);
1134        let pair = IcePair::new(local, remote);
1135        let expected: u64 = (1u64 << 32) * 100 + 2 * 100;
1136        assert_eq!(pair.priority, expected);
1137    }
1138
1139    #[test]
1140    fn test_form_check_pairs_capped_to_max() {
1141        let mut mgr = NatTraversalManager::new(TraversalConfig {
1142            max_pairs: 3,
1143            ..TraversalConfig::default()
1144        });
1145        for i in 0..4u16 {
1146            mgr.add_local_candidate(host("1.0.0.1", 1000 + i, 100 + i as u32))
1147                .expect("test: add_local_candidate should succeed");
1148        }
1149        mgr.add_remote_candidate(host("2.0.0.1", 2000, 50))
1150            .expect("test: add_remote_candidate should succeed");
1151        let pairs = mgr.form_check_pairs();
1152        assert_eq!(pairs.len(), 3);
1153    }
1154
1155    #[test]
1156    fn test_form_check_pairs_empty_returns_empty() {
1157        let mut mgr = default_manager();
1158        let pairs = mgr.form_check_pairs();
1159        assert!(pairs.is_empty());
1160    }
1161
1162    #[test]
1163    fn test_form_check_pairs_new_pairs_in_waiting_state() {
1164        let mut mgr = default_manager();
1165        mgr.add_local_candidate(host("1.0.0.1", 1000, 100))
1166            .expect("test: add_local_candidate should succeed");
1167        mgr.add_remote_candidate(host("2.0.0.1", 2000, 80))
1168            .expect("test: add_remote_candidate should succeed");
1169        let pairs = mgr.form_check_pairs();
1170        assert_eq!(pairs[0].state, PairState::Waiting);
1171    }
1172
1173    // ── 4. detect_nat_type ────────────────────────────────────────────────────
1174
1175    #[test]
1176    fn test_detect_nat_type_empty() {
1177        assert_eq!(NatTraversalManager::detect_nat_type(&[]), NatType::Unknown);
1178    }
1179
1180    #[test]
1181    fn test_detect_nat_type_only_host() {
1182        let c = [host("1.2.3.4", 5000, 100)];
1183        assert_eq!(
1184            NatTraversalManager::detect_nat_type(&c),
1185            NatType::OpenInternet
1186        );
1187    }
1188
1189    #[test]
1190    fn test_detect_nat_type_single_srflx_full_cone() {
1191        let c = [host("1.2.3.4", 5000, 100), srflx("5.6.7.8", 3478, 90)];
1192        assert_eq!(NatTraversalManager::detect_nat_type(&c), NatType::FullCone);
1193    }
1194
1195    #[test]
1196    fn test_detect_nat_type_symmetric_multiple_srflx() {
1197        let c = [
1198            srflx("5.6.7.8", 3478, 90),
1199            srflx("9.10.11.12", 4444, 85), // different IP → symmetric
1200        ];
1201        assert_eq!(NatTraversalManager::detect_nat_type(&c), NatType::Symmetric);
1202    }
1203
1204    #[test]
1205    fn test_detect_nat_type_symmetric_same_ip_diff_port() {
1206        let c = [
1207            srflx("5.6.7.8", 3478, 90),
1208            srflx("5.6.7.8", 3479, 85), // same IP, different port → symmetric
1209        ];
1210        assert_eq!(NatTraversalManager::detect_nat_type(&c), NatType::Symmetric);
1211    }
1212
1213    #[test]
1214    fn test_detect_nat_type_multiple_identical_srflx() {
1215        let c = [
1216            srflx("5.6.7.8", 3478, 90),
1217            srflx("5.6.7.8", 3478, 85), // identical mapping → full cone
1218        ];
1219        assert_eq!(NatTraversalManager::detect_nat_type(&c), NatType::FullCone);
1220    }
1221
1222    #[test]
1223    fn test_detect_nat_type_relayed_only() {
1224        let c = [relay("5.6.7.8", 3478, 50)];
1225        assert_eq!(
1226            NatTraversalManager::detect_nat_type(&c),
1227            NatType::PortRestrictedCone
1228        );
1229    }
1230
1231    #[test]
1232    fn test_detect_nat_type_host_and_relayed() {
1233        let c = [host("1.2.3.4", 5000, 100), relay("5.6.7.8", 3478, 50)];
1234        // host + relayed (no srflx) → PortRestrictedCone
1235        assert_eq!(
1236            NatTraversalManager::detect_nat_type(&c),
1237            NatType::PortRestrictedCone
1238        );
1239    }
1240
1241    #[test]
1242    fn test_detect_nat_type_srflx_and_relayed_restricted_cone() {
1243        let c = [
1244            host("1.2.3.4", 5000, 100),
1245            srflx("5.6.7.8", 3478, 90),
1246            relay("5.6.7.8", 9999, 50),
1247        ];
1248        assert_eq!(
1249            NatTraversalManager::detect_nat_type(&c),
1250            NatType::RestrictedCone
1251        );
1252    }
1253
1254    // ── 5. check_pair ─────────────────────────────────────────────────────────
1255
1256    #[test]
1257    fn test_check_pair_host_host_succeeds() {
1258        let mgr = default_manager();
1259        let mut pair = IcePair::new(host("1.0.0.1", 1000, 100), host("2.0.0.1", 2000, 80));
1260        let state = mgr.check_pair(&mut pair, 0);
1261        assert_eq!(state, PairState::Succeeded);
1262        assert_eq!(pair.state, PairState::Succeeded);
1263    }
1264
1265    #[test]
1266    fn test_check_pair_srflx_srflx_succeeds() {
1267        let mgr = default_manager();
1268        let mut pair = IcePair::new(srflx("1.0.0.1", 1000, 100), srflx("2.0.0.1", 2000, 80));
1269        let state = mgr.check_pair(&mut pair, 0);
1270        assert_eq!(state, PairState::Succeeded);
1271    }
1272
1273    #[test]
1274    fn test_check_pair_relayed_host_succeeds() {
1275        let mgr = default_manager();
1276        let mut pair = IcePair::new(relay("1.0.0.1", 1000, 50), host("2.0.0.1", 2000, 80));
1277        let state = mgr.check_pair(&mut pair, 0);
1278        assert_eq!(state, PairState::Succeeded);
1279    }
1280
1281    #[test]
1282    fn test_check_pair_host_prflx_succeeds() {
1283        let mgr = default_manager();
1284        let mut pair = IcePair::new(host("1.0.0.1", 1000, 100), prflx("2.0.0.1", 2000, 90));
1285        let state = mgr.check_pair(&mut pair, 0);
1286        assert_eq!(state, PairState::Succeeded);
1287    }
1288
1289    #[test]
1290    fn test_check_pair_transitions_through_in_progress() {
1291        // We verify the method sets InProgress then Succeeded atomically
1292        // (our impl does both; the state after return should be Succeeded).
1293        let mgr = default_manager();
1294        let mut pair = IcePair::new(host("1.0.0.1", 1000, 100), host("2.0.0.1", 2000, 80));
1295        assert_eq!(pair.state, PairState::Waiting);
1296        let result = mgr.check_pair(&mut pair, 42_000);
1297        assert_eq!(result, PairState::Succeeded);
1298    }
1299
1300    #[test]
1301    fn test_check_pair_nominated_false_by_default() {
1302        let mgr = default_manager();
1303        let mut pair = IcePair::new(host("1.0.0.1", 1000, 100), host("2.0.0.1", 2000, 80));
1304        mgr.check_pair(&mut pair, 0);
1305        assert!(!pair.nominated);
1306    }
1307
1308    // ── 6. nominate_best_pair ─────────────────────────────────────────────────
1309
1310    #[test]
1311    fn test_nominate_best_pair_picks_highest_priority() {
1312        let mut mgr = default_manager();
1313        mgr.add_local_candidate(host("1.0.0.1", 1000, 200))
1314            .expect("test: add_local_candidate should succeed");
1315        mgr.add_local_candidate(host("1.0.0.2", 1001, 50))
1316            .expect("test: add_local_candidate should succeed");
1317        mgr.add_remote_candidate(host("2.0.0.1", 2000, 150))
1318            .expect("test: add_remote_candidate should succeed");
1319        mgr.form_check_pairs();
1320        for pair in &mut mgr.check_pairs {
1321            pair.state = PairState::Succeeded;
1322        }
1323        let nominated = mgr
1324            .nominate_best_pair()
1325            .expect("test: nominate_best_pair should succeed with succeeded pairs");
1326        assert!(nominated.nominated);
1327        // Highest-priority pair has local prio=200, remote prio=150
1328        assert_eq!(nominated.local.priority, 200);
1329    }
1330
1331    #[test]
1332    fn test_nominate_best_pair_no_succeeded_pairs_err() {
1333        let mut mgr = default_manager();
1334        mgr.add_local_candidate(host("1.0.0.1", 1000, 100))
1335            .expect("test: add_local_candidate should succeed");
1336        mgr.add_remote_candidate(host("2.0.0.1", 2000, 80))
1337            .expect("test: add_remote_candidate should succeed");
1338        mgr.form_check_pairs();
1339        assert!(matches!(
1340            mgr.nominate_best_pair(),
1341            Err(TraversalError::NoValidPair)
1342        ));
1343    }
1344
1345    #[test]
1346    fn test_nominate_best_pair_marks_nominated() {
1347        let mut mgr = default_manager();
1348        mgr.add_local_candidate(host("1.0.0.1", 1000, 100))
1349            .expect("test: add_local_candidate should succeed");
1350        mgr.add_remote_candidate(host("2.0.0.1", 2000, 80))
1351            .expect("test: add_remote_candidate should succeed");
1352        mgr.form_check_pairs();
1353        mgr.check_pairs[0].state = PairState::Succeeded;
1354        let pair = mgr
1355            .nominate_best_pair()
1356            .expect("test: nominate_best_pair should succeed with succeeded pairs");
1357        assert!(pair.nominated);
1358    }
1359
1360    #[test]
1361    fn test_nominate_best_pair_updates_stats() {
1362        let mut mgr = default_manager();
1363        mgr.add_local_candidate(host("1.0.0.1", 1000, 100))
1364            .expect("test: add_local_candidate should succeed");
1365        mgr.add_remote_candidate(host("2.0.0.1", 2000, 80))
1366            .expect("test: add_remote_candidate should succeed");
1367        mgr.form_check_pairs();
1368        mgr.check_pairs[0].state = PairState::Succeeded;
1369        mgr.nominate_best_pair()
1370            .expect("test: nominate_best_pair should succeed with succeeded pairs");
1371        assert!(mgr.stats().nominated_pair.is_some());
1372    }
1373
1374    // ── 7. run_checks ─────────────────────────────────────────────────────────
1375
1376    #[test]
1377    fn test_run_checks_all_waiting_checked() {
1378        let mut mgr = default_manager();
1379        mgr.add_local_candidate(host("1.0.0.1", 1000, 100))
1380            .expect("test: add_local_candidate should succeed");
1381        mgr.add_remote_candidate(host("2.0.0.1", 2000, 80))
1382            .expect("test: add_remote_candidate should succeed");
1383        mgr.form_check_pairs();
1384        let results = mgr.run_checks(0);
1385        assert_eq!(results.len(), 1);
1386    }
1387
1388    #[test]
1389    fn test_run_checks_returns_pair_key_and_state() {
1390        let mut mgr = default_manager();
1391        mgr.add_local_candidate(host("1.0.0.1", 1000, 100))
1392            .expect("test: add_local_candidate should succeed");
1393        mgr.add_remote_candidate(host("2.0.0.1", 2000, 80))
1394            .expect("test: add_remote_candidate should succeed");
1395        mgr.form_check_pairs();
1396        let results = mgr.run_checks(0);
1397        assert!(!results[0].0.is_empty());
1398        assert_eq!(results[0].1, PairState::Succeeded);
1399    }
1400
1401    #[test]
1402    fn test_run_checks_skips_frozen_pairs() {
1403        let mut mgr = default_manager();
1404        mgr.add_local_candidate(host("1.0.0.1", 1000, 100))
1405            .expect("test: add_local_candidate should succeed");
1406        mgr.add_remote_candidate(host("2.0.0.1", 2000, 80))
1407            .expect("test: add_remote_candidate should succeed");
1408        mgr.form_check_pairs();
1409        mgr.check_pairs[0].state = PairState::Frozen;
1410        let results = mgr.run_checks(0);
1411        assert!(results.is_empty());
1412    }
1413
1414    #[test]
1415    fn test_run_checks_skips_succeeded_pairs() {
1416        let mut mgr = default_manager();
1417        mgr.add_local_candidate(host("1.0.0.1", 1000, 100))
1418            .expect("test: add_local_candidate should succeed");
1419        mgr.add_remote_candidate(host("2.0.0.1", 2000, 80))
1420            .expect("test: add_remote_candidate should succeed");
1421        mgr.form_check_pairs();
1422        mgr.check_pairs[0].state = PairState::Succeeded;
1423        let results = mgr.run_checks(0);
1424        assert!(results.is_empty());
1425    }
1426
1427    #[test]
1428    fn test_run_checks_updates_stats_succeeded() {
1429        let mut mgr = default_manager();
1430        mgr.add_local_candidate(host("1.0.0.1", 1000, 100))
1431            .expect("test: add_local_candidate should succeed");
1432        mgr.add_remote_candidate(host("2.0.0.1", 2000, 80))
1433            .expect("test: add_remote_candidate should succeed");
1434        mgr.form_check_pairs();
1435        mgr.run_checks(0);
1436        let s = mgr.stats();
1437        assert_eq!(s.pairs_checked, 1);
1438        assert_eq!(s.pairs_succeeded, 1);
1439    }
1440
1441    #[test]
1442    fn test_run_checks_multiple_pairs() {
1443        let mut mgr = default_manager();
1444        mgr.add_local_candidate(host("1.0.0.1", 1000, 100))
1445            .expect("test: add_local_candidate should succeed");
1446        mgr.add_remote_candidate(host("2.0.0.1", 2000, 80))
1447            .expect("test: add_remote_candidate should succeed");
1448        mgr.add_remote_candidate(host("2.0.0.2", 2001, 70))
1449            .expect("test: add_remote_candidate should succeed");
1450        mgr.form_check_pairs();
1451        let results = mgr.run_checks(0);
1452        assert_eq!(results.len(), 2);
1453    }
1454
1455    // ── 8. STUN encode / decode round-trip ────────────────────────────────────
1456
1457    #[test]
1458    fn test_stun_binding_request_round_trip() {
1459        let mgr = default_manager();
1460        let msg = StunMessage::new(StunMessageType::BindingRequest, 0x123456);
1461        let encoded = mgr.encode_stun_message(&msg);
1462        let decoded = mgr
1463            .decode_stun_message(&encoded)
1464            .expect("test: decode_stun_message should succeed for valid binding request");
1465        assert_eq!(decoded.msg_type, StunMessageType::BindingRequest);
1466        assert_eq!(decoded.transaction_id, msg.transaction_id);
1467    }
1468
1469    #[test]
1470    fn test_stun_binding_response_round_trip() {
1471        let mgr = default_manager();
1472        let mut msg = StunMessage::new(StunMessageType::BindingResponse, 0xABCD);
1473        msg.attributes
1474            .push(StunAttribute::MappedAddress("1.2.3.4".to_string(), 5000));
1475        let encoded = mgr.encode_stun_message(&msg);
1476        let decoded = mgr
1477            .decode_stun_message(&encoded)
1478            .expect("test: decode_stun_message should succeed for valid binding response");
1479        assert_eq!(decoded.msg_type, StunMessageType::BindingResponse);
1480        assert_eq!(decoded.attributes.len(), 1);
1481        if let StunAttribute::MappedAddress(addr, port) = &decoded.attributes[0] {
1482            assert_eq!(addr, "1.2.3.4");
1483            assert_eq!(*port, 5000);
1484        } else {
1485            panic!("expected MappedAddress");
1486        }
1487    }
1488
1489    #[test]
1490    fn test_stun_xor_mapped_address_round_trip() {
1491        let mgr = default_manager();
1492        let mut msg = StunMessage::new(StunMessageType::BindingResponse, 0xFFFF);
1493        msg.attributes.push(StunAttribute::XorMappedAddress(
1494            "192.168.1.1".to_string(),
1495            54321,
1496        ));
1497        let encoded = mgr.encode_stun_message(&msg);
1498        let decoded = mgr
1499            .decode_stun_message(&encoded)
1500            .expect("test: decode_stun_message should succeed for xor mapped address");
1501        if let StunAttribute::XorMappedAddress(addr, port) = &decoded.attributes[0] {
1502            assert_eq!(addr, "192.168.1.1");
1503            assert_eq!(*port, 54321);
1504        } else {
1505            panic!("expected XorMappedAddress");
1506        }
1507    }
1508
1509    #[test]
1510    fn test_stun_username_round_trip() {
1511        let mgr = default_manager();
1512        let mut msg = StunMessage::new(StunMessageType::BindingRequest, 1);
1513        msg.attributes
1514            .push(StunAttribute::Username("alice:bob".to_string()));
1515        let encoded = mgr.encode_stun_message(&msg);
1516        let decoded = mgr
1517            .decode_stun_message(&encoded)
1518            .expect("test: decode_stun_message should succeed for username attribute");
1519        if let StunAttribute::Username(name) = &decoded.attributes[0] {
1520            assert_eq!(name, "alice:bob");
1521        } else {
1522            panic!("expected Username");
1523        }
1524    }
1525
1526    #[test]
1527    fn test_stun_realm_round_trip() {
1528        let mgr = default_manager();
1529        let mut msg = StunMessage::new(StunMessageType::AllocateRequest, 2);
1530        msg.attributes
1531            .push(StunAttribute::Realm("example.com".to_string()));
1532        let encoded = mgr.encode_stun_message(&msg);
1533        let decoded = mgr
1534            .decode_stun_message(&encoded)
1535            .expect("test: decode_stun_message should succeed for realm attribute");
1536        if let StunAttribute::Realm(r) = &decoded.attributes[0] {
1537            assert_eq!(r, "example.com");
1538        } else {
1539            panic!("expected Realm");
1540        }
1541    }
1542
1543    #[test]
1544    fn test_stun_error_code_round_trip() {
1545        let mgr = default_manager();
1546        let mut msg = StunMessage::new(StunMessageType::BindingError, 3);
1547        msg.attributes
1548            .push(StunAttribute::ErrorCode(401, "Unauthorized".to_string()));
1549        let encoded = mgr.encode_stun_message(&msg);
1550        let decoded = mgr
1551            .decode_stun_message(&encoded)
1552            .expect("test: decode_stun_message should succeed for error code attribute");
1553        if let StunAttribute::ErrorCode(code, reason) = &decoded.attributes[0] {
1554            assert_eq!(*code, 401);
1555            assert_eq!(reason, "Unauthorized");
1556        } else {
1557            panic!("expected ErrorCode");
1558        }
1559    }
1560
1561    #[test]
1562    fn test_stun_fingerprint_round_trip() {
1563        let mgr = default_manager();
1564        let mut msg = StunMessage::new(StunMessageType::BindingRequest, 4);
1565        msg.attributes.push(StunAttribute::Fingerprint(0xDEAD_BEEF));
1566        let encoded = mgr.encode_stun_message(&msg);
1567        let decoded = mgr
1568            .decode_stun_message(&encoded)
1569            .expect("test: decode_stun_message should succeed for fingerprint attribute");
1570        if let StunAttribute::Fingerprint(crc) = decoded.attributes[0] {
1571            assert_eq!(crc, 0xDEAD_BEEF);
1572        } else {
1573            panic!("expected Fingerprint");
1574        }
1575    }
1576
1577    #[test]
1578    fn test_stun_lifetime_round_trip() {
1579        let mgr = default_manager();
1580        let mut msg = StunMessage::new(StunMessageType::AllocateResponse, 5);
1581        msg.attributes.push(StunAttribute::Lifetime(600));
1582        let encoded = mgr.encode_stun_message(&msg);
1583        let decoded = mgr
1584            .decode_stun_message(&encoded)
1585            .expect("test: decode_stun_message should succeed for lifetime attribute");
1586        if let StunAttribute::Lifetime(secs) = decoded.attributes[0] {
1587            assert_eq!(secs, 600);
1588        } else {
1589            panic!("expected Lifetime");
1590        }
1591    }
1592
1593    #[test]
1594    fn test_stun_multiple_attributes_round_trip() {
1595        let mgr = default_manager();
1596        let mut msg = StunMessage::new(StunMessageType::BindingResponse, 6);
1597        msg.attributes
1598            .push(StunAttribute::MappedAddress("10.0.0.1".to_string(), 4000));
1599        msg.attributes
1600            .push(StunAttribute::Username("u1:u2".to_string()));
1601        msg.attributes.push(StunAttribute::Fingerprint(42));
1602        let encoded = mgr.encode_stun_message(&msg);
1603        let decoded = mgr
1604            .decode_stun_message(&encoded)
1605            .expect("test: decode_stun_message should succeed for multiple attributes");
1606        assert_eq!(decoded.attributes.len(), 3);
1607    }
1608
1609    // ── 9. decode error paths ─────────────────────────────────────────────────
1610
1611    #[test]
1612    fn test_decode_stun_too_short() {
1613        let mgr = default_manager();
1614        let result = mgr.decode_stun_message(&[0u8; 10]);
1615        assert!(matches!(result, Err(TraversalError::StunError(_))));
1616    }
1617
1618    #[test]
1619    fn test_decode_stun_wrong_magic() {
1620        let mgr = default_manager();
1621        let mut buf = vec![0u8; 20];
1622        // Correct type bytes
1623        buf[0] = 0x00;
1624        buf[1] = 0x01;
1625        // Wrong magic cookie
1626        buf[4] = 0xFF;
1627        buf[5] = 0xFF;
1628        buf[6] = 0xFF;
1629        buf[7] = 0xFF;
1630        let result = mgr.decode_stun_message(&buf);
1631        assert!(matches!(result, Err(TraversalError::StunError(_))));
1632    }
1633
1634    #[test]
1635    fn test_decode_stun_unknown_type() {
1636        let mgr = default_manager();
1637        let mut buf = vec![0u8; 20];
1638        // Unknown message type 0x00FF
1639        buf[0] = 0x00;
1640        buf[1] = 0xFF;
1641        // Correct magic
1642        let magic = STUN_MAGIC.to_be_bytes();
1643        buf[4] = magic[0];
1644        buf[5] = magic[1];
1645        buf[6] = magic[2];
1646        buf[7] = magic[3];
1647        let result = mgr.decode_stun_message(&buf);
1648        assert!(matches!(result, Err(TraversalError::StunError(_))));
1649    }
1650
1651    #[test]
1652    fn test_decode_stun_truncated_attributes() {
1653        let mgr = default_manager();
1654        let mut buf = vec![0u8; 20];
1655        buf[0] = 0x00;
1656        buf[1] = 0x01;
1657        let magic = STUN_MAGIC.to_be_bytes();
1658        buf[4] = magic[0];
1659        buf[5] = magic[1];
1660        buf[6] = magic[2];
1661        buf[7] = magic[3];
1662        // Declare 10 bytes of attributes but provide none
1663        buf[2] = 0x00;
1664        buf[3] = 0x0A;
1665        let result = mgr.decode_stun_message(&buf);
1666        assert!(matches!(result, Err(TraversalError::StunError(_))));
1667    }
1668
1669    // ── 10. stats ─────────────────────────────────────────────────────────────
1670
1671    #[test]
1672    fn test_stats_initial_state() {
1673        let mgr = default_manager();
1674        let s = mgr.stats();
1675        assert_eq!(s.candidates_gathered, 0);
1676        assert_eq!(s.pairs_checked, 0);
1677        assert_eq!(s.pairs_succeeded, 0);
1678        assert_eq!(s.pairs_failed, 0);
1679        assert!(s.nominated_pair.is_none());
1680    }
1681
1682    #[test]
1683    fn test_stats_nat_type_string_open_internet() {
1684        let mut mgr = default_manager();
1685        mgr.add_local_candidate(host("1.2.3.4", 5000, 100))
1686            .expect("test: add_local_candidate should succeed");
1687        let s = mgr.stats();
1688        assert!(s.nat_type.contains("OpenInternet"));
1689    }
1690
1691    #[test]
1692    fn test_stats_nat_type_string_unknown_no_candidates() {
1693        let mgr = default_manager();
1694        let s = mgr.stats();
1695        assert!(s.nat_type.contains("Unknown"));
1696    }
1697
1698    #[test]
1699    fn test_stats_after_full_workflow() {
1700        let mut mgr = default_manager();
1701        mgr.add_local_candidate(host("1.0.0.1", 1000, 100))
1702            .expect("test: add_local_candidate should succeed");
1703        mgr.add_remote_candidate(host("2.0.0.1", 2000, 80))
1704            .expect("test: add_remote_candidate should succeed");
1705        mgr.form_check_pairs();
1706        mgr.run_checks(0);
1707        mgr.nominate_best_pair()
1708            .expect("test: nominate_best_pair should succeed after checks");
1709        let s = mgr.stats();
1710        assert_eq!(s.pairs_checked, 1);
1711        assert_eq!(s.pairs_succeeded, 1);
1712        assert!(s.nominated_pair.is_some());
1713    }
1714
1715    // ── 11. error type coverage ───────────────────────────────────────────────
1716
1717    #[test]
1718    fn test_traversal_error_display_no_valid_pair() {
1719        let e = TraversalError::NoValidPair;
1720        assert!(e.to_string().contains("no valid ICE pair"));
1721    }
1722
1723    #[test]
1724    fn test_traversal_error_display_gathering_failed() {
1725        let e = TraversalError::CandidateGatheringFailed("oops".to_string());
1726        assert!(e.to_string().contains("oops"));
1727    }
1728
1729    #[test]
1730    fn test_traversal_error_display_stun_error() {
1731        let e = TraversalError::StunError("bad magic".to_string());
1732        assert!(e.to_string().contains("bad magic"));
1733    }
1734
1735    #[test]
1736    fn test_traversal_error_display_turn_failed() {
1737        let e = TraversalError::TurnAllocationFailed("quota exceeded".to_string());
1738        assert!(e.to_string().contains("quota exceeded"));
1739    }
1740
1741    #[test]
1742    fn test_traversal_error_display_checklist_exhausted() {
1743        let e = TraversalError::ChecklistExhausted;
1744        assert!(e.to_string().contains("exhausted"));
1745    }
1746
1747    // ── 12. xorshift64 / fnv1a_64 ────────────────────────────────────────────
1748
1749    #[test]
1750    fn test_xorshift64_non_zero_output() {
1751        let mut s = 0xDEAD_BEEF_1234_5678u64;
1752        let v = xorshift64(&mut s);
1753        assert_ne!(v, 0);
1754        assert_ne!(s, 0xDEAD_BEEF_1234_5678u64);
1755    }
1756
1757    #[test]
1758    fn test_xorshift64_deterministic() {
1759        let mut s1 = 12345u64;
1760        let mut s2 = 12345u64;
1761        assert_eq!(xorshift64(&mut s1), xorshift64(&mut s2));
1762    }
1763
1764    #[test]
1765    fn test_fnv1a_64_empty() {
1766        // Empty slice returns the offset basis.
1767        assert_eq!(fnv1a_64(&[]), 14_695_981_039_346_656_037u64);
1768    }
1769
1770    #[test]
1771    fn test_fnv1a_64_known_value() {
1772        // "hello" should produce a known hash.
1773        let h = fnv1a_64(b"hello");
1774        assert_ne!(h, 14_695_981_039_346_656_037u64);
1775    }
1776
1777    #[test]
1778    fn test_fnv1a_64_differs_for_different_input() {
1779        assert_ne!(fnv1a_64(b"abc"), fnv1a_64(b"def"));
1780    }
1781
1782    // ── 13. IcePair helpers ───────────────────────────────────────────────────
1783
1784    #[test]
1785    fn test_ice_pair_key_format() {
1786        let pair = IcePair::new(host("1.0.0.1", 1000, 100), host("2.0.0.1", 2000, 80));
1787        assert_eq!(pair.key(), "1.0.0.1:1000 -> 2.0.0.1:2000");
1788    }
1789
1790    #[test]
1791    fn test_ice_pair_initial_state_waiting() {
1792        let pair = IcePair::new(host("1.0.0.1", 1000, 100), srflx("2.0.0.1", 2000, 90));
1793        assert_eq!(pair.state, PairState::Waiting);
1794        assert!(!pair.nominated);
1795    }
1796
1797    // ── 14. StunMessage constructor ───────────────────────────────────────────
1798
1799    #[test]
1800    fn test_stun_message_new_transaction_id_non_zero() {
1801        let msg = StunMessage::new(StunMessageType::BindingRequest, 999);
1802        assert!(msg.transaction_id.iter().any(|&b| b != 0));
1803    }
1804
1805    #[test]
1806    fn test_stun_message_new_zero_seed_fallback() {
1807        // seed=0 → use fallback seed, should still produce non-zero tx ID
1808        let msg = StunMessage::new(StunMessageType::BindingRequest, 0);
1809        assert!(msg.transaction_id.iter().any(|&b| b != 0));
1810    }
1811
1812    #[test]
1813    fn test_stun_message_all_types_encode_decode() {
1814        let mgr = default_manager();
1815        let types = [
1816            StunMessageType::BindingRequest,
1817            StunMessageType::BindingResponse,
1818            StunMessageType::BindingError,
1819            StunMessageType::AllocateRequest,
1820            StunMessageType::AllocateResponse,
1821        ];
1822        for t in &types {
1823            let msg = StunMessage::new(t.clone(), 1);
1824            let encoded = mgr.encode_stun_message(&msg);
1825            let decoded = mgr
1826                .decode_stun_message(&encoded)
1827                .expect("test: decode_stun_message should succeed for all stun message types");
1828            assert_eq!(&decoded.msg_type, t);
1829        }
1830    }
1831
1832    // ── 15. Full end-to-end workflow ──────────────────────────────────────────
1833
1834    #[test]
1835    fn test_full_ice_workflow() {
1836        let mut mgr = NatTraversalManager::new(TraversalConfig {
1837            max_pairs: 50,
1838            ..TraversalConfig::default()
1839        });
1840
1841        // Gather local candidates
1842        mgr.add_local_candidate(host("192.168.1.1", 5000, 2130706431))
1843            .expect("test: add_local_candidate should succeed");
1844        mgr.add_local_candidate(srflx("203.0.113.1", 5001, 1694498815))
1845            .expect("test: add_local_candidate should succeed");
1846
1847        // Gather remote candidates
1848        mgr.add_remote_candidate(host("10.0.0.1", 6000, 2130706431))
1849            .expect("test: add_remote_candidate should succeed");
1850        mgr.add_remote_candidate(srflx("198.51.100.1", 6001, 1694498815))
1851            .expect("test: add_remote_candidate should succeed");
1852
1853        // Form pairs
1854        let pairs = mgr.form_check_pairs();
1855        assert_eq!(pairs.len(), 4);
1856
1857        // Run all checks
1858        let results = mgr.run_checks(1_000_000);
1859        assert_eq!(results.len(), 4);
1860        assert!(results.iter().all(|(_, s)| *s == PairState::Succeeded));
1861
1862        // Nominate best
1863        let best = mgr
1864            .nominate_best_pair()
1865            .expect("test: nominate_best_pair should succeed in full workflow");
1866        assert!(best.nominated);
1867        assert_eq!(best.state, PairState::Succeeded);
1868
1869        // Stats
1870        let s = mgr.stats();
1871        assert_eq!(s.pairs_checked, 4);
1872        assert_eq!(s.pairs_succeeded, 4);
1873        assert!(s.nominated_pair.is_some());
1874    }
1875}