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}