Skip to main content

sipx_media/ice/
checklist.rs

1//! Checklists: pairing, ordering, pruning and pair state (RFC 8445 §6; [spec] §6).
2//!
3//! One checklist per data stream, and the ordered list of them is the "checklist set" §6.1.2.6
4//! computes initial states over. sipx builds one checklist per [`Agent`](super::Agent) because a
5//! media session is one data stream — but the set is a set, not a special case of one, because
6//! §6.1.2.6's rule is *about* the set: a foundation already unfrozen in one checklist is not
7//! unfrozen again in another, which is a sentence that has no meaning with a single checklist and
8//! is the difference between checking a path once and checking it three times.
9//!
10//! [spec]: https://github.com/codewandler/sipx/blob/main/docs/specs/ice.md
11
12use std::collections::VecDeque;
13use std::net::{IpAddr, SocketAddr};
14
15use sipx_sdp::ice::{CandidateType, ComponentId};
16
17use super::candidate::{
18    LocalCandidate, LocalId, PairFoundation, RemoteCandidate, RemoteId, find_local, find_remote,
19    pair_priority,
20};
21
22/// Which end decides (§6.1.1).
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
24pub enum Role {
25    /// Responsible for nominating the pairs that become the selected pairs.
26    Controlling,
27    /// Answers checks and follows the controlling agent's nomination.
28    Controlled,
29}
30
31impl Role {
32    /// §6.1.1's role determination, for an agent that is always full ([spec] §12).
33    ///
34    /// Both full: the initiating agent controls. Full against lite: the full agent controls,
35    /// unconditionally — which is why the peer's `a=ice-lite` is an input here and not a detail
36    /// for the driver.
37    ///
38    /// "The offerer controls" is the right answer and the wrong mechanism: two agents can both
39    /// believe they offered — third-party call control, glare, a re-INVITE crossing — and two
40    /// controlling agents never converge, because neither will accept the other's nomination.
41    /// §7.3.1.1 is what repairs that, and it is why this function is not the last word on the
42    /// role.
43    ///
44    /// [spec]: https://github.com/codewandler/sipx/blob/main/docs/specs/ice.md
45    #[must_use]
46    pub const fn determine(offerer: bool, remote_lite: bool) -> Self {
47        if remote_lite || offerer {
48            Self::Controlling
49        } else {
50            Self::Controlled
51        }
52    }
53
54    /// The other role. A 487 switches to it (§7.2.5.1), as does the losing side of §7.3.1.1.
55    #[must_use]
56    pub const fn opposite(self) -> Self {
57        match self {
58            Self::Controlling => Self::Controlled,
59            Self::Controlled => Self::Controlling,
60        }
61    }
62
63    /// Whether this is the controlling role.
64    #[must_use]
65    pub const fn is_controlling(self) -> bool {
66        matches!(self, Self::Controlling)
67    }
68}
69
70/// A pair's state (§6.1.2.6).
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
72pub enum PairState {
73    /// No check sent, and none may be sent until the pair is unfrozen.
74    Frozen,
75    /// No check sent, but the pair is not Frozen.
76    Waiting,
77    /// A check has been sent and the transaction is in progress.
78    InProgress,
79    /// A check was sent and produced a successful result.
80    Succeeded,
81    /// A check was sent and failed, or timed out.
82    Failed,
83}
84
85impl PairState {
86    /// Whether the pair has finished — §7.2.5.4 asks whether every pair is in one of these two.
87    #[must_use]
88    pub const fn is_final(self) -> bool {
89        matches!(self, Self::Succeeded | Self::Failed)
90    }
91}
92
93/// A checklist's state (§6.1.2.1).
94///
95/// `Completed` and not `Succeeded`: §6.1.2.1 names the states and §7.2.5.4 calls the same state
96/// Succeeded in passing. The name that appears in the state definitions is the one used here.
97#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
98pub enum ChecklistState {
99    /// Neither Completed nor Failed yet. Checklists start here.
100    Running,
101    /// There is a nominated pair for every component of the data stream.
102    Completed,
103    /// Every pair is Failed or Succeeded and some component has no valid pair.
104    Failed,
105}
106
107/// A pair's identity, stable across sorting, pruning and removal.
108///
109/// Positions are not: §6.1.2.3 re-sorts every checklist on a role change, §8.1.2 removes pairs
110/// once a component is nominated, and a triggered-check queue holding indices into a list that
111/// does both would name a different pair after either.
112#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
113pub struct PairId(pub u32);
114
115/// Allocates [`PairId`]s that are unique for the lifetime of an agent.
116#[derive(Debug, Default)]
117pub struct PairIds(u32);
118
119impl PairIds {
120    /// The next identity.
121    pub fn allocate(&mut self) -> PairId {
122        let id = PairId(self.0);
123        self.0 = self.0.saturating_add(1);
124        id
125    }
126}
127
128/// One entry in a checklist (§6.1.2.2, figure 5).
129#[derive(Debug, Clone, PartialEq, Eq)]
130pub struct CandidatePair {
131    /// Its identity.
132    pub id: PairId,
133    /// The local candidate, by index into the agent's table.
134    pub local: LocalId,
135    /// The remote candidate, by index into the agent's table.
136    pub remote: RemoteId,
137    /// The component both candidates are for.
138    pub component: ComponentId,
139    /// The combination of the two candidates' foundations.
140    pub foundation: PairFoundation,
141    /// §6.1.2.3's pair priority. Recomputed on every role change, because `G` and `D` swap.
142    pub priority: u64,
143    /// Its state.
144    pub state: PairState,
145    /// Whether a check on this pair carried `USE-CANDIDATE` and succeeded (§7.2.5.3.4).
146    pub nominated: bool,
147}
148
149impl CandidatePair {
150    /// Whether §6.1.2.5 may discard this pair.
151    ///
152    /// A pair with a check in flight or a check that succeeded is holding state outside the
153    /// checklist — a transaction, or an entry in the valid list — and removing it silently would
154    /// lose that rather than bound anything.
155    #[must_use]
156    pub const fn is_discardable(&self) -> bool {
157        matches!(
158            self.state,
159            PairState::Frozen | PairState::Waiting | PairState::Failed
160        ) && !self.nominated
161    }
162}
163
164/// A pair in a valid list (§7.2.5.3.2).
165///
166/// Not a [`CandidatePair`], and deliberately so: §7.2.5.3.2 builds it from the *mapped address*
167/// of the response and the address the request was sent to, so "it will be very common that the
168/// valid pair will not be in any checklist" — its local candidate is the reflexive address a NAT
169/// showed us, and every checklist pair had its reflexive locals replaced by their bases.
170#[derive(Debug, Clone, PartialEq, Eq)]
171pub struct ValidPair {
172    /// The component it serves.
173    pub component: ComponentId,
174    /// The local candidate the check went out from — which is what the driver must send on.
175    pub local: LocalId,
176    /// The remote address the check was sent to.
177    pub remote: SocketAddr,
178    /// Its pair priority (§6.1.2.3).
179    pub priority: u64,
180    /// Whether it has been nominated (§7.2.5.3.4, §7.3.1.5).
181    pub nominated: bool,
182    /// The checklist pair whose check produced it.
183    pub generated_by: PairId,
184}
185
186/// One data stream's checklist, its triggered-check queue and its valid list.
187#[derive(Debug, Default)]
188pub struct Checklist {
189    pairs: Vec<CandidatePair>,
190    state: Option<ChecklistState>,
191    triggered: VecDeque<PairId>,
192    valid: Vec<ValidPair>,
193}
194
195impl Checklist {
196    /// A checklist over these pairs, before §6.1.2.6 has set any state.
197    #[must_use]
198    pub fn new(pairs: Vec<CandidatePair>) -> Self {
199        Self {
200            pairs,
201            state: None,
202            triggered: VecDeque::new(),
203            valid: Vec::new(),
204        }
205    }
206
207    /// The pairs, in checklist order.
208    #[must_use]
209    pub fn pairs(&self) -> &[CandidatePair] {
210        &self.pairs
211    }
212
213    /// The pair with this identity.
214    #[must_use]
215    pub fn pair(&self, id: PairId) -> Option<&CandidatePair> {
216        self.pairs.iter().find(|pair| pair.id == id)
217    }
218
219    /// The pair with this identity, mutably.
220    pub fn pair_mut(&mut self, id: PairId) -> Option<&mut CandidatePair> {
221        self.pairs.iter_mut().find(|pair| pair.id == id)
222    }
223
224    /// The checklist's state. `Running` until §6.1.2.6 has run.
225    #[must_use]
226    pub fn state(&self) -> ChecklistState {
227        self.state.unwrap_or(ChecklistState::Running)
228    }
229
230    /// Set the checklist's state (§7.2.5.4, §8.1.2).
231    pub fn set_state(&mut self, state: ChecklistState) {
232        self.state = Some(state);
233    }
234
235    /// The valid list (§7.2.5.3.2).
236    #[must_use]
237    pub fn valid(&self) -> &[ValidPair] {
238        &self.valid
239    }
240
241    /// Add a valid pair, or update the one already there for the same local and remote.
242    ///
243    /// Returns whether it was new, which is what arms [spec] §8's `Tn`: the stopping criterion
244    /// counts from the *first* valid pair, and a retransmitted check that revalidates a pair
245    /// already in the list must not restart it.
246    ///
247    /// [spec]: https://github.com/codewandler/sipx/blob/main/docs/specs/ice.md
248    pub fn add_valid(&mut self, pair: ValidPair) -> bool {
249        if let Some(existing) = self
250            .valid
251            .iter_mut()
252            .find(|known| known.local == pair.local && known.remote == pair.remote)
253        {
254            existing.nominated |= pair.nominated;
255            return false;
256        }
257        self.valid.push(pair);
258        true
259    }
260
261    /// Mark the valid pair a check produced as nominated (§7.2.5.3.4).
262    pub fn nominate_valid(&mut self, generated_by: PairId) {
263        for valid in &mut self.valid {
264            if valid.generated_by == generated_by {
265                valid.nominated = true;
266            }
267        }
268    }
269
270    /// Drop a valid pair whose nominated check later failed (§7.2.5.3.4).
271    pub fn remove_valid(&mut self, generated_by: PairId) {
272        self.valid
273            .retain(|valid| valid.generated_by != generated_by);
274    }
275
276    /// Enqueue a triggered check (§7.3.1.4). A pair already queued is not queued twice.
277    pub fn trigger(&mut self, id: PairId) {
278        if !self.triggered.contains(&id) {
279            self.triggered.push_back(id);
280        }
281    }
282
283    /// Take the next triggered check. §6.1.4.1's queue is FIFO, and §6.1.4.2 empties it before it
284    /// looks at any `Waiting` pair — which is what makes ICE converge in the time it takes a
285    /// peer's check to arrive rather than in checklist order.
286    pub fn take_triggered(&mut self) -> Option<PairId> {
287        self.triggered.pop_front()
288    }
289
290    /// Whether anything at all is queued for a triggered check.
291    #[must_use]
292    pub fn has_triggered(&self) -> bool {
293        !self.triggered.is_empty()
294    }
295
296    /// Whether a triggered check is queued for this pair.
297    #[must_use]
298    pub fn is_triggered(&self, id: PairId) -> bool {
299        self.triggered.contains(&id)
300    }
301
302    /// The pair joining these two candidates, if the checklist has one.
303    #[must_use]
304    pub fn find(&self, local: LocalId, remote: RemoteId) -> Option<PairId> {
305        self.pairs
306            .iter()
307            .find(|pair| pair.local == local && pair.remote == remote)
308            .map(|pair| pair.id)
309    }
310
311    /// Insert a pair §7.3.1.4 built from an inbound check, "based on its priority".
312    pub fn insert(&mut self, pair: CandidatePair) {
313        let position = self
314            .pairs
315            .iter()
316            .position(|existing| existing.priority < pair.priority)
317            .unwrap_or(self.pairs.len());
318        self.pairs.insert(position, pair);
319    }
320
321    /// Sort into decreasing pair priority (§6.1.2.3).
322    ///
323    /// Stable, so that equal priorities keep the order they were formed in: §6.1.2.3 says ties
324    /// are ordered arbitrarily, and a test that asserts on a checklist needs the same arbitrary
325    /// answer twice.
326    pub fn sort(&mut self) {
327        self.pairs
328            .sort_by_key(|pair| std::cmp::Reverse(pair.priority));
329    }
330
331    /// The components this checklist has pairs for.
332    #[must_use]
333    pub fn components(&self) -> Vec<ComponentId> {
334        let mut components: Vec<ComponentId> =
335            self.pairs.iter().map(|pair| pair.component).collect();
336        components.sort_unstable();
337        components.dedup();
338        components
339    }
340
341    /// Remove every pair for a component but the one just nominated (§8.1.2).
342    pub fn keep_only_nominated(&mut self, component: ComponentId, keep: PairId) {
343        self.pairs
344            .retain(|pair| pair.component != component || pair.id == keep);
345        let live: Vec<PairId> = self.pairs.iter().map(|pair| pair.id).collect();
346        self.triggered.retain(|id| live.contains(id));
347    }
348}
349
350/// The ordered set of checklists, one per data stream (§6.1.2, §6.1.2.6).
351#[derive(Debug, Default)]
352pub struct ChecklistSet {
353    checklists: Vec<Checklist>,
354    next: usize,
355}
356
357impl ChecklistSet {
358    /// An empty set.
359    #[must_use]
360    pub fn new() -> Self {
361        Self::default()
362    }
363
364    /// Append a checklist. The order is the "usage-defined checklist set order" §6.1.2.6 unfreezes
365    /// against.
366    pub fn push(&mut self, checklist: Checklist) {
367        self.checklists.push(checklist);
368    }
369
370    /// The checklists, in order.
371    #[must_use]
372    pub fn checklists(&self) -> &[Checklist] {
373        &self.checklists
374    }
375
376    /// The checklists, mutably.
377    pub fn checklists_mut(&mut self) -> &mut [Checklist] {
378        &mut self.checklists
379    }
380
381    /// Whether the set holds no checklists at all.
382    #[must_use]
383    pub fn is_empty(&self) -> bool {
384        self.checklists.is_empty()
385    }
386
387    /// The pair with this identity, wherever it is.
388    #[must_use]
389    pub fn pair(&self, id: PairId) -> Option<&CandidatePair> {
390        self.checklists.iter().find_map(|list| list.pair(id))
391    }
392
393    /// The pair with this identity, mutably.
394    pub fn pair_mut(&mut self, id: PairId) -> Option<&mut CandidatePair> {
395        self.checklists
396            .iter_mut()
397            .find_map(|list| list.pair_mut(id))
398    }
399
400    /// Which checklist holds this pair.
401    #[must_use]
402    pub fn checklist_of(&self, id: PairId) -> Option<usize> {
403        self.checklists
404            .iter()
405            .position(|list| list.pair(id).is_some())
406    }
407
408    /// `N` in §14.3's RTO: the total number of connectivity checks to be performed.
409    #[must_use]
410    pub fn total_pairs(&self) -> usize {
411        self.checklists.iter().map(|list| list.pairs.len()).sum()
412    }
413
414    /// `Num-Waiting + Num-In-Progress` in §14.3's RTO, across the set.
415    #[must_use]
416    pub fn outstanding(&self) -> usize {
417        self.checklists
418            .iter()
419            .flat_map(|list| list.pairs.iter())
420            .filter(|pair| matches!(pair.state, PairState::Waiting | PairState::InProgress))
421            .count()
422    }
423
424    /// §6.1.2.5: discard the lowest-priority pairs until the set holds at most `limit` of them.
425    ///
426    /// The limit is an attack control and not tidiness — it bounds how many packets a hostile
427    /// candidate list can make sipx send — which is why §6.1.2.5 makes it a MUST and makes it
428    /// configurable. The discarding is spread across checklists ("SHOULD be done evenly so that
429    /// the number of candidate pairs in each checklist is reduced the same amount") by always
430    /// taking from the longest checklist.
431    ///
432    /// **Only pairs that have not been checked, or have finished failing, are discardable.**
433    /// §6.1.2.5 runs at checklist formation, when every pair is Frozen; this runs again every
434    /// time §7.3.1.4 inserts a pair, because that is the path a peer can drive. Discarding an
435    /// `In-Progress` pair would orphan its transaction, and discarding a `Succeeded` one would
436    /// take a working path out of the valid list — so a set already full of live pairs stops
437    /// shrinking rather than tearing itself down, and the limit then binds by refusing growth.
438    pub fn limit(&mut self, limit: usize) {
439        while self.total_pairs() > limit {
440            let longest = self
441                .checklists
442                .iter()
443                .enumerate()
444                .filter(|(_, list)| list.pairs.iter().any(CandidatePair::is_discardable))
445                .max_by_key(|(_, list)| list.pairs.len())
446                .map(|(index, _)| index);
447            let Some(index) = longest else { return };
448            let Some(list) = self.checklists.get_mut(index) else {
449                return;
450            };
451            let lowest = list
452                .pairs
453                .iter()
454                .enumerate()
455                .filter(|(_, pair)| pair.is_discardable())
456                .min_by_key(|(_, pair)| pair.priority)
457                .map(|(position, _)| position);
458            match lowest {
459                Some(position) => {
460                    let dropped = list.pairs.remove(position);
461                    list.triggered.retain(|id| *id != dropped.id);
462                }
463                None => return,
464            }
465        }
466    }
467
468    /// §6.1.2.6's initial states: everything Frozen, every checklist Running, and then exactly
469    /// one pair per foundation moved to Waiting.
470    ///
471    /// The pair to unfreeze is "the first candidate pair (ordered by the lowest component ID and
472    /// then the highest priority if component IDs are equal) in the first checklist … that has
473    /// that foundation", and a foundation already unfrozen in an earlier checklist is not
474    /// unfrozen again. RFC 8445's own Table 1 walks the case that distinguishes this from
475    /// RFC 5245's rule, and `the_rfcs_three_checklist_five_foundation_example_unfreezes_five_pairs`
476    /// asserts it cell by cell.
477    pub fn compute_initial_states(&mut self) {
478        for list in &mut self.checklists {
479            for pair in &mut list.pairs {
480                pair.state = PairState::Frozen;
481            }
482            list.set_state(ChecklistState::Running);
483        }
484        self.unfreeze_added();
485    }
486
487    /// §6.1.2, applied to a set that has grown: "if candidates are added to a checklist … the
488    /// agent will re-perform these steps for the updated checklist".
489    ///
490    /// The same rule as §6.1.2.6 step 4, expressed over whatever is Frozen now: for each
491    /// foundation that has no pair anywhere in the set outside the Frozen state, the first Frozen
492    /// pair with it — by lowest component ID, then highest priority, in the first checklist that
493    /// has it — moves to Waiting. Run over a set where everything is Frozen this *is* step 4; run
494    /// over a live set it unfreezes exactly the foundations the new pairs brought.
495    pub fn unfreeze_added(&mut self) {
496        let mut unfrozen: Vec<PairFoundation> = self
497            .checklists
498            .iter()
499            .flat_map(|list| list.pairs.iter())
500            .filter(|pair| pair.state != PairState::Frozen)
501            .map(|pair| pair.foundation.clone())
502            .collect();
503
504        for list in &mut self.checklists {
505            let mut order: Vec<usize> = (0..list.pairs.len()).collect();
506            order.sort_by_key(|position| {
507                list.pairs
508                    .get(*position)
509                    .map(|pair| (pair.component, std::cmp::Reverse(pair.priority)))
510            });
511            for position in order {
512                let Some(pair) = list.pairs.get_mut(position) else {
513                    continue;
514                };
515                if pair.state != PairState::Frozen || unfrozen.contains(&pair.foundation) {
516                    continue;
517                }
518                pair.state = PairState::Waiting;
519                unfrozen.push(pair.foundation.clone());
520            }
521        }
522    }
523
524    /// §7.2.5.3.3: every Frozen pair in every checklist that shares this foundation moves to
525    /// Waiting.
526    pub fn unfreeze_foundation(&mut self, foundation: &PairFoundation) {
527        for list in &mut self.checklists {
528            for pair in &mut list.pairs {
529                if pair.state == PairState::Frozen && pair.foundation == *foundation {
530                    pair.state = PairState::Waiting;
531                }
532            }
533        }
534    }
535
536    /// §6.1.4.2 step 2: with nothing Waiting in this checklist and something Frozen in it,
537    /// unfreeze the Frozen pairs whose foundation has no pair Waiting or In-Progress anywhere in
538    /// the set.
539    ///
540    /// This is the second unfreeze trigger, and the one that is easy to miss. Without it, a
541    /// foundation whose one unfrozen pair failed leaves its remaining pairs Frozen for the rest
542    /// of the session — §6.1.2.6 unfreezes each foundation exactly once, and §7.2.5.3.3 only ever
543    /// unfreezes on *success* — so ICE reports a failure for a path it never finished checking.
544    pub fn unfreeze_idle(&mut self, index: usize) {
545        let Some(list) = self.checklists.get(index) else {
546            return;
547        };
548        if list
549            .pairs
550            .iter()
551            .any(|pair| pair.state == PairState::Waiting)
552        {
553            return;
554        }
555        let frozen: Vec<PairFoundation> = list
556            .pairs
557            .iter()
558            .filter(|pair| pair.state == PairState::Frozen)
559            .map(|pair| pair.foundation.clone())
560            .collect();
561        let busy: Vec<PairFoundation> = self
562            .checklists
563            .iter()
564            .flat_map(|list| list.pairs.iter())
565            .filter(|pair| matches!(pair.state, PairState::Waiting | PairState::InProgress))
566            .map(|pair| pair.foundation.clone())
567            .collect();
568
569        let Some(list) = self.checklists.get_mut(index) else {
570            return;
571        };
572        let mut thawed: Vec<PairFoundation> = Vec::new();
573        for foundation in frozen {
574            if busy.contains(&foundation) || thawed.contains(&foundation) {
575                continue;
576            }
577            if let Some(pair) = list
578                .pairs
579                .iter_mut()
580                .find(|pair| pair.state == PairState::Frozen && pair.foundation == foundation)
581            {
582                pair.state = PairState::Waiting;
583                thawed.push(foundation);
584            }
585        }
586    }
587
588    /// The next checklist Ta may act on, round-robin (§6.1.4.2).
589    ///
590    /// "Whenever Ta fires the next checklist in the Running state in the checklist set is picked
591    /// … After the last checklist in the Running state has been processed, the first checklist is
592    /// picked again." A Completed checklist is included when it still has a triggered check
593    /// queued, because §8.1.2 requires an agent to keep answering for a concluded stream and
594    /// §8.1.1's tolerance clause depends on it.
595    pub fn next_active(&mut self) -> Option<usize> {
596        let count = self.checklists.len();
597        for offset in 0..count {
598            let index = (self.next.wrapping_add(offset)) % count.max(1);
599            if self
600                .checklists
601                .get(index)
602                .is_some_and(|list| list.state() == ChecklistState::Running || list.has_triggered())
603            {
604                self.next = index.wrapping_add(1) % count.max(1);
605                return Some(index);
606            }
607        }
608        None
609    }
610
611    /// Recompute every pair priority and re-sort every checklist (§6.1.2.3).
612    ///
613    /// A role change swaps which side is `G` and which is `D`, so this runs on every role change —
614    /// forgetting it is one of the two ways role conflict is mishandled, and the other is not
615    /// detecting the conflict at all.
616    pub fn recompute_priorities(
617        &mut self,
618        role: Role,
619        locals: &[LocalCandidate],
620        remotes: &[RemoteCandidate],
621    ) {
622        for list in &mut self.checklists {
623            for pair in &mut list.pairs {
624                let (Some(local), Some(remote)) = (
625                    find_local(locals, pair.local),
626                    find_remote(remotes, pair.remote),
627                ) else {
628                    continue;
629                };
630                pair.priority = ordered_pair_priority(role, local.priority, remote.priority);
631            }
632            list.sort();
633        }
634        for list in &mut self.checklists {
635            for valid in &mut list.valid {
636                let Some(local) = find_local(locals, valid.local) else {
637                    continue;
638                };
639                let remote = remotes
640                    .iter()
641                    .find(|candidate| candidate.address == valid.remote)
642                    .map(|candidate| candidate.priority);
643                if let Some(remote) = remote {
644                    valid.priority = ordered_pair_priority(role, local.priority, remote);
645                }
646            }
647        }
648    }
649}
650
651/// §6.1.2.3's pair priority with the operands put in the roles the formula names.
652///
653/// `G` is "the priority for the candidate provided by the controlling agent" — so which of the
654/// two candidate priorities is `G` is a fact about our role, not about the pair.
655#[must_use]
656pub fn ordered_pair_priority(
657    role: Role,
658    local: sipx_sdp::ice::Priority,
659    remote: sipx_sdp::ice::Priority,
660) -> u64 {
661    if role.is_controlling() {
662        pair_priority(local, remote)
663    } else {
664        pair_priority(remote, local)
665    }
666}
667
668/// Whether an address is an IPv6 link-local unicast address (`fe80::/10`).
669///
670/// §6.1.2.2 makes pairing one with anything but another link-local address a MUST NOT: with IPv6
671/// a host commonly has several addresses per interface, and a link-local paired with a global one
672/// is a check that cannot work and a packet that was sent anyway.
673#[must_use]
674pub fn is_link_local(address: IpAddr) -> bool {
675    match address {
676        IpAddr::V6(v6) => v6
677            .segments()
678            .first()
679            .is_some_and(|first| *first & 0xffc0 == 0xfe80),
680        IpAddr::V4(_) => false,
681    }
682}
683
684/// §6.1.2.2: pair each local candidate with each remote candidate of the same component and the
685/// same address family, then §6.1.2.4's pruning.
686///
687/// The component reduction is §6.1.2.2's: "the number of components for that data stream is
688/// effectively reduced … to the minimum across both agents of the maximum component ID provided
689/// by each agent". If sipx offers RTP alone because [`MediaPort`](crate::session) did not get the
690/// control port, the peer's RTCP candidates go unpaired, which is exactly the case that sentence
691/// describes.
692pub fn form_pairs(
693    ids: &mut PairIds,
694    role: Role,
695    locals: &[LocalCandidate],
696    remotes: &[RemoteCandidate],
697) -> Vec<CandidatePair> {
698    let max_local = locals
699        .iter()
700        .map(|candidate| candidate.gathered.component)
701        .max();
702    let max_remote = remotes.iter().map(|candidate| candidate.component).max();
703    let (Some(max_local), Some(max_remote)) = (max_local, max_remote) else {
704        return Vec::new();
705    };
706    let ceiling = max_local.min(max_remote);
707
708    let mut pairs = Vec::new();
709    for local in locals {
710        if local.gathered.component > ceiling {
711            continue;
712        }
713        for remote in remotes {
714            if local.gathered.component != remote.component {
715                continue;
716            }
717            let local_ip = local.gathered.address.ip();
718            let remote_ip = remote.address.ip();
719            if local_ip.is_ipv4() != remote_ip.is_ipv4() {
720                continue;
721            }
722            if is_link_local(local_ip) != is_link_local(remote_ip) {
723                continue;
724            }
725            pairs.push(CandidatePair {
726                id: ids.allocate(),
727                local: local.id,
728                remote: remote.id,
729                component: local.gathered.component,
730                foundation: PairFoundation {
731                    local: local.foundation,
732                    remote: remote.foundation.clone(),
733                },
734                priority: ordered_pair_priority(role, local.priority, remote.priority),
735                state: PairState::Frozen,
736                nominated: false,
737            });
738        }
739    }
740    pairs.sort_by_key(|pair| std::cmp::Reverse(pair.priority));
741    prune(&mut pairs, locals, remotes);
742    pairs
743}
744
745/// §6.1.2.4: replace a reflexive local candidate with its base, then drop redundant pairs.
746///
747/// Both halves matter and only one of them is obvious. A check is sent *from a base* — there is
748/// no socket at a reflexive address — so a pair whose local candidate is reflexive names an
749/// address nothing can send from. Replacing it then makes pairs collide, and the second half
750/// removes the collisions: "two candidate pairs are redundant if their local candidates have the
751/// same base and their remote candidates are identical", keeping the higher-priority one, which
752/// is the first in an already-sorted list.
753fn prune(pairs: &mut Vec<CandidatePair>, locals: &[LocalCandidate], remotes: &[RemoteCandidate]) {
754    for pair in pairs.iter_mut() {
755        let Some(local) = find_local(locals, pair.local) else {
756            continue;
757        };
758        if !matches!(
759            local.gathered.kind,
760            CandidateType::ServerReflexive | CandidateType::PeerReflexive
761        ) {
762            continue;
763        }
764        let base = local.gathered.base_address;
765        let component = local.gathered.component;
766        if let Some(host) = locals.iter().find(|candidate| {
767            candidate.gathered.kind == CandidateType::Host
768                && candidate.gathered.address == base
769                && candidate.gathered.component == component
770        }) {
771            pair.local = host.id;
772        }
773    }
774
775    let mut kept: Vec<(SocketAddr, SocketAddr)> = Vec::new();
776    pairs.retain(|pair| {
777        let (Some(local), Some(remote)) = (
778            find_local(locals, pair.local),
779            find_remote(remotes, pair.remote),
780        ) else {
781            return false;
782        };
783        let key = (local.gathered.base_address, remote.address);
784        if kept.contains(&key) {
785            return false;
786        }
787        kept.push(key);
788        true
789    });
790}
791
792#[cfg(test)]
793#[allow(
794    clippy::unwrap_used,
795    clippy::expect_used,
796    clippy::panic,
797    clippy::indexing_slicing
798)]
799mod tests {
800    use sipx_sdp::ice::{Foundation, Priority};
801
802    use super::*;
803    use crate::ice::candidate::{
804        Gathered, LocalBase, LocalFoundation, RemoteFoundation, SINGLE_ADDRESS_PREFERENCE,
805        assign_local_preferences,
806    };
807    use crate::ice::candidate::{find_local, find_remote};
808
809    fn component(id: u16) -> ComponentId {
810        ComponentId::new(id).unwrap()
811    }
812
813    fn host(id: usize, ip: &str, port: u16, component_id: u16) -> LocalCandidate {
814        let address = SocketAddr::new(ip.parse().unwrap(), port);
815        LocalCandidate {
816            id: LocalId(id),
817            gathered: Gathered {
818                base: LocalBase(0),
819                base_address: address,
820                address,
821                kind: CandidateType::Host,
822                component: component(component_id),
823                server: None,
824            },
825            foundation: LocalFoundation(1),
826            local_preference: SINGLE_ADDRESS_PREFERENCE,
827            priority: crate::ice::candidate::priority(
828                crate::ice::candidate::HOST_PREFERENCE,
829                SINGLE_ADDRESS_PREFERENCE,
830                component(component_id),
831            ),
832        }
833    }
834
835    fn remote(ip: &str, port: u16, component_id: u16, foundation: u32) -> RemoteCandidate {
836        RemoteCandidate {
837            // Distinct per fixture: the address decides, as it does for a real candidate.
838            id: RemoteId(ip.bytes().map(usize::from).sum::<usize>() * 100_000 + usize::from(port)),
839            address: SocketAddr::new(ip.parse().unwrap(), port),
840            kind: CandidateType::Host,
841            component: component(component_id),
842            foundation: RemoteFoundation::Signalled(
843                Foundation::new(&foundation.to_string()).unwrap(),
844            ),
845            // Server-reflexive, so that a pair's `G` and `D` differ and a role swap is visible.
846            priority: crate::ice::candidate::priority(
847                crate::ice::candidate::SERVER_REFLEXIVE_PREFERENCE,
848                SINGLE_ADDRESS_PREFERENCE,
849                component(component_id),
850            ),
851        }
852    }
853
854    fn pair(id: u32, component_id: u16, foundation: u32, priority: u64) -> CandidatePair {
855        CandidatePair {
856            id: PairId(id),
857            local: LocalId(0),
858            remote: RemoteId(0),
859            component: component(component_id),
860            foundation: PairFoundation {
861                local: LocalFoundation(foundation),
862                remote: RemoteFoundation::Learned(foundation),
863            },
864            priority,
865            state: PairState::Frozen,
866            nominated: false,
867        }
868    }
869
870    /// RFC 8445 §6.1.2.6's Table 1, cell by cell.
871    ///
872    /// Three checklists over five foundations: `m1` has f1, f2, f3; `m2` has f1, f2, f3, f4; `m3`
873    /// has f1 and f5. Exactly five pairs end up Waiting — every pair in `m1`, f4 in `m2`, f5 in
874    /// `m3` — and the rest stay Frozen because their foundation was already unfrozen in an
875    /// earlier checklist. This is the case the RFC's own NOTE calls out as different from
876    /// RFC 5245, where only the first checklist was ever unfrozen.
877    #[test]
878    fn the_rfcs_three_checklist_five_foundation_example_unfreezes_five_pairs() {
879        let mut set = ChecklistSet::new();
880        set.push(Checklist::new(vec![
881            pair(1, 1, 1, 300),
882            pair(2, 1, 2, 200),
883            pair(3, 1, 3, 100),
884        ]));
885        set.push(Checklist::new(vec![
886            pair(4, 1, 1, 300),
887            pair(5, 1, 2, 200),
888            pair(6, 1, 3, 100),
889            pair(7, 1, 4, 50),
890        ]));
891        set.push(Checklist::new(vec![pair(8, 1, 1, 300), pair(9, 1, 5, 10)]));
892
893        set.compute_initial_states();
894
895        let state = |id: u32| set.pair(PairId(id)).unwrap().state;
896        // m1: every foundation is new here, so every pair is unfrozen.
897        assert_eq!(state(1), PairState::Waiting);
898        assert_eq!(state(2), PairState::Waiting);
899        assert_eq!(state(3), PairState::Waiting);
900        // m2: f1, f2 and f3 were already unfrozen in m1; only f4 is new.
901        assert_eq!(state(4), PairState::Frozen);
902        assert_eq!(state(5), PairState::Frozen);
903        assert_eq!(state(6), PairState::Frozen);
904        assert_eq!(state(7), PairState::Waiting);
905        // m3: f1 was unfrozen in m1; only f5 is new.
906        assert_eq!(state(8), PairState::Frozen);
907        assert_eq!(state(9), PairState::Waiting);
908
909        assert!(
910            set.checklists()
911                .iter()
912                .all(|list| list.state() == ChecklistState::Running)
913        );
914    }
915
916    /// "ordered by the lowest component ID and then the highest priority if component IDs are
917    /// equal": with both components sharing a foundation, it is the RTP pair that thaws.
918    #[test]
919    fn the_unfrozen_pair_for_a_foundation_is_the_lowest_component_then_the_highest_priority() {
920        let mut set = ChecklistSet::new();
921        set.push(Checklist::new(vec![
922            pair(1, 2, 1, 900),
923            pair(2, 1, 1, 100),
924            pair(3, 1, 1, 500),
925        ]));
926        set.compute_initial_states();
927        // Component 1 beats the higher-priority component 2 pair; within component 1, 500 wins.
928        assert_eq!(set.pair(PairId(3)).unwrap().state, PairState::Waiting);
929        assert_eq!(set.pair(PairId(1)).unwrap().state, PairState::Frozen);
930        assert_eq!(set.pair(PairId(2)).unwrap().state, PairState::Frozen);
931    }
932
933    /// §6.1.4.2 step 2. The foundation's one unfrozen pair failed; without this the rest of the
934    /// foundation stays Frozen for the session and ICE fails a path it never checked.
935    #[test]
936    fn a_foundation_whose_only_unfrozen_pair_failed_is_thawed_again() {
937        let mut set = ChecklistSet::new();
938        set.push(Checklist::new(vec![pair(1, 1, 1, 300), pair(2, 2, 1, 200)]));
939        set.compute_initial_states();
940        assert_eq!(set.pair(PairId(1)).unwrap().state, PairState::Waiting);
941        assert_eq!(set.pair(PairId(2)).unwrap().state, PairState::Frozen);
942
943        set.pair_mut(PairId(1)).unwrap().state = PairState::Failed;
944        set.unfreeze_idle(0);
945        assert_eq!(set.pair(PairId(2)).unwrap().state, PairState::Waiting);
946    }
947
948    /// …and it must not fire while the foundation still has a check in flight, or the pacing of
949    /// §6.1.4.2 becomes two checks per Ta tick for the same foundation.
950    #[test]
951    fn nothing_is_thawed_while_the_foundation_still_has_a_check_outstanding() {
952        let mut set = ChecklistSet::new();
953        set.push(Checklist::new(vec![pair(1, 1, 1, 300), pair(2, 2, 1, 200)]));
954        set.compute_initial_states();
955        set.pair_mut(PairId(1)).unwrap().state = PairState::InProgress;
956        set.unfreeze_idle(0);
957        assert_eq!(set.pair(PairId(2)).unwrap().state, PairState::Frozen);
958    }
959
960    #[test]
961    fn pairs_are_ordered_by_decreasing_priority_and_ties_keep_their_order() {
962        let mut list = Checklist::new(vec![
963            pair(1, 1, 1, 100),
964            pair(2, 1, 2, 900),
965            pair(3, 1, 3, 100),
966        ]);
967        list.sort();
968        let order: Vec<u32> = list.pairs().iter().map(|pair| pair.id.0).collect();
969        assert_eq!(order, vec![2, 1, 3]);
970        list.sort();
971        let again: Vec<u32> = list.pairs().iter().map(|pair| pair.id.0).collect();
972        assert_eq!(order, again);
973    }
974
975    #[test]
976    fn the_hundred_pair_limit_is_enforced_and_configurable() {
977        let mut set = ChecklistSet::new();
978        set.push(Checklist::new(
979            (0..150u32).map(|n| pair(n, 1, 1, u64::from(n))).collect(),
980        ));
981        set.limit(100);
982        assert_eq!(set.total_pairs(), 100);
983        // The lowest-priority pairs went.
984        assert!(set.pair(PairId(0)).is_none());
985        assert!(set.pair(PairId(149)).is_some());
986
987        set.limit(10);
988        assert_eq!(set.total_pairs(), 10);
989    }
990
991    /// §6.1.2.5 wants the discarding spread across checklists, so a long checklist loses pairs
992    /// before a short one does.
993    #[test]
994    fn the_limit_takes_from_the_longest_checklist_first() {
995        let mut set = ChecklistSet::new();
996        set.push(Checklist::new(
997            (0..8u32).map(|n| pair(n, 1, 1, u64::from(n))).collect(),
998        ));
999        set.push(Checklist::new(
1000            (10..12u32).map(|n| pair(n, 1, 1, u64::from(n))).collect(),
1001        ));
1002        set.limit(6);
1003        assert_eq!(set.checklists()[0].pairs().len(), 4);
1004        assert_eq!(set.checklists()[1].pairs().len(), 2);
1005    }
1006
1007    #[test]
1008    fn candidates_pair_only_within_a_component_and_an_address_family() {
1009        let locals = vec![host(1, "192.0.2.1", 5000, 1), host(2, "192.0.2.1", 5001, 2)];
1010        let remotes = vec![
1011            remote("198.51.100.1", 6000, 1, 1),
1012            remote("198.51.100.1", 6001, 2, 1),
1013            remote("2001:db8::1", 6002, 1, 2),
1014        ];
1015        let mut ids = PairIds::default();
1016        let pairs = form_pairs(&mut ids, Role::Controlling, &locals, &remotes);
1017        assert_eq!(pairs.len(), 2);
1018        assert!(pairs.iter().all(|pair| {
1019            find_local(&locals, pair.local).unwrap().gathered.component
1020                == find_remote(&remotes, pair.remote).unwrap().component
1021        }));
1022    }
1023
1024    /// §6.1.2.2's MUST NOT: a link-local address pairs only with another link-local one.
1025    #[test]
1026    fn an_ipv6_link_local_candidate_pairs_only_with_link_local_addresses() {
1027        assert!(is_link_local("fe80::1".parse().unwrap()));
1028        assert!(!is_link_local("2001:db8::1".parse().unwrap()));
1029        assert!(!is_link_local("192.0.2.1".parse().unwrap()));
1030
1031        let locals = vec![host(1, "fe80::1", 5000, 1), host(2, "2001:db8::1", 5000, 1)];
1032        let remotes = vec![
1033            remote("fe80::2", 6000, 1, 1),
1034            remote("2001:db8::2", 6000, 1, 2),
1035        ];
1036        let mut ids = PairIds::default();
1037        let pairs = form_pairs(&mut ids, Role::Controlling, &locals, &remotes);
1038        assert_eq!(pairs.len(), 2);
1039        for pair in &pairs {
1040            assert_eq!(
1041                is_link_local(
1042                    find_local(&locals, pair.local)
1043                        .unwrap()
1044                        .gathered
1045                        .address
1046                        .ip()
1047                ),
1048                is_link_local(find_remote(&remotes, pair.remote).unwrap().address.ip())
1049            );
1050        }
1051    }
1052
1053    /// §6.1.2.2: the number of components is the minimum of the two agents' maxima, so a peer
1054    /// that offers RTCP to an agent that has no control port gets its RTCP candidates unpaired.
1055    #[test]
1056    fn a_peer_offering_rtcp_to_an_agent_without_one_gets_no_rtcp_pairs() {
1057        let locals = vec![host(1, "192.0.2.1", 5000, 1)];
1058        let remotes = vec![
1059            remote("198.51.100.1", 6000, 1, 1),
1060            remote("198.51.100.1", 6001, 2, 1),
1061        ];
1062        let mut ids = PairIds::default();
1063        let pairs = form_pairs(&mut ids, Role::Controlling, &locals, &remotes);
1064        assert_eq!(pairs.len(), 1);
1065        assert_eq!(pairs[0].component, component(1));
1066    }
1067
1068    /// §6.1.2.4, both halves: the reflexive local becomes its base, and the pair that collides
1069    /// with the host pair as a result is dropped rather than checked twice.
1070    #[test]
1071    fn a_reflexive_local_becomes_its_base_and_the_redundant_pair_goes() {
1072        let base = SocketAddr::new("192.0.2.1".parse().unwrap(), 5000);
1073        let mut locals = vec![
1074            host(1, "192.0.2.1", 5000, 1),
1075            LocalCandidate {
1076                id: LocalId(1),
1077                gathered: Gathered {
1078                    base: LocalBase(0),
1079                    base_address: base,
1080                    address: SocketAddr::new("198.51.100.9".parse().unwrap(), 7000),
1081                    kind: CandidateType::ServerReflexive,
1082                    component: component(1),
1083                    server: Some("198.51.100.1".parse().unwrap()),
1084                },
1085                foundation: LocalFoundation(2),
1086                local_preference: SINGLE_ADDRESS_PREFERENCE,
1087                priority: Priority::new(1).unwrap(),
1088            },
1089        ];
1090        assign_local_preferences(&mut locals);
1091        let remotes = vec![remote("198.51.100.2", 6000, 1, 1)];
1092
1093        let mut ids = PairIds::default();
1094        let pairs = form_pairs(&mut ids, Role::Controlling, &locals, &remotes);
1095        assert_eq!(pairs.len(), 1);
1096        // Whichever pair survived, its local candidate is one a socket exists at.
1097        assert_eq!(
1098            find_local(&locals, pairs[0].local).unwrap().gathered.kind,
1099            CandidateType::Host
1100        );
1101    }
1102
1103    /// §6.1.2.3: `G` is the controlling agent's candidate, so the same pair seen from the two
1104    /// ends must produce the same number — which it only does if the role decides the operands.
1105    #[test]
1106    fn both_ends_compute_the_same_pair_priority_for_the_same_pair() {
1107        let ours = Priority::new(2_130_706_431).unwrap();
1108        let theirs = Priority::new(1_694_498_815).unwrap();
1109        assert_eq!(
1110            ordered_pair_priority(Role::Controlling, ours, theirs),
1111            ordered_pair_priority(Role::Controlled, theirs, ours)
1112        );
1113    }
1114
1115    #[test]
1116    fn a_role_change_recomputes_every_pair_priority_and_re_sorts() {
1117        let locals = vec![host(1, "192.0.2.1", 5000, 1)];
1118        let remotes = vec![remote("198.51.100.1", 6000, 1, 1)];
1119        let mut ids = PairIds::default();
1120        let mut set = ChecklistSet::new();
1121        set.push(Checklist::new(form_pairs(
1122            &mut ids,
1123            Role::Controlling,
1124            &locals,
1125            &remotes,
1126        )));
1127        let before = set.checklists()[0].pairs()[0].priority;
1128
1129        set.recompute_priorities(Role::Controlled, &locals, &remotes);
1130        let after = set.checklists()[0].pairs()[0].priority;
1131        // The two candidates have different priorities here, so swapping G and D must move it.
1132        assert_ne!(before, after);
1133        assert_eq!(
1134            after,
1135            ordered_pair_priority(Role::Controlled, locals[0].priority, remotes[0].priority)
1136        );
1137        // And the pair still names the candidates it was formed for.
1138        let pair = &set.checklists()[0].pairs()[0];
1139        assert!(find_local(&locals, pair.local).is_some());
1140        assert!(find_remote(&remotes, pair.remote).is_some());
1141    }
1142
1143    #[test]
1144    fn the_triggered_queue_is_fifo_and_holds_a_pair_once() {
1145        let mut list = Checklist::new(vec![pair(1, 1, 1, 100), pair(2, 1, 2, 200)]);
1146        list.trigger(PairId(2));
1147        list.trigger(PairId(1));
1148        list.trigger(PairId(2));
1149        assert_eq!(list.take_triggered(), Some(PairId(2)));
1150        assert_eq!(list.take_triggered(), Some(PairId(1)));
1151        assert_eq!(list.take_triggered(), None);
1152    }
1153
1154    #[test]
1155    fn determining_the_role_follows_section_6_1_1() {
1156        assert_eq!(Role::determine(true, false), Role::Controlling);
1157        assert_eq!(Role::determine(false, false), Role::Controlled);
1158        // Full against lite: controlling, whoever offered.
1159        assert_eq!(Role::determine(false, true), Role::Controlling);
1160        assert_eq!(Role::determine(true, true), Role::Controlling);
1161        assert_eq!(Role::Controlling.opposite(), Role::Controlled);
1162        assert_eq!(Role::Controlled.opposite(), Role::Controlling);
1163    }
1164}