Skip to main content

sipx_sdp/
ice.rs

1//! ICE in SDP: the attributes RFC 8839 §5 defines.
2//!
3//! This is the signalling half of ICE and nothing else. The agent, the connectivity checks and
4//! the sockets live in `sipx-media`; what belongs here is the grammar, because it is pure
5//! parsing — no clock, no socket, no runtime — and because the rest of ICE should reason about a
6//! typed description rather than search an SDP body for substrings.
7//!
8//! Two rules run through the whole module, and both exist because the alternative breaks calls
9//! with peers that are behaving perfectly legally.
10//!
11//! **A line sipx cannot use is ignored, not fatal.** RFC 8839 §5.1's `connection-address` admits
12//! an FQDN and its `transport` admits any token, so a description may carry candidates sipx has
13//! no way to check. The candidate is dropped and the description survives. A parser that fails
14//! the whole body on one such line refuses a call over a candidate it was never asked to use.
15//!
16//! **A `priority` is range-checked on parse.** The grammar is `1*10DIGIT`, so `4294967295` is
17//! well-formed text, but §5.1 bounds the value at 2^31 − 1. RFC 8445 §6.1.2.3 then combines two
18//! priorities as `2^32*MIN(G,D) + 2*MAX(G,D) + (G>D?1:0)`, and that expression leaves `u64` for
19//! operands near `u32::MAX` — `4294967295` on both sides is the case that overflows, which is
20//! exactly the value the grammar admits and the range forbids. Checking on parse is what keeps
21//! the value that would wrap from ever reaching the arithmetic; see [`Priority`] for the
22//! headroom the bound actually buys, and [`docs/specs/ice.md`] §4 and §6.2.
23//!
24//! [`docs/specs/ice.md`]: https://github.com/codewandler/sipx/blob/main/docs/specs/ice.md
25
26use std::fmt::{self, Write as _};
27use std::net::IpAddr;
28
29/// The `ice2` option tag every RFC 8839 agent must advertise (§5.6).
30pub const ICE2: &str = "ice2";
31
32/// `ice-char = ALPHA / DIGIT / "+" / "/"` (RFC 8839 §5.1).
33fn is_ice_char(c: char) -> bool {
34    c.is_ascii_alphanumeric() || c == '+' || c == '/'
35}
36
37/// Whether `text` is `min*max ice-char`. Lengths are in characters, which for `ice-char` is the
38/// same as bytes.
39fn is_ice_chars(text: &str, min: usize, max: usize) -> bool {
40    let len = text.len();
41    len >= min && len <= max && text.chars().all(is_ice_char)
42}
43
44/// A candidate foundation: `1*32ice-char` (RFC 8839 §5.1).
45///
46/// The value is opaque on the wire — RFC 8445 §5.1.1.3 gives it meaning only by equality, two
47/// candidates sharing a foundation being unfrozen together. It is a type rather than a `String`
48/// so the length and character-set bound cannot be lost between parsing and re-emitting.
49#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
50pub struct Foundation(String);
51
52impl Foundation {
53    /// The foundation a token names, if the token is one.
54    pub fn new(token: &str) -> Option<Self> {
55        is_ice_chars(token, 1, 32).then(|| Self(token.to_owned()))
56    }
57
58    /// The token as it appears in SDP.
59    pub fn as_str(&self) -> &str {
60        &self.0
61    }
62}
63
64impl fmt::Display for Foundation {
65    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66        f.write_str(&self.0)
67    }
68}
69
70/// Which component of a media stream a candidate is for (RFC 8839 §5.1).
71///
72/// A number and not an enum of `Rtp`/`Rtcp`: §5.1 makes it `1*3DIGIT` between 1 and 256, and a
73/// stream may have components sipx does not itself offer. Refusing a candidate for component 3
74/// would drop a line the peer is entitled to send.
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
76pub struct ComponentId(u16);
77
78impl ComponentId {
79    /// RTP, which is component 1.
80    pub const RTP: Self = Self(1);
81    /// RTCP, which is component 2.
82    pub const RTCP: Self = Self(2);
83
84    /// The component with this identifier, if §5.1's 1–256 range admits it.
85    pub fn new(id: u16) -> Option<Self> {
86        (1..=256).contains(&id).then_some(Self(id))
87    }
88
89    /// The identifier as it appears in SDP.
90    pub fn get(self) -> u16 {
91        self.0
92    }
93}
94
95impl fmt::Display for ComponentId {
96    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97        self.0.fmt(f)
98    }
99}
100
101/// The transport a candidate names.
102///
103/// One variant, deliberately. RFC 8839 §5.1's grammar is `"UDP" / transport-extension`, and sipx
104/// checks candidates over UDP only — so a candidate naming anything else parses as far as this
105/// type and is then dropped by [`Candidate::parse`], rather than failing the description. A peer
106/// offering an ICE-TCP candidate alongside UDP ones is offering something usable.
107#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
108pub enum Transport {
109    /// UDP, the only transport sipx checks over.
110    Udp,
111}
112
113impl Transport {
114    /// The token as it appears in SDP.
115    pub fn as_str(self) -> &'static str {
116        match self {
117            Self::Udp => "UDP",
118        }
119    }
120
121    /// The transport a token names, if it is one sipx can check over.
122    ///
123    /// Case-insensitive: ABNF string literals are, and RFC 5245 — which this grammar is inherited
124    /// from — printed its own examples in lower case.
125    pub fn parse(token: &str) -> Option<Self> {
126        token
127            .eq_ignore_ascii_case(Self::Udp.as_str())
128            .then_some(Self::Udp)
129    }
130}
131
132/// A candidate priority: a positive integer up to 2^31 − 1 (RFC 8839 §5.1).
133///
134/// The bound is the type's whole reason for existing. RFC 8445 §6.1.2.3 combines two priorities
135/// into a pair priority as `2^32*MIN(G,D) + 2*MAX(G,D) + (G>D?1:0)`. With both operands at
136/// 2^31 − 1 that comes to `2^63 − 2`: the `G > D` term is zero when the two are equal, so the
137/// `2^63 − 1` upper bound is approached and never reached, and every in-range pair therefore has
138/// half a `u64` of headroom.
139///
140/// Carry an unchecked `u32` from the wire into the same expression and the headroom is spent.
141/// The overflow is not one step past the bound — the arithmetic is still exact at 4294967294 —
142/// but `4294967295` on both sides, the ten-digit value `1*10DIGIT` admits, comes to `2^64 + 2^32
143/// − 2` and wraps. In a release build it wraps silently, reordering the checklist that the
144/// arithmetic exists to order. `the_priority_bound_is_what_keeps_the_pair_priority_in_a_u64`
145/// asserts both halves of this.
146#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
147pub struct Priority(u32);
148
149impl Priority {
150    /// The largest priority RFC 8839 §5.1 permits.
151    pub const MAX: Self = Self(0x7fff_ffff);
152    /// The smallest. §5.1 says "positive", so zero is not a priority.
153    pub const MIN: Self = Self(1);
154
155    /// The priority with this value, if it is in range.
156    pub fn new(value: u32) -> Option<Self> {
157        (Self::MIN.0..=Self::MAX.0)
158            .contains(&value)
159            .then_some(Self(value))
160    }
161
162    /// Read a `priority` production.
163    ///
164    /// `1*10DIGIT` admits ten digits, so `4294967295` is well-formed text that is not a legal
165    /// priority. It is read wide and then range-checked, so that an out-of-range value is
166    /// rejected as out of range rather than silently truncated to something plausible.
167    pub fn parse(text: &str) -> Option<Self> {
168        let value: u64 = text.parse().ok()?;
169        Self::new(u32::try_from(value).ok()?)
170    }
171
172    /// The value.
173    pub fn get(self) -> u32 {
174        self.0
175    }
176}
177
178impl fmt::Display for Priority {
179    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
180        self.0.fmt(f)
181    }
182}
183
184/// How a candidate was obtained (RFC 8839 §5.1, RFC 8445 §5.1.1).
185#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
186pub enum CandidateType {
187    /// A local interface address.
188    Host,
189    /// An address a STUN server reported.
190    ServerReflexive,
191    /// An address learned from a peer's connectivity check.
192    PeerReflexive,
193    /// An address on a TURN relay.
194    Relayed,
195}
196
197impl CandidateType {
198    /// The token as it appears in SDP.
199    pub fn as_str(self) -> &'static str {
200        match self {
201            Self::Host => "host",
202            Self::ServerReflexive => "srflx",
203            Self::PeerReflexive => "prflx",
204            Self::Relayed => "relay",
205        }
206    }
207
208    /// The type a token names, if it is one sipx knows.
209    ///
210    /// `candidate-types` ends in `token`, so the set is extensible and a peer may name one sipx
211    /// has never heard of. `None` here makes [`Candidate::parse`] ignore the line, which is the
212    /// conservative reading rather than the only legal one: RFC 8839 §5.1 requires a document
213    /// defining a new candidate type to define how it is processed, so a type sipx does not know
214    /// is a type whose processing rules sipx does not have. Checking it as though it were a host
215    /// candidate would be guessing at those rules against a peer that published them.
216    pub fn parse(token: &str) -> Option<Self> {
217        match token {
218            "host" => Some(Self::Host),
219            "srflx" => Some(Self::ServerReflexive),
220            "prflx" => Some(Self::PeerReflexive),
221            "relay" => Some(Self::Relayed),
222            _ => None,
223        }
224    }
225}
226
227/// The `raddr`/`rport` pair a candidate may carry (RFC 8839 §5.1).
228#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
229pub struct RelatedAddress {
230    /// The related address.
231    pub address: IpAddr,
232    /// The related port.
233    pub port: u16,
234}
235
236/// One `a=candidate` line (RFC 8839 §5.1). Media-level.
237#[derive(Debug, Clone, PartialEq, Eq)]
238pub struct Candidate {
239    /// The foundation.
240    pub foundation: Foundation,
241    /// Which component of the stream this candidate is for.
242    pub component: ComponentId,
243    /// The transport. Always [`Transport::Udp`]; see the type.
244    pub transport: Transport,
245    /// The priority, range-checked on parse.
246    pub priority: Priority,
247    /// The transport address.
248    pub address: IpAddr,
249    /// The port.
250    pub port: u16,
251    /// How the candidate was obtained.
252    pub kind: CandidateType,
253    /// The `raddr`/`rport` pair.
254    ///
255    /// RFC 8839 §5.1 requires it for `srflx`, `prflx` and `relay` and forbids it for `host`, and
256    /// a candidate sipx *generates* must obey that. It is not enforced when reading: §5.1 gives
257    /// the field to "diagnostics", nothing in RFC 8445's checks consults it, and dropping a
258    /// peer's only working candidate over a diagnostic field would trade a call for a nicety. A
259    /// privacy-preserving agent writes `0.0.0.0`/`::` and port 9 here, which is ordinary.
260    pub related: Option<RelatedAddress>,
261    /// `cand-extension` name/value pairs sipx does not model, in the order they arrived.
262    ///
263    /// Kept rather than dropped, the same discipline [`crate::session`] applies to unknown SDP
264    /// lines one level up: §5.1 says unknown extensions MUST be ignored, and ignoring an
265    /// extension is not the same as deleting it from a description that is about to be relayed.
266    pub extensions: Vec<(String, String)>,
267}
268
269impl Candidate {
270    /// Read an `a=candidate` value.
271    ///
272    /// `None` means **ignore this line and keep the description** — RFC 8839 §5.1's rule for a
273    /// candidate carrying an FQDN or an address family the agent does not support, and by the
274    /// same argument for a transport or candidate type sipx cannot check over. It also covers a
275    /// line that is simply malformed, because the outcome the peer needs is identical.
276    pub fn parse(value: &str) -> Option<Self> {
277        let mut parts = value.split_whitespace();
278        let foundation = Foundation::new(parts.next()?)?;
279        let component = ComponentId::new(parts.next()?.parse().ok()?)?;
280        let transport = Transport::parse(parts.next()?)?;
281        let priority = Priority::parse(parts.next()?)?;
282        // An FQDN, or a literal of a family this build cannot represent, fails here — which is
283        // exactly §5.1's "the candidate MUST be ignored".
284        let address: IpAddr = parts.next()?.parse().ok()?;
285        let port: u16 = parts.next()?.parse().ok()?;
286        if parts.next()? != "typ" {
287            return None;
288        }
289        let kind = CandidateType::parse(parts.next()?)?;
290
291        // `rel-addr`, `rel-port` and every `cand-extension` are name/value pairs, so they are
292        // read as pairs rather than by position. The grammar puts `raddr`/`rport` first; peers
293        // that put an extension there are still understood, and a name with no value is not.
294        //
295        // `extension-att-value = *VCHAR` does admit an empty value, and this drops such a line
296        // rather than keeping the name with an empty value. That is deliberate: an empty value
297        // is only distinguishable from a missing one by a trailing space, so keeping it would
298        // make `to_value` emit a trailing space — and a round trip that adds a byte the peer did
299        // not send is a worse failure than ignoring a candidate whose extension said nothing.
300        let mut related_address = None;
301        let mut related_port = None;
302        let mut extensions = Vec::new();
303        while let Some(name) = parts.next() {
304            let value = parts.next()?;
305            match name {
306                "raddr" => related_address = Some(value.parse::<IpAddr>().ok()?),
307                "rport" => related_port = Some(value.parse::<u16>().ok()?),
308                _ => extensions.push((name.to_owned(), value.to_owned())),
309            }
310        }
311        let related = match (related_address, related_port) {
312            (Some(address), Some(port)) => Some(RelatedAddress { address, port }),
313            (None, None) => None,
314            // Half a related address is not a related address, and guessing the other half would
315            // put an address sipx invented into a description it may go on to relay.
316            _ => return None,
317        };
318
319        Some(Self {
320            foundation,
321            component,
322            transport,
323            priority,
324            address,
325            port,
326            kind,
327            related,
328            extensions,
329        })
330    }
331
332    /// Render as an `a=candidate` value.
333    pub fn to_value(&self) -> String {
334        let mut out = String::with_capacity(64);
335        let _ = write!(
336            out,
337            "{} {} {} {} {} {} typ {}",
338            self.foundation,
339            self.component,
340            self.transport.as_str(),
341            self.priority,
342            self.address,
343            self.port,
344            self.kind.as_str()
345        );
346        if let Some(related) = &self.related {
347            let _ = write!(out, " raddr {} rport {}", related.address, related.port);
348        }
349        for (name, value) in &self.extensions {
350            let _ = write!(out, " {name} {value}");
351        }
352        out
353    }
354}
355
356/// One entry of an `a=remote-candidates` line (RFC 8839 §5.2). Media-level.
357///
358/// A controlling agent includes it in an offer for a stream that is Completed, and in no other
359/// case; it names the pair it selected so the answerer can agree without a second round of
360/// checks.
361#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
362pub struct RemoteCandidate {
363    /// Which component this is the selected remote candidate for.
364    pub component: ComponentId,
365    /// Its address.
366    pub address: IpAddr,
367    /// Its port.
368    pub port: u16,
369}
370
371impl RemoteCandidate {
372    /// Read an `a=remote-candidates` value, which carries one entry per component.
373    ///
374    /// `None` for a malformed line. Unlike a candidate this is all-or-nothing: §5.2 requires a
375    /// value for *each* component, so a half-read line would name a selected pair for one
376    /// component and silently drop the other.
377    pub fn parse_list(value: &str) -> Option<Vec<Self>> {
378        let mut parts = value.split_whitespace().peekable();
379        let mut out = Vec::new();
380        while parts.peek().is_some() {
381            let component = ComponentId::new(parts.next()?.parse().ok()?)?;
382            let address: IpAddr = parts.next()?.parse().ok()?;
383            let port: u16 = parts.next()?.parse().ok()?;
384            out.push(Self {
385                component,
386                address,
387                port,
388            });
389        }
390        (!out.is_empty()).then_some(out)
391    }
392
393    /// Render several remote candidates as one `a=remote-candidates` value.
394    pub fn to_value(candidates: &[Self]) -> String {
395        let mut out = String::with_capacity(candidates.len() * 24);
396        for candidate in candidates {
397            if !out.is_empty() {
398                out.push(' ');
399            }
400            let _ = write!(
401                out,
402                "{} {} {}",
403                candidate.component, candidate.address, candidate.port
404            );
405        }
406        out
407    }
408}
409
410/// The short-term credentials for a stream: `ice-ufrag` and `ice-pwd` (RFC 8839 §5.4).
411///
412/// The two travel together because §5.4 requires both for every data stream, whether they are
413/// written at session or media level, and because RFC 8445 §7.1.2 keys a connectivity check's
414/// `MESSAGE-INTEGRITY` on the password that goes with a particular fragment. A type carrying one
415/// without the other would be a credential that cannot authenticate anything.
416///
417/// The fields are private so the length bounds cannot be lost after construction: the send and
418/// receive bounds differ, and which one applied is a property of how the value arrived.
419#[derive(Debug, Clone, PartialEq, Eq)]
420pub struct Credentials {
421    ufrag: String,
422    pwd: String,
423}
424
425impl Credentials {
426    /// The longest `ice-ufrag` or `ice-pwd` §5.4 permits sipx to **send**.
427    pub const MAX_SENT_LEN: usize = 32;
428    /// The longest either that sipx must **accept** on receive.
429    pub const MAX_ACCEPTED_LEN: usize = 256;
430    /// The shortest `ice-ufrag` the grammar admits.
431    pub const MIN_UFRAG_LEN: usize = 4;
432    /// The shortest `ice-pwd` the grammar admits.
433    pub const MIN_PWD_LEN: usize = 22;
434
435    /// Credentials sipx will put in an offer or answer.
436    ///
437    /// `None` when either value is outside what §5.4 permits to be sent: "MUST NOT be longer
438    /// than 32 characters when sending, but an implementation MUST accept up to 256 characters
439    /// when receiving". The asymmetry is why this is a different constructor from
440    /// [`Credentials::received`] rather than a flag — sending 200 characters is a defect, and
441    /// receiving them is Tuesday.
442    pub fn new(ufrag: impl Into<String>, pwd: impl Into<String>) -> Option<Self> {
443        Self::checked(ufrag.into(), pwd.into(), Self::MAX_SENT_LEN)
444    }
445
446    /// Credentials read from a peer's description.
447    pub fn received(ufrag: impl Into<String>, pwd: impl Into<String>) -> Option<Self> {
448        Self::checked(ufrag.into(), pwd.into(), Self::MAX_ACCEPTED_LEN)
449    }
450
451    fn checked(ufrag: String, pwd: String, max: usize) -> Option<Self> {
452        let ok = is_ice_chars(&ufrag, Self::MIN_UFRAG_LEN, max)
453            && is_ice_chars(&pwd, Self::MIN_PWD_LEN, max);
454        ok.then_some(Self { ufrag, pwd })
455    }
456
457    /// The username fragment.
458    pub fn ufrag(&self) -> &str {
459        &self.ufrag
460    }
461
462    /// The password.
463    pub fn pwd(&self) -> &str {
464        &self.pwd
465    }
466}
467
468/// The `a=ice-pacing` value: the Ta interval an agent wants, in milliseconds (RFC 8839 §5.5).
469/// Session-level.
470#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
471pub struct Pacing(u32);
472
473impl Pacing {
474    /// What §5.5 says the value is when the attribute is absent.
475    pub const DEFAULT: Self = Self(50);
476
477    /// The pacing for a number of milliseconds.
478    pub fn from_millis(millis: u32) -> Self {
479        Self(millis)
480    }
481
482    /// Read an `a=ice-pacing` value: `1*10DIGIT`.
483    pub fn parse(text: &str) -> Option<Self> {
484        let millis: u64 = text.parse().ok()?;
485        Some(Self(u32::try_from(millis).ok()?))
486    }
487
488    /// The interval in milliseconds.
489    pub fn millis(self) -> u32 {
490        self.0
491    }
492
493    /// What the two agents will actually use.
494    ///
495    /// §5.5: "both agents will use the larger of the indicated values". The slower agent wins,
496    /// because pacing exists to protect whichever end has less to spend.
497    #[must_use]
498    pub fn agreed(self, other: Self) -> Self {
499        Self(self.0.max(other.0))
500    }
501}
502
503impl fmt::Display for Pacing {
504    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
505        self.0.fmt(f)
506    }
507}
508
509/// Split an `a=ice-options` value into its tags (RFC 8839 §5.6).
510///
511/// A tag that is not `1*ice-char` is dropped and its neighbours are kept: the attribute is a list
512/// of independent capabilities, and one unreadable entry says nothing about the others.
513pub(crate) fn option_tags(value: &str) -> impl Iterator<Item = &str> {
514    value
515        .split_whitespace()
516        .filter(|tag| is_ice_chars(tag, 1, usize::MAX))
517}
518
519#[cfg(test)]
520#[allow(
521    clippy::unwrap_used,
522    clippy::expect_used,
523    clippy::panic,
524    clippy::indexing_slicing
525)]
526mod tests {
527    use super::*;
528
529    /// RFC 8839 §5.1's own example line, wrapped in the RFC for width and joined here.
530    const RFC_EXAMPLE: &str =
531        "2 1 UDP 1694498815 192.0.2.3 45664 typ srflx raddr 203.0.113.141 rport 8998";
532
533    /// The vector `docs/specs/ice.md` §14 names, and the one the rest of ICE is built on: if a
534    /// candidate does not survive a round trip byte-for-byte, every description sipx relays or
535    /// re-offers is a description the peer did not send.
536    #[test]
537    fn the_rfc_8839_candidate_example_round_trips_unchanged() {
538        let parsed = Candidate::parse(RFC_EXAMPLE).expect("the RFC's own example parses");
539        assert_eq!(parsed.to_value(), RFC_EXAMPLE);
540
541        assert_eq!(parsed.foundation.as_str(), "2");
542        assert_eq!(parsed.component, ComponentId::RTP);
543        assert_eq!(parsed.transport, Transport::Udp);
544        // `docs/specs/ice.md` §4's table: server-reflexive, one address, RTP.
545        assert_eq!(parsed.priority.get(), 1_694_498_815);
546        assert_eq!(parsed.address, "192.0.2.3".parse::<IpAddr>().unwrap());
547        assert_eq!(parsed.port, 45664);
548        assert_eq!(parsed.kind, CandidateType::ServerReflexive);
549        assert_eq!(
550            parsed.related,
551            Some(RelatedAddress {
552                address: "203.0.113.141".parse::<IpAddr>().unwrap(),
553                port: 8998,
554            })
555        );
556        assert!(parsed.extensions.is_empty());
557    }
558
559    /// A privacy-preserving agent writes `0.0.0.0`/`::` and port 9 rather than reveal the
560    /// address behind the NAT (RFC 8839 §5.1). It is ordinary, and it must not look malformed.
561    #[test]
562    fn a_masked_related_address_parses() {
563        for line in [
564            "1 1 UDP 1694498815 192.0.2.3 45664 typ srflx raddr 0.0.0.0 rport 9",
565            "1 1 UDP 1694498815 2001:db8::3 45664 typ relay raddr :: rport 9",
566        ] {
567            let parsed = Candidate::parse(line).expect("a masked related address is well-formed");
568            assert_eq!(parsed.to_value(), line);
569            assert_eq!(parsed.related.expect("kept").port, 9);
570        }
571    }
572
573    /// A host candidate carries no `raddr`/`rport`, and must not grow one on the way out.
574    #[test]
575    fn a_host_candidate_round_trips_without_a_related_address() {
576        let line = "1 2 UDP 2130706430 192.0.2.1 5001 typ host";
577        let parsed = Candidate::parse(line).expect("parses");
578        assert_eq!(parsed.related, None);
579        assert_eq!(parsed.component, ComponentId::RTCP);
580        assert_eq!(parsed.to_value(), line);
581    }
582
583    /// RFC 8839 §5.1 says unknown `cand-extension` pairs are ignored. Ignoring one is not the
584    /// same as deleting it: sipx re-offers descriptions, and a peer's extension has to reach the
585    /// far end intact rather than be quietly dropped by the element in the middle.
586    #[test]
587    fn an_unknown_candidate_extension_survives() {
588        let line = "3 1 UDP 2130706431 192.0.2.1 8998 typ host generation 0 network-id 4";
589        let parsed = Candidate::parse(line).expect("an unknown extension is not an error");
590        assert_eq!(
591            parsed.extensions,
592            vec![
593                ("generation".to_owned(), "0".to_owned()),
594                ("network-id".to_owned(), "4".to_owned()),
595            ]
596        );
597        assert_eq!(parsed.to_value(), line);
598    }
599
600    /// The extensions follow `raddr`/`rport` on the way out whatever order they arrived in, so
601    /// the line sipx writes matches the grammar even when the peer's did not.
602    #[test]
603    fn extensions_are_written_after_the_related_address() {
604        let parsed = Candidate::parse(
605            "1 1 UDP 100 192.0.2.1 1 typ srflx ufrag 8hhY raddr 192.0.2.9 rport 2",
606        )
607        .expect("parses");
608        assert_eq!(
609            parsed.to_value(),
610            "1 1 UDP 100 192.0.2.1 1 typ srflx raddr 192.0.2.9 rport 2 ufrag 8hhY"
611        );
612    }
613
614    /// The range check is load-bearing, not defensive. `1*10DIGIT` admits `4294967295`, and RFC
615    /// 8445 §6.1.2.3's pair priority overflows a `u64` on it — see `docs/specs/ice.md` §6.2.
616    #[test]
617    fn a_priority_the_grammar_admits_but_the_range_forbids_is_rejected() {
618        assert_eq!(
619            Priority::parse("1694498815").map(Priority::get),
620            Some(1_694_498_815)
621        );
622        assert_eq!(Priority::parse("2147483647"), Some(Priority::MAX));
623        assert_eq!(Priority::parse("1"), Some(Priority::MIN));
624
625        // Ten digits, so the grammar is satisfied; 2^31 - 1 is not.
626        assert_eq!(
627            Priority::parse("4294967295"),
628            None,
629            "u32::MAX is not a priority"
630        );
631        assert_eq!(Priority::parse("2147483648"), None, "one past 2^31 - 1");
632        assert_eq!(Priority::parse("0"), None, "§5.1 says positive");
633        assert_eq!(Priority::parse("-1"), None);
634        assert_eq!(Priority::parse(""), None);
635
636        // And the whole line goes with it, rather than the priority being clamped to something
637        // plausible: a candidate whose priority sipx invented would be ordered wrongly against
638        // the far end's copy of the same checklist.
639        assert_eq!(
640            Candidate::parse("1 1 UDP 4294967295 192.0.2.1 8998 typ host"),
641            None
642        );
643    }
644
645    /// Why the bound exists, asserted rather than asserted-in-a-comment. `docs/specs/ice.md` §6.2
646    /// bounds RFC 8445 §6.1.2.3's pair priority at `2^32*(2^31−1) + 2*(2^31−1) + 1` = `2^63 − 1`;
647    /// let the `1*10DIGIT` grammar's own worst case through unchecked instead and the identical
648    /// expression leaves `u64`. `M-21` computes it, and this is the reason it may.
649    #[test]
650    fn the_priority_bound_is_what_keeps_the_pair_priority_in_a_u64() {
651        let pair = |g: u64, d: u64| {
652            (1u64 << 32)
653                .checked_mul(g.min(d))
654                .and_then(|v| v.checked_add(2 * g.max(d)))
655                .and_then(|v| v.checked_add(u64::from(g > d)))
656        };
657        let max = u64::from(Priority::MAX.get());
658        let bound = (1u64 << 63) - 1;
659
660        // The extreme in-range pair. The `G > D` term is zero when both are at the ceiling, so
661        // the attained maximum is one below the bound §6.2 states rather than equal to it, and
662        // every in-range pair therefore has half a `u64` of headroom.
663        assert_eq!(pair(max, max), Some(bound - 1));
664        assert!(pair(max, max - 1).is_some_and(|priority| priority < bound));
665        assert!(pair(Priority::MIN.get().into(), max).is_some());
666
667        // `4294967295` — ten digits, so the grammar admits it, and `Priority::parse` does not.
668        // This is the value that reaches the arithmetic in a stack that skips the range check.
669        assert_eq!(Priority::parse("4294967295"), None);
670        assert_eq!(
671            pair(u64::from(u32::MAX), u64::from(u32::MAX)),
672            None,
673            "the value the range check keeps out is the value that overflows"
674        );
675    }
676
677    /// RFC 8839 §5.1: a candidate naming an FQDN or an address family the agent does not support
678    /// "MUST be ignored" — the line, not the description. Nor does a transport or a candidate
679    /// type sipx cannot check over take the rest of the stream down with it.
680    #[test]
681    fn a_candidate_sipx_cannot_use_is_ignored_and_the_description_survives() {
682        const OFFER: &str = concat!(
683            "v=0\r\n",
684            "o=- 1 1 IN IP4 192.0.2.1\r\n",
685            "s=-\r\n",
686            "c=IN IP4 192.0.2.1\r\n",
687            "t=0 0\r\n",
688            "m=audio 49170 RTP/AVP 0\r\n",
689            "a=ice-ufrag:8hhY\r\n",
690            "a=ice-pwd:asd88fgpdd777uzjYhagZg\r\n",
691            "a=candidate:1 1 UDP 2130706431 relay.example.com 8998 typ host\r\n",
692            "a=candidate:2 1 TCP 2130706431 192.0.2.1 8998 typ host tcptype active\r\n",
693            "a=candidate:3 1 UDP 2130706431 192.0.2.1 8998 typ mystery\r\n",
694            "a=candidate:4 1 UDP 2130706431 192.0.2.1 9000 typ host\r\n",
695        );
696
697        let offer = crate::parse(OFFER).expect("the description parses despite the four lines");
698        let stream = offer.media.first().expect("one stream");
699        let candidates = stream.ice_candidates();
700        assert_eq!(
701            candidates.len(),
702            1,
703            "only the last line is usable: {candidates:?}"
704        );
705        assert_eq!(candidates[0].foundation.as_str(), "4");
706
707        // Ignored is not deleted. Every line is still on the description and still goes out.
708        assert_eq!(offer.to_string_sdp(), OFFER);
709    }
710
711    /// The transport case in isolation: §5.1's `transport-extension` means a peer offering an
712    /// ICE-TCP candidate alongside UDP ones is offering something usable, so the TCP line is
713    /// accepted as well-formed and discarded rather than treated as a parse failure.
714    #[test]
715    fn a_transport_other_than_udp_is_discarded_and_udp_is_case_insensitive() {
716        assert_eq!(Transport::parse("UDP"), Some(Transport::Udp));
717        assert_eq!(Transport::parse("udp"), Some(Transport::Udp));
718        assert_eq!(Transport::parse("TCP"), None);
719        // Whatever case it arrived in, sipx writes the spelling the grammar prints.
720        let parsed = Candidate::parse("1 1 udp 100 192.0.2.1 8998 typ host").expect("parses");
721        assert_eq!(parsed.to_value(), "1 1 UDP 100 192.0.2.1 8998 typ host");
722    }
723
724    /// A line that is malformed rather than merely unsupported is ignored the same way — there
725    /// is no shape of `a=candidate` that costs the peer the whole description.
726    #[test]
727    fn a_malformed_candidate_is_ignored_rather_than_fatal() {
728        for line in [
729            "",
730            "1 1 UDP 2130706431 192.0.2.1 8998",
731            "1 1 UDP 2130706431 192.0.2.1 8998 host",
732            "1 0 UDP 2130706431 192.0.2.1 8998 typ host",
733            "1 257 UDP 2130706431 192.0.2.1 8998 typ host",
734            " 1 UDP 2130706431 192.0.2.1 99999 typ host",
735            "1 1 UDP 2130706431 192.0.2.1 8998 typ srflx raddr 192.0.2.9",
736            "1 1 UDP 2130706431 192.0.2.1 8998 typ host generation",
737            // `*VCHAR` admits an empty extension value, and it is still dropped: see the note in
738            // `parse`. Keeping it would cost a trailing space on every round trip.
739            "1 1 UDP 100 192.0.2.1 9 typ host generation ",
740            "th!s 1 UDP 2130706431 192.0.2.1 8998 typ host",
741        ] {
742            assert_eq!(Candidate::parse(line), None, "{line:?}");
743        }
744
745        // A trailing space on an otherwise complete line is *not* malformed — the pair loop just
746        // ends — and it must not pick up an empty extension on the way out.
747        let padded = Candidate::parse("1 1 UDP 100 192.0.2.1 9 typ host ").expect("parses");
748        assert!(padded.extensions.is_empty());
749        assert_eq!(padded.to_value(), "1 1 UDP 100 192.0.2.1 9 typ host");
750    }
751
752    /// RFC 8839 §5.2's example lines, and the rule that they are read at media level only.
753    #[test]
754    fn remote_candidates_name_one_address_per_component() {
755        let one = RemoteCandidate::parse_list("1 192.0.2.3 45664").expect("parses");
756        let two = RemoteCandidate::parse_list("2 192.0.2.3 45665").expect("parses");
757        assert_eq!(one[0].component, ComponentId::RTP);
758        assert_eq!(two[0].component, ComponentId::RTCP);
759        assert_eq!(RemoteCandidate::to_value(&one), "1 192.0.2.3 45664");
760
761        // Several may share one line, which is what the `0*(SP remote-candidate)` is for.
762        let both =
763            RemoteCandidate::parse_list("1 192.0.2.3 45664 2 192.0.2.3 45665").expect("parses");
764        assert_eq!(both.len(), 2);
765        assert_eq!(
766            RemoteCandidate::to_value(&both),
767            "1 192.0.2.3 45664 2 192.0.2.3 45665"
768        );
769
770        // All or nothing: §5.2 requires a value for each component, so half a line would claim a
771        // selected pair for one component and silently drop the other.
772        assert_eq!(
773            RemoteCandidate::parse_list("1 192.0.2.3 45664 2 192.0.2.3"),
774            None
775        );
776        assert_eq!(RemoteCandidate::parse_list(""), None);
777    }
778
779    /// RFC 8839 §5.4's own example values, and the length bounds — which are asymmetric on
780    /// purpose: 32 characters is what sipx may send, 256 is what it must accept.
781    #[test]
782    fn credentials_are_short_to_send_and_long_to_accept() {
783        let credentials =
784            Credentials::new("8hhY", "asd88fgpdd777uzjYhagZg").expect("§5.4's example");
785        assert_eq!(credentials.ufrag(), "8hhY");
786        assert_eq!(credentials.pwd(), "asd88fgpdd777uzjYhagZg");
787
788        let long_ufrag = "u".repeat(33);
789        let long_pwd = "p".repeat(200);
790        assert_eq!(
791            Credentials::new(&long_ufrag, "asd88fgpdd777uzjYhagZg"),
792            None,
793            "33 sent"
794        );
795        assert!(
796            Credentials::received(&long_ufrag, &long_pwd).is_some(),
797            "up to 256 must be accepted"
798        );
799        assert_eq!(Credentials::received("u".repeat(257), &long_pwd), None);
800        assert_eq!(Credentials::received(&long_ufrag, "p".repeat(257)), None);
801
802        // Below the grammar's floor, at either end.
803        assert_eq!(Credentials::received("8hh", "asd88fgpdd777uzjYhagZg"), None);
804        assert_eq!(Credentials::received("8hhY", "tooshort"), None);
805        // `ice-char` is ALPHA / DIGIT / "+" / "/" and nothing else.
806        assert_eq!(
807            Credentials::received("8h:Y", "asd88fgpdd777uzjYhagZg"),
808            None
809        );
810    }
811
812    fn description(session: &str, media: &str) -> crate::session::SessionDescription {
813        let text = format!(
814            "v=0\r\no=- 1 1 IN IP4 192.0.2.1\r\ns=-\r\nc=IN IP4 192.0.2.1\r\nt=0 0\r\n{session}m=audio 49170 RTP/AVP 0\r\n{media}"
815        );
816        crate::parse(&text).expect("parses")
817    }
818
819    /// RFC 8839 §5.4: both levels are allowed and the media level wins. The pair is taken from
820    /// one level or the other and never mixed — a fragment from the `m=` line with a password
821    /// from the session line authenticates nothing, and fails looking like a network fault.
822    #[test]
823    fn media_level_credentials_win_and_are_never_mixed_with_the_session_level() {
824        let both = description(
825            "a=ice-ufrag:sess\r\na=ice-pwd:sessionpasswordlongenough\r\n",
826            "a=ice-ufrag:8hhY\r\na=ice-pwd:asd88fgpdd777uzjYhagZg\r\n",
827        );
828        let stream = both.media.first().expect("one stream");
829        let credentials = both.ice_credentials_for(stream).expect("present");
830        assert_eq!(credentials.ufrag(), "8hhY");
831        assert_eq!(credentials.pwd(), "asd88fgpdd777uzjYhagZg");
832
833        // Session level is a default for a stream that declares nothing.
834        let inherited = description(
835            "a=ice-ufrag:sess\r\na=ice-pwd:sessionpasswordlongenough\r\n",
836            "",
837        );
838        let stream = inherited.media.first().expect("one stream");
839        let credentials = inherited.ice_credentials_for(stream).expect("inherited");
840        assert_eq!(credentials.ufrag(), "sess");
841        assert_eq!(credentials.pwd(), "sessionpasswordlongenough");
842
843        // Half a pair at the media level falls back to the session's *pair*, not to its password.
844        let half = description(
845            "a=ice-ufrag:sess\r\na=ice-pwd:sessionpasswordlongenough\r\n",
846            "a=ice-ufrag:8hhY\r\n",
847        );
848        let stream = half.media.first().expect("one stream");
849        let credentials = half.ice_credentials_for(stream).expect("falls back whole");
850        assert_eq!(credentials.ufrag(), "sess");
851        assert_eq!(credentials.pwd(), "sessionpasswordlongenough");
852
853        // No ICE credentials anywhere means the stream is not doing ICE (§5.4).
854        let none = description("", "");
855        let stream = none.media.first().expect("one stream");
856        assert_eq!(none.ice_credentials_for(stream), None);
857    }
858
859    /// Each attribute is read at the level RFC 8839 defines it at, and nowhere else. A
860    /// media-level `a=ice-lite` does not make a peer lite — §5.3 puts it at session level, and
861    /// honouring it anywhere would let one stream change how the whole agent is treated.
862    #[test]
863    fn each_attribute_is_read_only_at_the_level_that_defines_it() {
864        let right = description("a=ice-lite\r\na=ice-pacing:100\r\n", "a=ice-mismatch\r\n");
865        let stream = right.media.first().expect("one stream");
866        assert!(right.is_ice_lite());
867        assert_eq!(right.ice_pacing(), Pacing::from_millis(100));
868        assert!(stream.ice_mismatch());
869
870        let wrong = description("a=ice-mismatch\r\n", "a=ice-lite\r\na=ice-pacing:100\r\n");
871        let stream = wrong.media.first().expect("one stream");
872        assert!(!wrong.is_ice_lite(), "§5.3 puts ice-lite at session level");
873        assert!(
874            !stream.ice_mismatch(),
875            "§5.3 puts ice-mismatch at media level"
876        );
877        assert_eq!(
878            wrong.ice_pacing(),
879            Pacing::DEFAULT,
880            "§5.5 puts ice-pacing at session level"
881        );
882
883        // `candidate` and `remote-candidates` are media-level (§5.1, §5.2): a session-level copy
884        // is not a candidate for any stream.
885        let stray = description(
886            "a=candidate:1 1 UDP 2130706431 192.0.2.1 8998 typ host\r\na=remote-candidates:1 192.0.2.3 45664\r\n",
887            "",
888        );
889        let stream = stray.media.first().expect("one stream");
890        assert!(stream.ice_candidates().is_empty());
891        assert!(stream.ice_remote_candidates().is_empty());
892    }
893
894    /// §5.5: absent means 50 ms, and the two agents use the larger of what they asked for — the
895    /// slower end wins, because pacing protects whichever end has less to spend.
896    #[test]
897    fn pacing_defaults_to_50_and_the_larger_value_is_agreed() {
898        let silent = description("", "");
899        assert_eq!(silent.ice_pacing(), Pacing::DEFAULT);
900        assert_eq!(Pacing::DEFAULT.millis(), 50);
901        assert_eq!(Pacing::parse("100"), Some(Pacing::from_millis(100)));
902        assert_eq!(Pacing::parse("banana"), None);
903        assert_eq!(
904            Pacing::DEFAULT.agreed(Pacing::from_millis(200)),
905            Pacing::from_millis(200)
906        );
907        assert_eq!(
908            Pacing::from_millis(200).agreed(Pacing::DEFAULT),
909            Pacing::from_millis(200)
910        );
911        // An unreadable value takes the default rather than nothing: §5.5 gives the absent case
912        // a value, and a pacing of "unknown" has no meaning to give the checks.
913        let broken = description("a=ice-pacing:not-a-number\r\n", "");
914        assert_eq!(broken.ice_pacing(), Pacing::DEFAULT);
915    }
916
917    /// §5.6: option tags may appear at both levels, and both count. Unlike the credentials this
918    /// is a union — an agent does not stop supporting an extension because one `m=` line named a
919    /// different one.
920    #[test]
921    fn option_tags_are_read_from_both_levels() {
922        let offer = description(
923            "a=ice-options:ice2\r\n",
924            "a=ice-options:rtp+ecn trickle\r\n",
925        );
926        let stream = offer.media.first().expect("one stream");
927        assert_eq!(offer.ice_options().collect::<Vec<_>>(), vec![ICE2]);
928        assert_eq!(
929            stream.ice_options().collect::<Vec<_>>(),
930            vec!["rtp+ecn", "trickle"]
931        );
932        assert_eq!(
933            offer.ice_options_for(stream).collect::<Vec<_>>(),
934            vec![ICE2, "rtp+ecn", "trickle"]
935        );
936        // A tag outside `1*ice-char` is dropped and its neighbours are kept: the attribute is a
937        // list of independent capabilities.
938        let ragged = description("a=ice-options:ice2 b@d trickle\r\n", "");
939        assert_eq!(
940            ragged.ice_options().collect::<Vec<_>>(),
941            vec![ICE2, "trickle"]
942        );
943    }
944
945    /// A foundation is `1*32ice-char`, and the bound is kept by the type rather than by whoever
946    /// remembers to check it.
947    #[test]
948    fn a_foundation_keeps_its_bounds() {
949        assert_eq!(
950            Foundation::new("2").map(|f| f.as_str().to_owned()),
951            Some("2".to_owned())
952        );
953        assert!(Foundation::new(&"a".repeat(32)).is_some());
954        assert!(Foundation::new(&"a".repeat(33)).is_none());
955        assert!(Foundation::new("").is_none());
956        assert!(Foundation::new("a b").is_none());
957        assert!(Foundation::new("+/").is_some(), "ice-char includes + and /");
958    }
959}