Skip to main content

ezk_ice/
lib.rs

1#![deny(unreachable_pub, unsafe_code)]
2
3//! sans io implementation of an ICE agent
4
5use core::fmt;
6use rand::distr::{Alphanumeric, SampleString};
7use sdp_types::{IceCandidate, UntaggedAddress};
8use slotmap::{SlotMap, new_key_type};
9use std::{
10    cmp::{max, min},
11    collections::VecDeque,
12    hash::{DefaultHasher, Hash, Hasher},
13    mem::take,
14    net::{IpAddr, SocketAddr},
15    time::{Duration, Instant},
16};
17use stun::{StunConfig, StunServerBinding};
18use stun_types::{
19    Class, Message, TransactionId,
20    attributes::{
21        ErrorCode, Fingerprint, IceControlled, IceControlling, Priority, UseCandidate,
22        XorMappedAddress,
23    },
24};
25
26mod stun;
27
28/// A message received on a UDP socket
29pub struct ReceivedPkt<D = Vec<u8>> {
30    /// The received data
31    pub data: D,
32    /// Source address of the message
33    pub source: SocketAddr,
34    /// Local socket destination address of the message
35    pub destination: SocketAddr,
36    /// On which component socket this was received
37    pub component: Component,
38}
39
40/// Component of the data stream
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
42#[repr(u8)]
43pub enum Component {
44    /// The RTP component of the data stream. This will also contain RTCP if rtcp-mux is enabled.
45    Rtp = 1,
46    /// The RTCP component of the data stream. This will not be used if rtcp-mux is enabled.
47    Rtcp = 2,
48}
49
50/// ICE related events emitted by the [`IceAgent`]
51#[derive(Debug)]
52pub enum IceEvent {
53    GatheringStateChanged {
54        old: IceGatheringState,
55        new: IceGatheringState,
56    },
57    ConnectionStateChanged {
58        old: IceConnectionState,
59        new: IceConnectionState,
60    },
61    DiscoveredAddr {
62        component: Component,
63        target: SocketAddr,
64    },
65    SendData {
66        component: Component,
67        data: Vec<u8>,
68        source: Option<IpAddr>,
69        target: SocketAddr,
70    },
71}
72
73/// The ICE agent state machine
74pub struct IceAgent {
75    stun_config: StunConfig,
76
77    stun_server: Vec<StunServerBinding>,
78
79    local_credentials: IceCredentials,
80    remote_credentials: Option<IceCredentials>,
81
82    local_candidates: SlotMap<LocalCandidateId, Candidate>,
83    remote_candidates: SlotMap<RemoteCandidateId, Candidate>,
84
85    pairs: Vec<CandidatePair>,
86    triggered_check_queue: VecDeque<(LocalCandidateId, RemoteCandidateId)>,
87
88    rtcp_mux: bool,
89    is_controlling: bool,
90    control_tie_breaker: u64,
91    max_pairs: usize,
92
93    gathering_state: IceGatheringState,
94    connection_state: IceConnectionState,
95
96    last_ta_trigger: Option<Instant>,
97
98    /// STUN Messages that are received before the remote credentials are available.
99    /// Capped to prevent unbounded growth from unauthenticated traffic.
100    backlog: Vec<(Instant, ReceivedPkt<Message>)>,
101
102    events: VecDeque<IceEvent>,
103}
104
105/// State of gathering candidates from external (STUN/TURN) servers.
106/// If no STUN server is configured this state will jump directly to `Complete`.
107#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
108pub enum IceGatheringState {
109    /// The ICE agent was just created
110    New,
111    /// The ICE agent is in the process of gathering candidates
112    Gathering,
113    /// The ICE agent has finished gathering candidates. If something happens that requires collecting new candidates,
114    /// such as the addition of a new ICE server, the state will revert to `Gathering` to gather those candidates.
115    Complete,
116}
117
118/// State of the ICE agent
119#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
120// Ordering might look weird, but it's so the total state of all ice agents can be "combined" using `min`
121pub enum IceConnectionState {
122    /// The ICE agent has failed to find a valid candidate pair for all components
123    Failed,
124
125    /// Checks to ensure that components are still connected failed for at least one component.
126    /// This is a less stringent test than `Failed` and may trigger intermittently and resolve just as spontaneously on
127    /// less reliable networks, or during temporary disconnections.
128    /// When the problem resolves, the connection may return to the `Connected` state.
129    Disconnected,
130
131    /// The ICE agent is awaiting local & remote ice candidates
132    New,
133    /// The ICE agent is in the process of checking candidates pairs
134    Checking,
135    /// The ICE agent has found a valid pair for all components
136    Connected,
137}
138
139new_key_type!(
140    struct LocalCandidateId;
141    struct RemoteCandidateId;
142);
143
144#[derive(Debug, PartialEq, Clone, Copy, Hash)]
145enum CandidateKind {
146    Host = 126,
147    PeerReflexive = 110,
148    ServerReflexive = 100,
149    // TODO: Relayed = 0,
150}
151
152#[derive(Debug)]
153struct Candidate {
154    addr: SocketAddr,
155    // transport: udp
156    kind: CandidateKind,
157    priority: u32,
158    foundation: String,
159
160    component: Component,
161
162    // The transport address that an ICE agent sends from for a particular candidate.
163    // For host, server-reflexive, and peer-reflexive candidates, the base is the same as the host candidate.
164    // For relayed candidates, the base is the same as the relayed candidate
165    //  (i.e., the transport address used by the TURN server to send from).
166    base: SocketAddr,
167}
168
169#[derive(Debug)]
170struct CandidatePair {
171    local: LocalCandidateId,
172    remote: RemoteCandidateId,
173    priority: u64,
174    state: CandidatePairState,
175    component: Component,
176
177    nomination: CandidatePairNomination,
178}
179
180#[derive(Debug, PartialEq, Eq)]
181enum CandidatePairNomination {
182    /// Not nominated
183    None,
184    /// Sent off nominations STUN request with use-candidate attribute
185    Nominated,
186    /// Candidate is nominated as used
187    Selected,
188}
189
190#[derive(Debug, Clone, PartialEq)]
191enum CandidatePairState {
192    /// A check has not been sent for this pair, but the pair is not Frozen.
193    Waiting,
194
195    /// A check has been sent for this pair, but the transaction is in progress.
196    InProgress {
197        transaction_id: TransactionId,
198        stun_request: Vec<u8>,
199        retransmit_at: Instant,
200        retransmits: u32,
201        source: IpAddr,
202        target: SocketAddr,
203    },
204
205    // A check has been sent for this pair, and it produced a successful result.
206    Succeeded,
207
208    /// A check has been sent for this pair, and it failed (a response to the check
209    /// was never received, or a failure response was received).
210    Failed,
211}
212
213/// Credentials of an ICE agent
214///
215/// These must be exchanges using some external signaling protocol like SDP
216#[derive(Clone)]
217pub struct IceCredentials {
218    pub ufrag: String,
219    pub pwd: String,
220}
221
222impl IceCredentials {
223    pub fn random() -> Self {
224        let mut rng = rand::rng();
225
226        Self {
227            ufrag: Alphanumeric.sample_string(&mut rng, 8),
228            pwd: Alphanumeric.sample_string(&mut rng, 32),
229        }
230    }
231}
232
233impl IceAgent {
234    pub fn new_from_answer(
235        local_credentials: IceCredentials,
236        remote_credentials: IceCredentials,
237        is_controlling: bool,
238        rtcp_mux: bool,
239    ) -> Self {
240        IceAgent {
241            stun_config: StunConfig::new(),
242            stun_server: vec![],
243            local_credentials,
244            remote_credentials: Some(remote_credentials),
245            local_candidates: SlotMap::with_key(),
246            remote_candidates: SlotMap::with_key(),
247            pairs: Vec::new(),
248            triggered_check_queue: VecDeque::new(),
249            rtcp_mux,
250            is_controlling,
251            control_tie_breaker: rand::random(),
252            max_pairs: 100,
253            gathering_state: IceGatheringState::New,
254            connection_state: IceConnectionState::New,
255            last_ta_trigger: None,
256            backlog: vec![],
257            events: VecDeque::new(),
258        }
259    }
260
261    pub fn new_for_offer(
262        local_credentials: IceCredentials,
263        is_controlling: bool,
264        rtcp_mux: bool,
265    ) -> Self {
266        IceAgent {
267            stun_config: StunConfig::new(),
268            stun_server: vec![],
269            local_credentials,
270            remote_credentials: None,
271            local_candidates: SlotMap::with_key(),
272            remote_candidates: SlotMap::with_key(),
273            pairs: Vec::new(),
274            triggered_check_queue: VecDeque::new(),
275            rtcp_mux,
276            is_controlling,
277            control_tie_breaker: rand::random(),
278            max_pairs: 100,
279            gathering_state: IceGatheringState::New,
280            connection_state: IceConnectionState::New,
281            last_ta_trigger: None,
282            backlog: vec![],
283            events: VecDeque::new(),
284        }
285    }
286
287    /// Set all the remote information in one step. This function is usually set once after receiving a SDP answer.
288    pub fn set_remote_data(
289        &mut self,
290        credentials: IceCredentials,
291        candidates: &[IceCandidate],
292        rtcp_mux: bool,
293    ) {
294        // TODO: assert that we can't change from rtcp-mux: true -> false
295        self.rtcp_mux = rtcp_mux;
296
297        // Remove all rtcp candidates and stun server bindings rtcp-mux is enabled
298        if rtcp_mux {
299            self.stun_server.retain(|s| s.component() == Component::Rtp);
300            self.local_candidates
301                .retain(|_, c| c.component == Component::Rtp);
302        }
303
304        self.remote_credentials = Some(credentials);
305
306        for candidate in candidates {
307            self.add_remote_candidate(candidate);
308        }
309
310        for (instant, pkt) in take(&mut self.backlog) {
311            self.receive_stun(instant, pkt);
312        }
313    }
314
315    /// Return the ice-agent's ice credentials
316    pub fn credentials(&self) -> &IceCredentials {
317        &self.local_credentials
318    }
319
320    /// Register a host address for a given ICE component. This will be used to create a host candidate.
321    /// For the ICE agent to work properly, all available ip addresses of the host system should be provided.
322    pub fn add_host_addr(&mut self, component: Component, addr: SocketAddr) {
323        if addr.ip().is_unspecified() {
324            return;
325        }
326
327        if let SocketAddr::V6(v6) = addr {
328            let ip = v6.ip();
329            if ip.to_ipv4().is_some() || ip.to_ipv4_mapped().is_some() {
330                return;
331            }
332        }
333
334        self.add_local_candidate(component, CandidateKind::Host, addr, addr, None);
335    }
336
337    /// Add a STUN server which the ICE agent should use to gather additional (server-reflexive) candidates.
338    pub fn add_stun_server(&mut self, server: SocketAddr) {
339        // TODO: ideally create a stun server binding for every local interface
340        self.stun_server
341            .push(StunServerBinding::new(server, Component::Rtp));
342
343        if !self.rtcp_mux {
344            self.stun_server
345                .push(StunServerBinding::new(server, Component::Rtcp));
346        }
347    }
348
349    /// Returns the current ICE candidate gathering state
350    pub fn gathering_state(&self) -> IceGatheringState {
351        self.gathering_state
352    }
353
354    /// Returns the current ICE connection state
355    pub fn connection_state(&self) -> IceConnectionState {
356        self.connection_state
357    }
358
359    /// Returns the discovered local & remote address for the given component
360    pub fn discovered_addr(&self, mut component: Component) -> Option<(SocketAddr, SocketAddr)> {
361        if self.rtcp_mux {
362            component = Component::Rtp;
363        }
364
365        self.pairs
366            .iter()
367            .find(|pair| {
368                pair.component == component
369                    && pair.state == CandidatePairState::Succeeded
370                    && pair.nomination == CandidatePairNomination::Selected
371            })
372            .map(|pair| {
373                (
374                    self.local_candidates[pair.local].addr,
375                    self.remote_candidates[pair.remote].addr,
376                )
377            })
378    }
379
380    fn add_local_candidate(
381        &mut self,
382        component: Component,
383        kind: CandidateKind,
384        base: SocketAddr,
385        addr: SocketAddr,
386        stun_server: Option<SocketAddr>,
387    ) {
388        // Check if we need to create a new candidate for this
389        let already_exists = self
390            .local_candidates
391            .values()
392            .any(|c| c.kind == kind && c.base == base && c.addr == addr);
393
394        if already_exists {
395            // ignore
396            return;
397        }
398
399        log::debug!("add local candidate {component:?} {kind:?} {addr}");
400
401        // Calculate the candidate priority using offsets + count of candidates of the same type
402        // (trick that I have stolen from str0m's implementation)
403        let local_preference_offset = match kind {
404            CandidateKind::Host => (65535 / 4) * 3,
405            CandidateKind::PeerReflexive => (65535 / 4) * 2,
406            CandidateKind::ServerReflexive => 65535 / 4,
407            // CandidateKind::Relayed => 0,
408        };
409
410        let local_preference = self
411            .local_candidates
412            .values()
413            .filter(|c| c.component == component && c.kind == kind)
414            .count() as u32
415            + local_preference_offset;
416
417        let kind_preference = (kind as u32) << 24;
418        let local_preference = local_preference << 8;
419        let priority = kind_preference + local_preference + (256 - component as u32);
420
421        self.local_candidates.insert(Candidate {
422            addr,
423            kind,
424            priority,
425            foundation: compute_foundation(kind, base.ip(), stun_server, "udp").to_string(),
426            component,
427            base,
428        });
429
430        self.form_pairs();
431    }
432
433    /// Add a peer's ice-candidate which has been received using an extern signaling protocol
434    pub fn add_remote_candidate(&mut self, candidate: &IceCandidate) {
435        let kind = match candidate.typ.as_str() {
436            "host" => CandidateKind::Host,
437            "srflx" => CandidateKind::ServerReflexive,
438            _ => return,
439        };
440
441        // TODO: currently only udp transport is supported
442        if !candidate.transport.eq_ignore_ascii_case("udp") {
443            return;
444        }
445
446        let Ok(priority) = u32::try_from(candidate.priority) else {
447            log::warn!("Candidate has priority larger than u32::MAX");
448            return;
449        };
450
451        let component = match candidate.component {
452            1 => Component::Rtp,
453            // Discard candidates for rtcp if rtcp-mux is enabled
454            2 if !self.rtcp_mux => Component::Rtcp,
455            _ => {
456                log::debug!(
457                    "Discard remote candidate with unsupported component candidate:{candidate}"
458                );
459                return;
460            }
461        };
462
463        let ip = match candidate.address {
464            UntaggedAddress::Fqdn(..) => return,
465            UntaggedAddress::IpAddress(ip_addr) => ip_addr,
466        };
467
468        self.remote_candidates.insert(Candidate {
469            addr: SocketAddr::new(ip, candidate.port),
470            kind,
471            priority,
472            foundation: candidate.foundation.to_string(),
473            component,
474            base: SocketAddr::new(ip, candidate.port), // TODO: do I even need this?
475        });
476
477        self.form_pairs();
478    }
479
480    fn form_pairs(&mut self) {
481        for (local_id, local_candidate) in &self.local_candidates {
482            for (remote_id, remote_candidate) in &self.remote_candidates {
483                // Remote peer-reflexive candidates are not paired here
484                if remote_candidate.kind == CandidateKind::PeerReflexive {
485                    continue;
486                }
487
488                // Do not pair candidates with different components
489                if local_candidate.component != remote_candidate.component {
490                    continue;
491                }
492
493                // Check if the pair already exists
494                let already_exists = self
495                    .pairs
496                    .iter()
497                    .any(|pair| pair.local == local_id && pair.remote == remote_id);
498
499                if already_exists {
500                    continue;
501                }
502
503                // Exclude pairs with different ip version
504                match (local_candidate.addr.ip(), remote_candidate.addr.ip()) {
505                    (IpAddr::V4(l), IpAddr::V4(r)) if l.is_link_local() == r.is_link_local() => {
506                        /* ok */
507                    }
508                    // Only pair IPv6 addresses when either both or neither are link local addresses
509                    (IpAddr::V6(l), IpAddr::V6(r))
510                        if l.is_unicast_link_local() == r.is_unicast_link_local() =>
511                    { /* ok */ }
512                    _ => {
513                        // Would make an invalid pair, skip
514                        continue;
515                    }
516                }
517
518                Self::add_candidate_pair(
519                    local_id,
520                    local_candidate,
521                    remote_id,
522                    remote_candidate,
523                    self.is_controlling,
524                    &mut self.pairs,
525                    false,
526                );
527            }
528        }
529
530        self.pairs
531            .sort_unstable_by(|a, b| b.priority.cmp(&a.priority));
532
533        self.prune_pairs();
534    }
535
536    fn add_candidate_pair(
537        local_id: LocalCandidateId,
538        local_candidate: &Candidate,
539        remote_id: RemoteCandidateId,
540        remote_candidate: &Candidate,
541        is_controlling: bool,
542        pairs: &mut Vec<CandidatePair>,
543        received_use_candidate: bool,
544    ) {
545        if pairs
546            .iter()
547            .any(|p| p.local == local_id && p.remote == remote_id)
548        {
549            // pair already exists
550            return;
551        }
552
553        let priority = pair_priority(local_candidate, remote_candidate, is_controlling);
554
555        log::debug!(
556            "add pair {}, priority: {priority}, component={:?}",
557            DisplayPair(local_candidate, remote_candidate),
558            local_candidate.component,
559        );
560
561        pairs.push(CandidatePair {
562            local: local_id,
563            remote: remote_id,
564            priority,
565            state: CandidatePairState::Waiting,
566            component: local_candidate.component,
567            nomination: if received_use_candidate {
568                CandidatePairNomination::Selected
569            } else {
570                CandidatePairNomination::None
571            },
572        });
573        pairs.sort_unstable_by(|a, b| b.priority.cmp(&a.priority));
574    }
575
576    fn recompute_pair_priorities(&mut self) {
577        for pair in &mut self.pairs {
578            pair.priority = pair_priority(
579                &self.local_candidates[pair.local],
580                &self.remote_candidates[pair.remote],
581                self.is_controlling,
582            );
583        }
584
585        self.pairs
586            .sort_unstable_by(|a, b| b.priority.cmp(&a.priority));
587    }
588
589    /// Prune the lowest priority pairs until `max_pairs` is reached
590    fn prune_pairs(&mut self) {
591        let mut to_prune = Vec::new();
592
593        for (index, pair) in self.pairs.iter().enumerate().rev() {
594            // Stop pruning when no longer exceeding the limit
595            if (self.pairs.len() - to_prune.len()) <= self.max_pairs {
596                break;
597            }
598
599            // Don't prune nominated candidates
600            if pair.nomination != CandidatePairNomination::None {
601                continue;
602            }
603
604            // Don't prune successful or in-flight candidates
605            if matches!(
606                pair.state,
607                CandidatePairState::Succeeded | CandidatePairState::InProgress { .. }
608            ) {
609                continue;
610            }
611
612            to_prune.push(index);
613        }
614
615        for index in to_prune {
616            let pair = self.pairs.remove(index);
617
618            log::debug!(
619                "pruned pair {}",
620                DisplayPair(
621                    &self.local_candidates[pair.local],
622                    &self.remote_candidates[pair.remote]
623                )
624            );
625        }
626    }
627
628    /// Receive network packets for this ICE agent
629    pub fn receive(&mut self, now: Instant, pkt: ReceivedPkt) {
630        let mut stun_msg = match Message::parse(pkt.data) {
631            Ok(stun_msg) => stun_msg,
632            Err(e) => {
633                log::debug!("Failed to parse stun message {e}");
634                return;
635            }
636        };
637
638        let passed_fingerprint_check = stun_msg
639            .attribute::<Fingerprint>()
640            .is_some_and(|r| r.is_ok());
641
642        if !passed_fingerprint_check {
643            log::trace!(
644                "Incoming STUN {:?} failed fingerprint check, discarding",
645                stun_msg.class()
646            );
647            return;
648        }
649
650        let pkt = ReceivedPkt {
651            data: stun_msg,
652            source: pkt.source,
653            destination: pkt.destination,
654            component: pkt.component,
655        };
656
657        self.receive_stun(now, pkt);
658    }
659
660    fn receive_stun(&mut self, now: Instant, pkt: ReceivedPkt<Message>) {
661        match pkt.data.class() {
662            Class::Request => self.receive_stun_request(now, pkt),
663            Class::Indication => { /* ignore */ }
664            Class::Success => self.receive_stun_success(now, pkt),
665            Class::Error => self.receive_stun_error(now, pkt),
666        }
667    }
668
669    fn receive_stun_success(&mut self, now: Instant, mut pkt: ReceivedPkt<Message>) {
670        // Check our stun server binding checks before verifying integrity since these aren't authenticated
671        for stun_server_binding in &mut self.stun_server {
672            if !stun_server_binding.wants_stun_response(pkt.data.transaction_id()) {
673                continue;
674            }
675
676            let Some(addr) =
677                stun_server_binding.receive_stun_response(now, &self.stun_config, pkt.data)
678            else {
679                // TODO; no xor mapped in response, discard message
680                return;
681            };
682
683            let component = stun_server_binding.component();
684            let stun_server = stun_server_binding.server();
685            self.add_local_candidate(
686                component,
687                CandidateKind::ServerReflexive,
688                pkt.destination,
689                addr,
690                Some(stun_server),
691            );
692
693            return;
694        }
695
696        // Store messages later if the remote credentials aren't set yet
697        let Some(remote_credentials) = &self.remote_credentials else {
698            if self.backlog.len() < 128 {
699                self.backlog.push((now, pkt));
700            }
701            return;
702        };
703
704        if !stun::verify_integrity(&self.local_credentials, remote_credentials, &mut pkt.data) {
705            log::debug!("Incoming stun success failed the integrity check, discarding");
706            return;
707        }
708
709        // A connectivity check is considered a success if each of the following
710        // criteria is true:
711        // o  The Binding request generated a success response; and
712        // o  The source and destination transport addresses in the Binding
713        //    request and response are symmetric.
714        let Some(pair) = self
715            .pairs
716            .iter_mut()
717            .find(|p| {
718                matches!(p.state, CandidatePairState::InProgress { transaction_id, .. } if pkt.data.transaction_id() == transaction_id)
719            }) else {
720                log::debug!("Failed to find transaction for STUN success, discarding");
721                return;
722            };
723
724        let CandidatePairState::InProgress { source, target, .. } = &pair.state else {
725            unreachable!()
726        };
727
728        if pkt.source == *target && pkt.destination.ip() == *source {
729            log::debug!(
730                "got success response for pair {} nomination={:?}",
731                DisplayPair(
732                    &self.local_candidates[pair.local],
733                    &self.remote_candidates[pair.remote],
734                ),
735                pair.nomination,
736            );
737
738            // This request was a nomination for this pair
739            if pair.nomination == CandidatePairNomination::Nominated {
740                pair.nomination = CandidatePairNomination::Selected;
741
742                let local_candidate = &self.local_candidates[pair.local];
743                let remote_candidate = &self.remote_candidates[pair.remote];
744
745                self.events.push_back(IceEvent::DiscoveredAddr {
746                    component: local_candidate.component,
747                    target: remote_candidate.addr,
748                });
749
750                // Remove all triggered checks for this component
751                self.triggered_check_queue.retain(|(local_id, _)| {
752                    self.local_candidates[*local_id].component != local_candidate.component
753                });
754            }
755
756            pair.state = CandidatePairState::Succeeded;
757        } else {
758            log::debug!(
759                "got success response with invalid source address for pair {}",
760                DisplayPair(
761                    &self.local_candidates[pair.local],
762                    &self.remote_candidates[pair.remote]
763                )
764            );
765
766            // The ICE agent MUST check that the source and destination transport addresses in the Binding request and
767            // response are symmetric. That is, the source IP address and port of the response MUST be equal to the
768            // destination IP address and port to which the Binding request was sent, and the destination IP address and
769            // port of the response MUST be equal to the source IP address and port from which the Binding request was sent.
770            // If the addresses are not symmetric, the agent MUST set the candidate pair state to Failed.
771            pair.nomination = CandidatePairNomination::None;
772            pair.state = CandidatePairState::Failed;
773        }
774
775        // Check if we discover a new peer-reflexive candidate here
776        if let Some(Ok(mapped_addr)) = pkt.data.attribute::<XorMappedAddress>() {
777            if mapped_addr.0 != self.local_candidates[pair.local].addr {
778                let component = pair.component;
779                self.add_local_candidate(
780                    component,
781                    CandidateKind::PeerReflexive,
782                    pkt.destination,
783                    mapped_addr.0,
784                    None,
785                );
786            }
787        } else {
788            log::trace!("no (valid) XOR-MAPPED-ADDRESS attribute in STUN success response");
789        }
790    }
791
792    fn receive_stun_error(&mut self, now: Instant, mut pkt: ReceivedPkt<Message>) {
793        let Some(remote_credentials) = &self.remote_credentials else {
794            if self.backlog.len() < 128 {
795                self.backlog.push((now, pkt));
796            }
797            return;
798        };
799
800        if !stun::verify_integrity(&self.local_credentials, remote_credentials, &mut pkt.data) {
801            log::debug!("Incoming stun error response failed the integrity check, discarding");
802            return;
803        }
804
805        let Some(pair) = self
806            .pairs
807            .iter_mut()
808            .find(|p| {
809                matches!(p.state, CandidatePairState::InProgress { transaction_id, .. } if pkt.data.transaction_id() == transaction_id)
810            }) else {
811                log::debug!("Failed to find transaction for STUN error, discarding");
812                return;
813            };
814
815        let error_code = pkt.data.attribute::<ErrorCode>().and_then(|r| r.ok());
816
817        let display_pair = DisplayPair(
818            &self.local_candidates[pair.local],
819            &self.remote_candidates[pair.remote],
820        );
821
822        if let Some(error_code) = &error_code {
823            log::debug!(
824                "Candidate pair {display_pair} failed with code={}, reason={}",
825                error_code.number,
826                error_code.reason
827            );
828        } else {
829            log::debug!("Candidate pair {display_pair} failed without error code");
830        }
831
832        // If the Binding request generates a 487 (Role Conflict) error response,
833        // and if the ICE agent included an ICE-CONTROLLED attribute in the request,
834        // the agent MUST switch to the controlling role.
835        // If the agent included an ICE-CONTROLLING attribute in the request, the agent MUST switch to the controlled role.
836        if error_code.is_some_and(|e| e.number == 487) {
837            if pkt.data.attribute::<IceControlled>().is_some() {
838                self.is_controlling = true;
839            } else if pkt.data.attribute::<IceControlling>().is_some() {
840                self.is_controlling = false;
841            }
842
843            // Once the agent has switched its role, the agent MUST add the
844            // candidate pair whose check generated the 487 error response to the
845            // triggered-check queue associated with the checklist to which the pair
846            // belongs, and set the candidate pair state to Waiting.
847            pair.state = CandidatePairState::Waiting;
848            self.triggered_check_queue
849                .push_back((pair.local, pair.remote));
850
851            // A role switch requires an agent to recompute pair priorities, since the priority values depend on the role.
852            self.recompute_pair_priorities();
853        } else {
854            pair.state = CandidatePairState::Failed;
855            pair.nomination = CandidatePairNomination::None;
856        }
857    }
858
859    fn receive_stun_request(&mut self, now: Instant, mut pkt: ReceivedPkt<Message>) {
860        let Some(remote_credentials) = &self.remote_credentials else {
861            if self.backlog.len() < 128 {
862                self.backlog.push((now, pkt));
863            }
864            return;
865        };
866
867        if !stun::verify_integrity(&self.local_credentials, remote_credentials, &mut pkt.data) {
868            log::debug!("Incoming stun request failed the integrity check, discarding");
869            return;
870        }
871
872        let Some(Ok(priority)) = pkt.data.attribute::<Priority>() else {
873            log::debug!("Incoming stun request did not contain PRIORITY attribute");
874            return;
875        };
876
877        let use_candidate = pkt.data.attribute::<UseCandidate>().is_some();
878
879        // Detect and handle role conflict
880        if self.is_controlling {
881            if let Some(Ok(ice_controlling)) = pkt.data.attribute::<IceControlling>() {
882                if self.control_tie_breaker >= ice_controlling.0 {
883                    let response = stun::make_role_error(
884                        pkt.data.transaction_id(),
885                        &self.local_credentials,
886                        remote_credentials,
887                        pkt.source,
888                        true,
889                        self.control_tie_breaker,
890                    );
891
892                    self.events.push_back(IceEvent::SendData {
893                        component: pkt.component,
894                        data: response,
895                        source: Some(pkt.destination.ip()),
896                        target: pkt.source,
897                    });
898
899                    return;
900                } else {
901                    self.is_controlling = false;
902                    self.recompute_pair_priorities();
903                }
904            }
905        } else if !self.is_controlling
906            && let Some(Ok(ice_controlled)) = pkt.data.attribute::<IceControlled>()
907        {
908            if self.control_tie_breaker >= ice_controlled.0 {
909                let response = stun::make_role_error(
910                    pkt.data.transaction_id(),
911                    &self.local_credentials,
912                    remote_credentials,
913                    pkt.source,
914                    false,
915                    self.control_tie_breaker,
916                );
917
918                self.events.push_back(IceEvent::SendData {
919                    component: pkt.component,
920                    data: response,
921                    source: Some(pkt.destination.ip()),
922                    target: pkt.source,
923                });
924                return;
925            } else {
926                self.is_controlling = true;
927                self.recompute_pair_priorities();
928            }
929        }
930
931        let local_id = match self
932            .local_candidates
933            .iter()
934            .find(|(_, c)| c.kind == CandidateKind::Host && c.addr == pkt.destination)
935        {
936            Some((id, _)) => id,
937            None => {
938                log::warn!(
939                    "Failed to find matching local candidate for incoming STUN request ({})?",
940                    pkt.destination
941                );
942                return;
943            }
944        };
945
946        let matching_remote_candidate = self.remote_candidates.iter().find(|(_, c)| {
947            // todo: also match protocol
948            c.addr == pkt.source
949        });
950
951        let remote_id = match matching_remote_candidate {
952            Some((remote, _)) => remote,
953            None => {
954                // No remote candidate with the source ip addr, create new peer-reflexive candidate
955                let peer_reflexive_id = self.remote_candidates.insert(Candidate {
956                    addr: pkt.source,
957                    kind: CandidateKind::PeerReflexive,
958                    priority: priority.0,
959                    foundation: "~".into(),
960                    component: pkt.component,
961                    base: pkt.source,
962                });
963
964                // Pair it with the local candidate
965                Self::add_candidate_pair(
966                    local_id,
967                    &self.local_candidates[local_id],
968                    peer_reflexive_id,
969                    &self.remote_candidates[peer_reflexive_id],
970                    self.is_controlling,
971                    &mut self.pairs,
972                    false,
973                );
974
975                self.triggered_check_queue
976                    .push_back((local_id, peer_reflexive_id));
977
978                peer_reflexive_id
979            }
980        };
981
982        let pair = if let Some(pair) = self
983            .pairs
984            .iter_mut()
985            .find(|p| p.local == local_id && p.remote == remote_id)
986        {
987            pair
988        } else {
989            log::debug!(
990                "Got STUN request for candidates without pair, local-candidate: {} remote-candidate: {}, adding new pair",
991                DisplayCandidate(&self.local_candidates[local_id]),
992                DisplayCandidate(&self.remote_candidates[remote_id])
993            );
994
995            // Pair it with the local candidate
996            Self::add_candidate_pair(
997                local_id,
998                &self.local_candidates[local_id],
999                remote_id,
1000                &self.remote_candidates[remote_id],
1001                self.is_controlling,
1002                &mut self.pairs,
1003                false,
1004            );
1005
1006            self.triggered_check_queue.push_back((local_id, remote_id));
1007
1008            self.pairs
1009                .iter_mut()
1010                .find(|p| p.local == local_id && p.remote == remote_id)
1011                .expect("just inserted pair")
1012        };
1013
1014        if use_candidate && !self.is_controlling {
1015            pair.nomination = CandidatePairNomination::Selected;
1016        }
1017
1018        log::trace!(
1019            "got connectivity check for pair {}",
1020            DisplayPair(
1021                &self.local_candidates[pair.local],
1022                &self.remote_candidates[pair.remote],
1023            )
1024        );
1025
1026        let stun_response = stun::make_success_response(
1027            pkt.data.transaction_id(),
1028            &self.local_credentials,
1029            pkt.source,
1030        );
1031
1032        self.events.push_back(IceEvent::SendData {
1033            component: pair.component,
1034            data: stun_response,
1035            source: Some(self.local_candidates[local_id].base.ip()),
1036            target: pkt.source,
1037        });
1038
1039        // Check nomination state if we received a use-candidate
1040        if use_candidate {
1041            self.poll_nomination();
1042        }
1043    }
1044
1045    /// Drive the ICE agent forward. This must be called after the duration returned by [`timeout`](IceAgent::timeout).
1046    pub fn poll(&mut self, now: Instant) {
1047        // Progress all STUN-server bindings (used to create and maintain server-reflexive candidates)
1048        for stun_server_bindings in &mut self.stun_server {
1049            stun_server_bindings.poll(now, &self.stun_config, |event| self.events.push_back(event));
1050        }
1051
1052        // Handle pending stun retransmissions
1053        self.poll_retransmit(now);
1054        self.poll_state();
1055        self.poll_nomination();
1056
1057        // Skip anything beyond this before we received the remote credentials & candidates
1058        let Some(remote_credentials) = &self.remote_credentials else {
1059            return;
1060        };
1061
1062        // Limit new checks to 1 per 50ms
1063        if let Some(it) = self.last_ta_trigger
1064            && it + Duration::from_millis(50) > now
1065        {
1066            return;
1067        }
1068        self.last_ta_trigger = Some(now);
1069
1070        // If the triggered-check queue associated with the checklist
1071        // contains one or more candidate pairs, the agent removes the top
1072        // pair from the queue, performs a connectivity check on that pair,
1073        // puts the candidate pair state to In-Progress, and aborts the
1074        // subsequent steps.
1075        let pair = self
1076            .triggered_check_queue
1077            .pop_front()
1078            .and_then(|(local_id, remote_id)| {
1079                self.pairs
1080                    .iter_mut()
1081                    .find(|p| p.local == local_id && p.remote == remote_id)
1082            });
1083
1084        let pair = if let Some(pair) = pair {
1085            Some(pair)
1086        } else if !self.all_components_have_nominated_valid_pair() {
1087            // If there are one or more candidate pairs in the Waiting state,
1088            // the agent picks the highest-priority candidate pair (if there are
1089            // multiple pairs with the same priority, the pair with the lowest
1090            // component ID is picked) in the Waiting state, performs a
1091            // connectivity check on that pair, puts the candidate pair state to
1092            // In-Progress, and aborts the subsequent steps.
1093            self.pairs
1094                .iter_mut()
1095                .find(|p| p.state == CandidatePairState::Waiting)
1096        } else {
1097            None
1098        };
1099
1100        if let Some(pair) = pair {
1101            log::debug!(
1102                "start connectivity check for pair {}",
1103                DisplayPair(
1104                    &self.local_candidates[pair.local],
1105                    &self.remote_candidates[pair.remote]
1106                )
1107            );
1108
1109            let transaction_id = TransactionId::random();
1110
1111            let stun_request = stun::make_binding_request(
1112                transaction_id,
1113                &self.local_credentials,
1114                remote_credentials,
1115                &self.local_candidates[pair.local],
1116                self.is_controlling,
1117                self.control_tie_breaker,
1118                pair.nomination == CandidatePairNomination::Nominated,
1119            );
1120
1121            let source = self.local_candidates[pair.local].base.ip();
1122            let target = self.remote_candidates[pair.remote].addr;
1123
1124            pair.state = CandidatePairState::InProgress {
1125                transaction_id,
1126                stun_request: stun_request.clone(),
1127                retransmit_at: now + self.stun_config.retransmit_delta(0),
1128                retransmits: 0,
1129                source,
1130                target,
1131            };
1132
1133            self.events.push_back(IceEvent::SendData {
1134                component: pair.component,
1135                data: stun_request,
1136                source: Some(source),
1137                target,
1138            });
1139        }
1140    }
1141
1142    /// Check all pending STUN transactions for pending retransmits
1143    fn poll_retransmit(&mut self, now: Instant) {
1144        for pair in &mut self.pairs {
1145            let CandidatePairState::InProgress {
1146                transaction_id: _,
1147                stun_request,
1148                retransmit_at,
1149                retransmits,
1150                source,
1151                target,
1152            } = &mut pair.state
1153            else {
1154                continue;
1155            };
1156
1157            if *retransmit_at > now {
1158                continue;
1159            }
1160
1161            if *retransmits >= self.stun_config.max_retransmits {
1162                log::debug!(
1163                    "failed connectivity check for pair {}",
1164                    DisplayPair(
1165                        &self.local_candidates[pair.local],
1166                        &self.remote_candidates[pair.remote]
1167                    )
1168                );
1169
1170                pair.state = CandidatePairState::Failed;
1171                pair.nomination = CandidatePairNomination::None;
1172                continue;
1173            }
1174
1175            *retransmits += 1;
1176            *retransmit_at += self.stun_config.retransmit_delta(*retransmits);
1177
1178            log::trace!(
1179                "retransmitting connectivity check for pair {}",
1180                DisplayPair(
1181                    &self.local_candidates[pair.local],
1182                    &self.remote_candidates[pair.remote]
1183                )
1184            );
1185
1186            self.events.push_back(IceEvent::SendData {
1187                component: pair.component,
1188                data: stun_request.clone(),
1189                source: Some(*source),
1190                target: *target,
1191            });
1192        }
1193    }
1194
1195    fn poll_state(&mut self) {
1196        // Check gathering state
1197        let mut all_completed = true;
1198        for stun_server in &self.stun_server {
1199            if !stun_server.is_completed() {
1200                all_completed = false;
1201            }
1202        }
1203
1204        if all_completed && self.gathering_state != IceGatheringState::Complete {
1205            self.events.push_back(IceEvent::GatheringStateChanged {
1206                old: self.gathering_state,
1207                new: IceGatheringState::Complete,
1208            });
1209
1210            self.gathering_state = IceGatheringState::Complete;
1211        } else if !all_completed && self.gathering_state != IceGatheringState::Gathering {
1212            self.events.push_back(IceEvent::GatheringStateChanged {
1213                old: self.gathering_state,
1214                new: IceGatheringState::Gathering,
1215            });
1216
1217            self.gathering_state = IceGatheringState::Gathering;
1218        }
1219
1220        // Don't try to evaluate connection state when the remote data hasn't been set yet
1221        if self.remote_credentials.is_none() {
1222            return;
1223        }
1224
1225        // Check connection state
1226        let mut has_rtp_nomination = false;
1227        let mut has_rtcp_nomination = false;
1228
1229        let mut rtp_in_progress = false;
1230        let mut rtcp_in_progress = false;
1231
1232        for pair in &self.pairs {
1233            if pair.nomination == CandidatePairNomination::Selected
1234                && matches!(pair.state, CandidatePairState::Succeeded)
1235            {
1236                match pair.component {
1237                    Component::Rtp => has_rtp_nomination = true,
1238                    Component::Rtcp => has_rtcp_nomination = true,
1239                }
1240            }
1241
1242            if matches!(
1243                pair.state,
1244                CandidatePairState::Waiting
1245                    | CandidatePairState::InProgress { .. }
1246                    | CandidatePairState::Succeeded
1247            ) {
1248                match pair.component {
1249                    Component::Rtp => rtp_in_progress = true,
1250                    Component::Rtcp => rtcp_in_progress = true,
1251                }
1252            }
1253        }
1254
1255        let has_nomination_for_all_components = if self.rtcp_mux {
1256            has_rtp_nomination
1257        } else {
1258            has_rtp_nomination && has_rtcp_nomination
1259        };
1260
1261        let still_possible = if self.rtcp_mux {
1262            rtp_in_progress
1263        } else {
1264            rtp_in_progress && rtcp_in_progress
1265        };
1266
1267        if has_nomination_for_all_components
1268            && self.connection_state != IceConnectionState::Connected
1269        {
1270            self.set_connection_state(IceConnectionState::Connected);
1271        } else if !has_nomination_for_all_components {
1272            if still_possible {
1273                match self.connection_state {
1274                    IceConnectionState::New => {
1275                        self.set_connection_state(IceConnectionState::Checking);
1276                    }
1277                    IceConnectionState::Checking => {}
1278                    IceConnectionState::Connected => {
1279                        self.set_connection_state(IceConnectionState::Disconnected);
1280                    }
1281                    IceConnectionState::Failed => {}
1282                    IceConnectionState::Disconnected => {}
1283                }
1284            } else {
1285                self.set_connection_state(IceConnectionState::Failed);
1286            }
1287        }
1288    }
1289
1290    fn set_connection_state(&mut self, new: IceConnectionState) {
1291        if self.connection_state != new {
1292            self.events.push_back(IceEvent::ConnectionStateChanged {
1293                old: self.connection_state,
1294                new,
1295            });
1296            self.connection_state = new;
1297        }
1298    }
1299
1300    /// Progress the nomination state of the agent.
1301    fn poll_nomination(&mut self) {
1302        self.poll_nomination_of_component(Component::Rtp);
1303
1304        if !self.rtcp_mux {
1305            self.poll_nomination_of_component(Component::Rtcp);
1306        }
1307    }
1308    /// Progress the candidate nomination for a given component
1309    fn poll_nomination_of_component(&mut self, component: Component) {
1310        if self.component_has_nominated_valid_pair(component) {
1311            return;
1312        }
1313
1314        if self.is_controlling {
1315            // Controlling, select a succeeded pair and send a use-candidate
1316
1317            let best_pair = self
1318                .pairs
1319                .iter_mut()
1320                .filter(|p| {
1321                    p.component == component && matches!(p.state, CandidatePairState::Succeeded)
1322                })
1323                .max_by_key(|p| p.priority);
1324
1325            let Some(pair) = best_pair else {
1326                // no pair to nominate
1327                return;
1328            };
1329
1330            log::debug!(
1331                "nominating {}",
1332                DisplayPair(
1333                    &self.local_candidates[pair.local],
1334                    &self.remote_candidates[pair.remote]
1335                )
1336            );
1337
1338            pair.nomination = CandidatePairNomination::Nominated;
1339
1340            // Make another binding request with use-candidate as soon as possible, by pushing it to the front of the queue
1341            self.triggered_check_queue
1342                .push_front((pair.local, pair.remote));
1343        } else {
1344            // Not controlling, check if we have received a use-candidate for a successful pair
1345
1346            // Find the highest priority pair that received a use-candidate && was successful
1347            let pair = self
1348                .pairs
1349                .iter_mut()
1350                .filter(|p| {
1351                    p.component == component
1352                        && p.nomination == CandidatePairNomination::Selected
1353                        && matches!(p.state, CandidatePairState::Succeeded)
1354                })
1355                .max_by_key(|p| p.priority);
1356
1357            let Some(pair) = pair else {
1358                // no pair to nominate
1359                return;
1360            };
1361
1362            log::debug!(
1363                "using pair {}",
1364                DisplayPair(
1365                    &self.local_candidates[pair.local],
1366                    &self.remote_candidates[pair.remote]
1367                )
1368            );
1369
1370            self.events.push_back(IceEvent::DiscoveredAddr {
1371                component,
1372                target: self.remote_candidates[pair.remote].addr,
1373            });
1374
1375            // Remove all triggered checks for this component
1376            self.triggered_check_queue
1377                .retain(|(local_id, _)| self.local_candidates[*local_id].component != component);
1378        }
1379    }
1380
1381    fn all_components_have_nominated_valid_pair(&self) -> bool {
1382        self.component_has_nominated_valid_pair(Component::Rtp)
1383            && (self.rtcp_mux || self.component_has_nominated_valid_pair(Component::Rtcp))
1384    }
1385
1386    fn component_has_nominated_valid_pair(&self, component: Component) -> bool {
1387        self.pairs.iter().any(|p| {
1388            p.component == component
1389                && p.nomination == CandidatePairNomination::Selected
1390                && matches!(
1391                    p.state,
1392                    CandidatePairState::InProgress { .. } | CandidatePairState::Succeeded
1393                )
1394        })
1395    }
1396
1397    /// Returns the next event to process
1398    ///
1399    /// This must be called until it returns None
1400    pub fn pop_event(&mut self) -> Option<IceEvent> {
1401        self.events.pop_front()
1402    }
1403
1404    /// Returns a duration after which to call [`poll`](IceAgent::poll)
1405    pub fn timeout(&self, now: Instant) -> Option<Duration> {
1406        // Next TA trigger
1407        let ta = if self.remote_credentials.is_some()
1408            && !self.all_components_have_nominated_valid_pair()
1409        {
1410            Some(
1411                self.last_ta_trigger
1412                    .map(|it| {
1413                        let poll_at = it + Duration::from_millis(50);
1414                        poll_at.checked_duration_since(now).unwrap_or_default()
1415                    })
1416                    .unwrap_or_default(),
1417            )
1418        } else {
1419            None
1420        };
1421
1422        // Pair STUN retransmit
1423        let pair_retransmit = self
1424            .pairs
1425            .iter()
1426            .filter_map(|p| {
1427                if let CandidatePairState::InProgress { retransmit_at, .. } = &p.state {
1428                    retransmit_at.checked_duration_since(now)
1429                } else {
1430                    None
1431                }
1432            })
1433            .min();
1434
1435        // Next stun binding refresh/retransmit
1436        let stun_bindings = self.stun_server.iter().filter_map(|b| b.timeout(now)).min();
1437
1438        opt_min(opt_min(ta, pair_retransmit), stun_bindings)
1439    }
1440
1441    /// Returns all discovered local ice agents, does not include peer-reflexive candidates
1442    pub fn ice_candidates(&self) -> Vec<IceCandidate> {
1443        self.local_candidates
1444            .values()
1445            .filter(|c| matches!(c.kind, CandidateKind::Host | CandidateKind::ServerReflexive))
1446            .map(|c| {
1447                let rel_addr = if c.kind == CandidateKind::ServerReflexive {
1448                    Some(c.base)
1449                } else {
1450                    None
1451                };
1452
1453                IceCandidate {
1454                    foundation: c.foundation.clone().into(),
1455                    component: c.component as _,
1456                    transport: "UDP".into(),
1457                    priority: c.priority.into(),
1458                    address: UntaggedAddress::IpAddress(c.addr.ip()),
1459                    port: c.addr.port(),
1460                    typ: match c.kind {
1461                        CandidateKind::Host => "host".into(),
1462                        CandidateKind::ServerReflexive => "srflx".into(),
1463                        _ => unreachable!(),
1464                    },
1465                    rel_addr: rel_addr.map(|addr| UntaggedAddress::IpAddress(addr.ip())),
1466                    rel_port: rel_addr.map(|addr| addr.port()),
1467                    unknown: vec![],
1468                }
1469            })
1470            .collect()
1471    }
1472}
1473
1474fn pair_priority(
1475    local_candidate: &Candidate,
1476    remote_candidate: &Candidate,
1477    is_controlling: bool,
1478) -> u64 {
1479    let (g, d) = if is_controlling {
1480        (
1481            local_candidate.priority as u64,
1482            remote_candidate.priority as u64,
1483        )
1484    } else {
1485        (
1486            remote_candidate.priority as u64,
1487            local_candidate.priority as u64,
1488        )
1489    };
1490
1491    // pair priority = 2^32*MIN(G,D) + 2*MAX(G,D) + (G>D?1:0)
1492    2u64.pow(32) * min(g, d) + 2 * max(g, d) + if g > d { 1 } else { 0 }
1493}
1494
1495fn compute_foundation(
1496    kind: CandidateKind,
1497    base: IpAddr,
1498    // for server-reflexive candidates
1499    stun_server: Option<SocketAddr>,
1500    proto: &str,
1501) -> u64 {
1502    let mut hasher = DefaultHasher::new();
1503    (kind, base, stun_server, proto).hash(&mut hasher);
1504    hasher.finish()
1505}
1506
1507struct DisplayPair<'a>(&'a Candidate, &'a Candidate);
1508
1509impl fmt::Display for DisplayPair<'_> {
1510    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1511        fmt_candidate(f, self.0)?;
1512        write!(f, " <-> ")?;
1513        fmt_candidate(f, self.1)
1514    }
1515}
1516
1517struct DisplayCandidate<'a>(&'a Candidate);
1518
1519impl fmt::Display for DisplayCandidate<'_> {
1520    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1521        fmt_candidate(f, self.0)
1522    }
1523}
1524
1525fn fmt_candidate(f: &mut fmt::Formatter<'_>, c: &Candidate) -> fmt::Result {
1526    match c.kind {
1527        CandidateKind::Host => write!(f, "host({})", c.addr),
1528        CandidateKind::PeerReflexive => {
1529            write!(f, "peer-reflexive(base:{}, peer:{})", c.base, c.addr)
1530        }
1531        CandidateKind::ServerReflexive => {
1532            write!(f, "server-reflexive(base:{}, server:{})", c.base, c.addr)
1533        } // CandidateKind::Relayed => write!(f, "relayed(base:{}, relay:{})", c.base, c.addr),
1534    }
1535}
1536
1537fn opt_min<T: Ord>(a: Option<T>, b: Option<T>) -> Option<T> {
1538    match (a, b) {
1539        (None, None) => None,
1540        (None, Some(b)) => Some(b),
1541        (Some(a), None) => Some(a),
1542        (Some(a), Some(b)) => Some(min(a, b)),
1543    }
1544}