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}