Skip to main content

sipx_media/ice/
driver.rs

1//! The driver: the socket and the clock the sans-IO agent does without ([spec] §2, §15).
2//!
3//! The division is the whole point of the shape. [`Agent`](super::Agent) owns the protocol —
4//! which check goes out next, when it is retransmitted, which pair wins — and this task owns the
5//! two things a state machine must not: a `UdpSocket` and a deadline. Every datagram it sends is
6//! an [`Output::Send`] it was handed, and **every timer it arms is an [`Output::SetTimer`] it was
7//! handed**. It never schedules anything of its own.
8//!
9//! That last rule is not tidiness. A driver with a timer of its own can keep an agent that has
10//! stopped asking for ticks alive, which makes a dead pacing path look healthy from the outside —
11//! the exact defect `M-21`'s review found in a *test* that fired Ta by hand, one layer up. The
12//! deadline table here holds only what the agent put in it, a fired one-shot is removed before the
13//! agent sees it, and nothing re-arms it but the agent's own next output.
14//!
15//! The other rule is subtractive: the driver feeds the agent only inputs [spec] §2 names, and only
16//! when the thing they describe actually happened. Manufacturing an input — replaying a datagram,
17//! synthesising a `DataSent` for media that did not go out — is how an outside caller reintroduces
18//! the triggered-check storm `da9d49f` fixed on the inside.
19//!
20//! [spec]: https://github.com/codewandler/sipx/blob/main/docs/specs/ice.md
21
22use std::collections::HashMap;
23use std::net::SocketAddr;
24use std::sync::Arc;
25use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
26
27use tokio::net::UdpSocket;
28use tokio::sync::{Mutex, mpsc, oneshot, watch};
29
30use sipx_sdp::ice::{Candidate, CandidateType, ComponentId, Credentials};
31
32use super::agent::{Agent, Input, Output, Timer};
33use super::candidate::LocalBase;
34use crate::counters::DiscardMeters;
35
36/// How many events may queue for the driver before the media path stops offering them.
37///
38/// Small on purpose. Everything on this channel is either a datagram that has already been read
39/// off the socket or a note that a media packet went out, and none of it is worth blocking a
40/// receive loop or a send loop for: a driver that has fallen this far behind will not catch up by
41/// being given more.
42const EVENTS: usize = 64;
43
44/// The candidate path an ICE-backed media session actually selected.
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum IcePath {
47    /// The session did not negotiate ICE.
48    Disabled,
49    /// ICE is running but has not selected an RTP pair yet.
50    Checking,
51    /// Both ends of the selected pair are host candidates.
52    Host,
53    /// At least one end of the selected pair is server-reflexive.
54    ServerReflexive,
55    /// At least one end is peer-reflexive, and neither is relayed or server-reflexive.
56    PeerReflexive,
57    /// At least one end of the selected pair is relayed.
58    Relayed,
59}
60
61impl IcePath {
62    const fn encoded(self) -> u8 {
63        match self {
64            Self::Disabled => 0,
65            Self::Checking => 1,
66            Self::Host => 2,
67            Self::ServerReflexive => 3,
68            Self::PeerReflexive => 4,
69            Self::Relayed => 5,
70        }
71    }
72
73    fn decoded(encoded: u8) -> Self {
74        match encoded {
75            2 => Self::Host,
76            3 => Self::ServerReflexive,
77            4 => Self::PeerReflexive,
78            5 => Self::Relayed,
79            _ => Self::Checking,
80        }
81    }
82
83    fn selected(local: CandidateType, remote: CandidateType) -> Self {
84        if matches!(local, CandidateType::Relayed) || matches!(remote, CandidateType::Relayed) {
85            Self::Relayed
86        } else if matches!(local, CandidateType::ServerReflexive)
87            || matches!(remote, CandidateType::ServerReflexive)
88        {
89            Self::ServerReflexive
90        } else if matches!(local, CandidateType::PeerReflexive)
91            || matches!(remote, CandidateType::PeerReflexive)
92        {
93            Self::PeerReflexive
94        } else {
95            Self::Host
96        }
97    }
98}
99
100/// What the media path tells the driver about.
101///
102/// Exactly two things, and both are facts rather than requests. There is no "send a check now" —
103/// that decision is the agent's, and a channel that could carry it would be a second scheduler.
104#[derive(Debug)]
105pub(crate) enum Event {
106    /// A datagram [`crate::dtls::classify`] called STUN (RFC 5764 §5.1.2), and where it came from.
107    Datagram {
108        /// Its source address.
109        from: SocketAddr,
110        /// Which of our sockets it arrived on.
111        on: LocalBase,
112        /// The bytes, exactly as they arrived.
113        bytes: Vec<u8>,
114    },
115    /// Media went out on a component's selected pair, which resets that pair's keepalive (§11).
116    DataSent {
117        /// Which component carried it.
118        component: ComponentId,
119    },
120    /// A later offer or answer on this call carried the peer's ICE half (RFC 8839 §4.4; [spec]
121    /// §13.5).
122    ///
123    /// This is the third fact, and it is a fact like the other two: a description arrived. What it
124    /// means — merge the candidates, or rebuild for a restart — is the agent's to decide, exactly
125    /// as it decides for the description that started the session.
126    ///
127    /// [spec]: https://github.com/codewandler/sipx/blob/main/docs/specs/ice.md
128    Renegotiated {
129        /// The local parameters to adopt first, when this exchange is a restart this side is
130        /// offering or answering. `None` leaves the running session's credentials in place.
131        local: Option<(Credentials, u64)>,
132        /// The peer's half, when the description carried one. `None` is a restart this side is
133        /// offering, whose answer has not arrived yet.
134        peer: Option<Peer>,
135        /// Where to send back what the next description must signal, once both are applied.
136        reply: oneshot::Sender<Local>,
137    },
138}
139
140/// The peer's ICE half, as [`super::Negotiation`] read it out of a description.
141#[derive(Debug)]
142pub(crate) struct Peer {
143    pub(crate) credentials: Credentials,
144    pub(crate) candidates: Vec<Candidate>,
145    pub(crate) lite: bool,
146}
147
148/// What this side must put in its next offer or answer for the stream ([spec] §13.5).
149///
150/// [spec]: https://github.com/codewandler/sipx/blob/main/docs/specs/ice.md
151#[derive(Debug, Clone)]
152pub struct Local {
153    /// `a=ice-ufrag` and `a=ice-pwd`, read back from the agent rather than from the caller's copy.
154    pub credentials: Credentials,
155    /// `a=candidate`, priced by the agent, in descending priority.
156    pub candidates: Vec<Candidate>,
157}
158
159/// The handle the media path holds: where to send events, and whether it is worth sending them.
160#[derive(Debug, Clone)]
161pub(crate) struct Handle {
162    events: mpsc::Sender<Event>,
163    discards: Arc<DiscardMeters>,
164    /// Whether a pair has been selected for component 1.
165    ///
166    /// Read by the send loop before it reports a packet, so that the fifty notes a second an
167    /// ordinary call would produce are not even constructed until there is a selected pair for
168    /// them to be about. §11's keepalive is only ever on a selected pair, so before there is one
169    /// the agent would discard every one of them.
170    selected: Arc<AtomicBool>,
171    path: Arc<AtomicU8>,
172    #[cfg_attr(not(feature = "dtls"), allow(dead_code))]
173    selection: watch::Receiver<Selection>,
174}
175
176#[derive(Debug, Clone, Copy, PartialEq, Eq)]
177#[cfg_attr(not(feature = "dtls"), allow(dead_code))]
178enum Selection {
179    Checking,
180    Selected(crate::browser::SelectedComponent),
181    Failed,
182}
183
184/// Why component 1 produced no nominated pair.
185#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
186#[cfg(feature = "dtls")]
187pub(crate) enum SelectionError {
188    /// ICE exhausted component 1 without a nominated pair.
189    #[error("ICE failed before nominating component 1")]
190    Failed,
191    /// The driver stopped before reporting either selection or failure.
192    #[error("ICE stopped before nominating component 1")]
193    Stopped,
194}
195
196impl Handle {
197    /// The RTP candidate path selected so far.
198    pub(crate) fn path(&self) -> IcePath {
199        IcePath::decoded(self.path.load(Ordering::Relaxed))
200    }
201
202    /// Wait for component 1's exact selected pair or its terminal failure.
203    #[cfg(feature = "dtls")]
204    pub(crate) async fn wait_selected(
205        &self,
206        ice_generation: u64,
207    ) -> Result<crate::browser::SelectedComponent, SelectionError> {
208        let mut selection = self.selection.clone();
209        loop {
210            match *selection.borrow_and_update() {
211                Selection::Selected(mut selected) => {
212                    selected.ice_generation = ice_generation;
213                    return Ok(selected);
214                }
215                Selection::Failed => return Err(SelectionError::Failed),
216                Selection::Checking => {}
217            }
218            selection
219                .changed()
220                .await
221                .map_err(|_| SelectionError::Stopped)?;
222        }
223    }
224    /// Hand the driver a datagram. Non-blocking: a full queue drops it.
225    ///
226    /// Dropping is right and not merely convenient. A connectivity check is a retransmitted
227    /// transaction (RFC 5389 §7.2.1) and the far end will send it again; blocking the receive loop
228    /// on a slow driver would stall the *audio* to protect a check that is already redundant.
229    pub(crate) fn datagram(&self, from: SocketAddr, on: LocalBase, bytes: Vec<u8>) -> bool {
230        if self
231            .events
232            .try_send(Event::Datagram { from, on, bytes })
233            .is_err()
234        {
235            self.discards
236                .ice_driver_queue_refusals
237                .fetch_add(1, Ordering::Relaxed);
238            tracing::debug!(%from, "dropping a connectivity check the ice driver could not take");
239            false
240        } else {
241            true
242        }
243    }
244
245    /// Apply a later exchange's ICE half and read back what the next description must signal.
246    ///
247    /// Awaited rather than dropped on a full queue, which is the opposite of
248    /// [`Self::datagram`]'s rule and for the opposite reason: a connectivity check is
249    /// retransmitted by the far end, and an offer/answer is not. Losing one silently would leave
250    /// the agent keyed to credentials the peer has stopped using, so the checks would authenticate
251    /// against nothing and the caller would signal candidates for a session that no longer exists.
252    ///
253    /// `None` when the driver has stopped — the session is ending, and the caller answers without
254    /// ICE attributes rather than waiting for a task that will never reply.
255    pub(crate) async fn renegotiated(
256        &self,
257        local: Option<(Credentials, u64)>,
258        peer: Option<Peer>,
259    ) -> Option<Local> {
260        let (reply, answered) = oneshot::channel();
261        self.events
262            .send(Event::Renegotiated { local, peer, reply })
263            .await
264            .ok()?;
265        answered.await.ok()
266    }
267
268    /// Note that media went out, if there is a selected pair for it to have gone out on.
269    pub(crate) fn data_sent(&self, component: ComponentId) {
270        if !self.selected.load(Ordering::Relaxed) {
271            return;
272        }
273        // A dropped note costs one keepalive that did not need to be sent; §11's indication is
274        // unauthenticated and draws no response, so it is the cheapest thing here to lose.
275        if self.events.try_send(Event::DataSent { component }).is_err() {
276            self.discards
277                .ice_data_sent_queue_refusals
278                .fetch_add(1, Ordering::Relaxed);
279        }
280    }
281}
282
283/// Where the media path sends, and what the driver moves when ICE concludes.
284///
285/// Shared with the send loop and the report loop rather than pushed to them, because those loops
286/// already read `remote` on every packet: making the selected pair a write to the same cell is
287/// what makes the switch atomic with respect to a packet in flight.
288#[derive(Debug, Clone)]
289pub(crate) struct Destinations {
290    /// Component 1: where RTP goes. Starts at the `c=`/`m=` default destination.
291    pub(crate) rtp: Arc<Mutex<SocketAddr>>,
292    /// Component 2: where RTCP goes, once ICE has selected a pair for it.
293    ///
294    /// `None` leaves the report loop on RFC 3550 §11's convention — the RTP destination's port
295    /// plus one — which is what it does for a stream with no ICE and what it must keep doing for
296    /// a stream whose second component never concluded.
297    pub(crate) rtcp: Arc<Mutex<Option<SocketAddr>>>,
298}
299
300/// The running driver.
301struct Driver {
302    agent: Agent,
303    /// The sockets, indexed by the [`LocalBase`] the agent names them with ([spec] §2).
304    ///
305    /// [spec]: https://github.com/codewandler/sipx/blob/main/docs/specs/ice.md
306    sockets: Vec<Arc<UdpSocket>>,
307    /// What the agent has asked to be woken for, and when. **Only** what the agent asked for.
308    deadlines: HashMap<Timer, tokio::time::Instant>,
309    events: mpsc::Receiver<Event>,
310    destinations: Destinations,
311    selected: Arc<AtomicBool>,
312    path: Arc<AtomicU8>,
313    selection: watch::Sender<Selection>,
314    stop: Arc<crate::session::Stop>,
315    discards: Arc<DiscardMeters>,
316}
317
318/// A browser component retains both halves so shutdown can join the ICE task.
319#[cfg(feature = "dtls")]
320pub(crate) struct OwnedDriver {
321    pub(crate) handle: Handle,
322    pub(crate) task: tokio::task::JoinHandle<()>,
323}
324
325/// Start the driver for a stream, and hand the media path its end of it.
326pub(crate) fn spawn(
327    agent: Agent,
328    pending: Vec<Output>,
329    sockets: Vec<Arc<UdpSocket>>,
330    destinations: Destinations,
331    stop: Arc<crate::session::Stop>,
332    discards: Arc<DiscardMeters>,
333) -> (Handle, tokio::task::JoinHandle<()>) {
334    spawn_parts(agent, pending, sockets, destinations, stop, discards, None)
335}
336
337#[cfg(feature = "dtls")]
338pub(crate) fn spawn_owned(
339    agent: Agent,
340    pending: Vec<Output>,
341    sockets: Vec<Arc<UdpSocket>>,
342    destinations: Destinations,
343    stop: Arc<crate::session::Stop>,
344    discards: Arc<DiscardMeters>,
345    profile_tasks: Arc<crate::browser::ProfileTasks>,
346) -> OwnedDriver {
347    let (handle, task) = spawn_parts(
348        agent,
349        pending,
350        sockets,
351        destinations,
352        stop,
353        discards,
354        Some(profile_tasks),
355    );
356    OwnedDriver { handle, task }
357}
358
359fn spawn_parts(
360    agent: Agent,
361    pending: Vec<Output>,
362    sockets: Vec<Arc<UdpSocket>>,
363    destinations: Destinations,
364    stop: Arc<crate::session::Stop>,
365    discards: Arc<DiscardMeters>,
366    #[cfg_attr(not(feature = "dtls"), allow(unused_variables))] profile_tasks: Option<
367        Arc<crate::browser::ProfileTasks>,
368    >,
369) -> (Handle, tokio::task::JoinHandle<()>) {
370    let (events_tx, events_rx) = mpsc::channel(EVENTS);
371    let selected = Arc::new(AtomicBool::new(false));
372    let path = Arc::new(AtomicU8::new(IcePath::Checking.encoded()));
373    let (selection, selected_pair) = watch::channel(Selection::Checking);
374    let driver = Driver {
375        agent,
376        sockets,
377        deadlines: HashMap::new(),
378        events: events_rx,
379        destinations,
380        selected: Arc::clone(&selected),
381        path: Arc::clone(&path),
382        selection,
383        stop,
384        discards: Arc::clone(&discards),
385    };
386    let task = if let Some(profile_tasks) = profile_tasks {
387        tokio::spawn(crate::browser::profile_task(
388            profile_tasks,
389            driver.run(pending),
390        ))
391    } else {
392        tokio::spawn(driver.run(pending))
393    };
394    let handle = Handle {
395        events: events_tx,
396        selected,
397        path,
398        selection: selected_pair,
399        discards,
400    };
401    (handle, task)
402}
403
404impl Driver {
405    /// The loop. One `select!` over three things: the stop signal, an event from the media path,
406    /// and the earliest deadline the agent has asked for.
407    async fn run(mut self, pending: Vec<Output>) {
408        self.apply(pending).await;
409
410        loop {
411            if self.stop.is_stopped() {
412                return;
413            }
414            // Recomputed every pass, because the agent may have moved, cleared or added a
415            // deadline while handling the last event. There is no timer here that outlives the
416            // pass that armed it.
417            let next = self
418                .deadlines
419                .iter()
420                .min_by_key(|(_, at)| **at)
421                .map(|(timer, at)| (*timer, *at));
422
423            // Disabled outright when the agent has asked for nothing, so an agent that has gone
424            // quiet is not woken by a deadline this loop invented. The instant in that case is
425            // never waited on — the guard is what makes the arm inert.
426            let deadline = next.map_or_else(tokio::time::Instant::now, |(_, at)| at);
427            let event = tokio::select! {
428                () = self.stop.wait() => return,
429                event = self.events.recv() => event,
430                () = tokio::time::sleep_until(deadline), if next.is_some() => {
431                    if let Some((timer, _)) = next {
432                        // A one-shot that has fired is no longer armed. Removing it *before* the
433                        // agent sees it is what makes the next one the agent's to ask for.
434                        self.deadlines.remove(&timer);
435                        let outputs = self.agent.handle(Input::TimerFired(timer));
436                        self.apply(outputs).await;
437                    }
438                    continue;
439                }
440            };
441
442            let Some(event) = event else {
443                // Every sender is gone, which means the session's loops have ended.
444                return;
445            };
446            let outputs = match event {
447                Event::Datagram { from, on, bytes } => {
448                    self.agent.handle(Input::Datagram { from, on, bytes })
449                }
450                Event::DataSent { component } => self.agent.handle(Input::DataSent { component }),
451                Event::Renegotiated { local, peer, reply } => {
452                    let outputs = self.renegotiated(local, peer);
453                    // Dropped receiver means the signalling side gave up on this exchange; the
454                    // agent has still applied it, which is correct — the peer's credentials
455                    // changed whether or not anybody is waiting to hear what ours are.
456                    if reply
457                        .send(Local {
458                            credentials: self.agent.credentials().clone(),
459                            candidates: super::gather::lines(self.agent.local_candidates()),
460                        })
461                        .is_err()
462                    {
463                        self.discards
464                            .ice_renegotiation_reply_unobserved
465                            .fetch_add(1, Ordering::Relaxed);
466                    }
467                    outputs
468                }
469            };
470            self.apply(outputs).await;
471        }
472    }
473
474    /// Apply a later exchange's ICE half to the running agent (RFC 8839 §4.4; [spec] §13.5).
475    ///
476    /// The order is the contract and not an implementation detail. Our own parameters go in
477    /// **first**, so that when the peer's description turns out to be a restart, the checklists the
478    /// agent rebuilds are keyed to the credentials this side is about to signal rather than to the
479    /// ones the finished session used. Applied the other way round, the new session would start
480    /// authenticating with credentials the peer has already been told to forget.
481    ///
482    /// Whether this *is* a restart is not decided here. It is RFC 8839 §4.4.1.1.1's question about
483    /// the peer's two credentials, the agent has always answered it, and asking it a second time
484    /// here would be a second place for the answer to drift.
485    ///
486    /// [spec]: https://github.com/codewandler/sipx/blob/main/docs/specs/ice.md
487    fn renegotiated(
488        &mut self,
489        local: Option<(Credentials, u64)>,
490        peer: Option<Peer>,
491    ) -> Vec<Output> {
492        let mut outputs = Vec::new();
493        if let Some((credentials, tiebreaker)) = local {
494            outputs.extend(self.agent.handle(Input::LocalCredentials {
495                credentials,
496                tiebreaker,
497            }));
498        }
499        if let Some(peer) = peer {
500            outputs.extend(self.agent.handle(Input::RemoteDescription {
501                credentials: peer.credentials,
502                candidates: peer.candidates,
503                lite: peer.lite,
504            }));
505        }
506        outputs
507    }
508
509    /// Perform the agent's outputs, **in the order given** ([spec] §2): a `Send` always precedes
510    /// the `SetTimer` that would retransmit it, so this loop is sequential and awaits each send
511    /// before it arms anything.
512    ///
513    /// [spec]: https://github.com/codewandler/sipx/blob/main/docs/specs/ice.md
514    async fn apply(&mut self, outputs: Vec<Output>) {
515        for output in outputs {
516            match output {
517                Output::Send { on, to, bytes } => {
518                    let Some(socket) = self.sockets.get(usize::from(on.0)) else {
519                        // The agent named a base the driver did not bind. It cannot: every base
520                        // it knows came from a `LocalCandidate` this driver gathered.
521                        // discard: every base the agent can name came from a candidate gathered
522                        // over this exact socket vector, so this branch is structurally unreachable.
523                        tracing::warn!(base = on.0, "no socket for the base the agent named");
524                        continue;
525                    };
526                    if let Err(error) = socket.send_to(&bytes, to).await {
527                        // One unreachable candidate is an ordinary thing to find — it is what
528                        // checking is for — and the pair fails on its own timer rather than here.
529                        self.discards
530                            .ice_send_failures
531                            .fetch_add(1, Ordering::Relaxed);
532                        tracing::debug!(%to, %error, "a connectivity check could not be sent");
533                    }
534                }
535                Output::SetTimer { timer, after } => {
536                    let at = tokio::time::Instant::now()
537                        .checked_add(after)
538                        .unwrap_or_else(tokio::time::Instant::now);
539                    self.deadlines.insert(timer, at);
540                }
541                Output::ClearTimer(timer) => {
542                    self.deadlines.remove(&timer);
543                }
544                Output::Selected {
545                    component,
546                    local,
547                    local_kind,
548                    remote,
549                    remote_kind,
550                } => {
551                    self.select(component, local, local_kind, remote, remote_kind)
552                        .await;
553                }
554                Output::Failed { component } => {
555                    // The call layer decides what a failed component means ([spec] §2). What the
556                    // media path does is nothing: the stream keeps sending to the default
557                    // destination, which is where it was already sending.
558                    // discard: this is the agent's terminal outcome, not a payload with a later
559                    // consumer; the default path remains active.
560                    tracing::warn!(component = component.get(), "ice failed for a component");
561                    if component == ComponentId::RTP {
562                        self.selection.send_replace(Selection::Failed);
563                    }
564                }
565            }
566        }
567    }
568
569    /// Point the media at a selected pair (§8.1.1).
570    ///
571    /// This is the moment the stream stops being an SDP address and starts being a checked path,
572    /// and it is also the moment symmetric RTP stops applying — the receive loop was told at
573    /// startup not to learn, because on an ICE stream the address is ICE's to choose and an
574    /// unauthenticated packet must not be able to move it.
575    async fn select(
576        &mut self,
577        component: ComponentId,
578        local: LocalBase,
579        local_kind: CandidateType,
580        remote: SocketAddr,
581        remote_kind: CandidateType,
582    ) {
583        if component == ComponentId::RTP {
584            if local != LocalBase(0) {
585                // RTP leaves the media socket, which is base 0 by construction. A selected pair
586                // on any other base would mean audio and its checks on different sockets, and
587                // the far end would see media from an address it never validated.
588                tracing::warn!(
589                    base = local.0,
590                    "a selected rtp pair on a base that is not the media socket"
591                );
592                return;
593            }
594            let Some(socket) = self.sockets.get(usize::from(local.0)) else {
595                tracing::warn!(base = local.0, "selected pair names an unbound local base");
596                return;
597            };
598            let Ok(local_address) = socket.local_addr() else {
599                tracing::warn!(base = local.0, "selected pair's local base has no address");
600                return;
601            };
602            *self.destinations.rtp.lock().await = remote;
603            self.selected.store(true, Ordering::Relaxed);
604            self.path.store(
605                IcePath::selected(local_kind, remote_kind).encoded(),
606                Ordering::Relaxed,
607            );
608            self.selection.send_replace(Selection::Selected(
609                crate::browser::SelectedComponent::new(local_address, remote, 0)
610                    .with_candidate_types(local_kind, remote_kind),
611            ));
612        } else {
613            *self.destinations.rtcp.lock().await = Some(remote);
614        }
615        tracing::debug!(component = component.get(), %remote, "ice selected a pair");
616    }
617}
618
619#[cfg(test)]
620#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
621mod tests {
622    use super::*;
623
624    #[test]
625    fn the_reported_path_is_derived_from_the_selected_pair_not_the_requested_policy() {
626        assert_eq!(
627            IcePath::selected(CandidateType::Host, CandidateType::Host),
628            IcePath::Host
629        );
630        assert_eq!(
631            IcePath::selected(CandidateType::Host, CandidateType::ServerReflexive),
632            IcePath::ServerReflexive
633        );
634        assert_eq!(
635            IcePath::selected(CandidateType::PeerReflexive, CandidateType::Host),
636            IcePath::PeerReflexive
637        );
638        assert_eq!(
639            IcePath::selected(CandidateType::Host, CandidateType::Relayed),
640            IcePath::Relayed
641        );
642    }
643}