Skip to main content

sipx_sdp/
answer.rs

1//! Offer/answer (RFC 3264).
2//!
3//! A pure function. The rules here are full of cases that are awkward to reach through a live
4//! call — a stream with no common codec, an offer that reorders media, a `sendonly` that must
5//! become `recvonly` — and they are all one function call away.
6//!
7//! The rule that shapes everything: **the answer has the same number of `m=` lines as the
8//! offer, in the same order.** A stream that cannot be accepted is answered with port 0, not
9//! omitted. Omitting it shifts every stream after it, so the two ends disagree about which
10//! stream is which — and that is a call where video arrives on the audio port.
11
12use std::net::IpAddr;
13
14use crate::session::{
15    Attribute, Connection, Direction, MediaDescription, Origin, SessionDescription, Timing,
16};
17
18/// What this side can do.
19#[derive(Debug, Clone)]
20pub struct Capabilities {
21    /// Where to receive media.
22    pub address: IpAddr,
23    /// The port to receive audio on. Zero rejects audio entirely.
24    pub audio_port: u16,
25    /// Payload types this side supports, as offered.
26    pub audio_formats: Vec<String>,
27    /// `rtpmap` values by payload type, for the formats above.
28    pub rtpmaps: Vec<(String, String)>,
29    /// The direction this side wants.
30    pub direction: Direction,
31    /// The session identifier to use.
32    pub session_id: u64,
33    /// The session version to use.
34    pub session_version: u64,
35    /// The SRTP keying this side offers, if the media is to be encrypted (RFC 4568).
36    ///
37    /// `None` means plain RTP. It is `None` unless the signalling is secure, because
38    /// [`crate::crypto::Crypto::offer`] will not produce a key over a path that anyone can read.
39    pub crypto: Option<crate::crypto::Crypto>,
40    /// The certificate fingerprint this side offers, if the media is to be keyed with DTLS-SRTP
41    /// (RFC 5763 / 8122).
42    ///
43    /// Exclusive with `crypto`: they are different `m=` protocols, and a stream cannot be keyed
44    /// both ways at once.
45    pub dtls: Option<crate::fingerprint::Fingerprint>,
46    /// Whether this side can put RTP and RTCP on the media port (RFC 5761).
47    pub rtcp_mux: bool,
48    /// DTLS roles the local handshake can hold.
49    pub dtls_setup: crate::fingerprint::SetupCapabilities,
50}
51
52impl Capabilities {
53    /// The G.711 pair plus RFC 4733 DTMF, which is what practically every endpoint accepts.
54    #[must_use]
55    pub fn g711(address: IpAddr, audio_port: u16) -> Self {
56        Self {
57            address,
58            audio_port,
59            audio_formats: vec!["0".to_owned(), "8".to_owned(), "101".to_owned()],
60            rtpmaps: vec![
61                ("0".to_owned(), "PCMU/8000".to_owned()),
62                ("8".to_owned(), "PCMA/8000".to_owned()),
63                ("101".to_owned(), "telephone-event/8000".to_owned()),
64            ],
65            direction: Direction::SendRecv,
66            session_id: 1,
67            session_version: 1,
68            crypto: None,
69            dtls: None,
70            rtcp_mux: false,
71            dtls_setup: crate::fingerprint::SetupCapabilities::both(),
72        }
73    }
74
75    /// Opus first, then the G.711 pair, then DTMF.
76    ///
77    /// Order matters in an *offer* — it is how this side says what it would rather use — and
78    /// not in an answer, where RFC 3264 §6.1 gives the order to the offerer. So this is the
79    /// list to offer with; answering an offer that puts G.711 first still answers G.711 first,
80    /// which is the point.
81    ///
82    /// G.711 stays in the list rather than being replaced. Opus is better when both ends have
83    /// it and useless when they do not, and an endpoint that offered only Opus would fail to
84    /// call most of the telephone network.
85    ///
86    /// The payload type is 111 by convention rather than by standard: Opus has no static type
87    /// (RFC 7587 §7 assigns none), so the number means nothing on its own and the `rtpmap` is
88    /// what the far end matches on. 48000/2 is likewise fixed by RFC 7587 §7 regardless of the
89    /// rate the audio is sampled at or the number of channels actually sent.
90    #[must_use]
91    pub fn with_opus(address: IpAddr, audio_port: u16) -> Self {
92        Self {
93            address,
94            audio_port,
95            audio_formats: vec![
96                "111".to_owned(),
97                "0".to_owned(),
98                "8".to_owned(),
99                "101".to_owned(),
100            ],
101            rtpmaps: vec![
102                ("111".to_owned(), "opus/48000/2".to_owned()),
103                ("0".to_owned(), "PCMU/8000".to_owned()),
104                ("8".to_owned(), "PCMA/8000".to_owned()),
105                ("101".to_owned(), "telephone-event/8000".to_owned()),
106            ],
107            direction: Direction::SendRecv,
108            session_id: 1,
109            session_version: 1,
110            crypto: None,
111            dtls: None,
112            rtcp_mux: false,
113            dtls_setup: crate::fingerprint::SetupCapabilities::both(),
114        }
115    }
116
117    /// The same capabilities, offering SRTP.
118    ///
119    /// `secure_signalling` decides whether a key is generated at all: SDES carries the master
120    /// key in the SDP body, so offering one over cleartext SIP publishes it (RFC 4568 §7.1).
121    /// Passing `false` therefore leaves the offer as plain RTP rather than offering encryption
122    /// that would not be encryption.
123    #[must_use]
124    pub fn with_srtp(mut self, secure_signalling: bool) -> Self {
125        self.crypto = crate::crypto::Crypto::offer(
126            1,
127            crate::crypto::Suite::AesCm128HmacSha1_80,
128            secure_signalling,
129        );
130        self
131    }
132
133    /// The same capabilities, offering DTLS-SRTP (RFC 5763).
134    ///
135    /// `fingerprint` is of the certificate this endpoint will present on the media path. Unlike
136    /// [`Capabilities::with_srtp`] there is no `secure_signalling` flag, and its absence is the
137    /// point: SDES needs one because the SDP *carries the key*, and here it carries only a hash of
138    /// a certificate. That is what makes DTLS-SRTP usable over signalling sipx does not control —
139    /// a proxy that terminates the TLS learns nothing it can decrypt with.
140    ///
141    /// What it can do is substitute a fingerprint of its own. RFC 8122 §7 says so plainly, and it
142    /// is the reason this is a keying improvement rather than an authentication one.
143    ///
144    /// The role offered is `actpass`, which RFC 5763 §5 requires of an offerer: the answerer picks,
145    /// and picking `active` means *its* `ClientHello` opens the NAT it sits behind.
146    #[must_use]
147    pub fn with_dtls_srtp(mut self, fingerprint: crate::fingerprint::Fingerprint) -> Self {
148        self.dtls = Some(fingerprint);
149        // Mutually exclusive with SDES rather than additive. They are different `m=` protocols,
150        // so an offer cannot propose both on one stream, and leaving a stale `a=crypto` in place
151        // would put a master key in an SDP whose whole purpose is not to carry one.
152        self.crypto = None;
153        self
154    }
155
156    /// Offer or answer RTP/RTCP multiplexing on the media port (RFC 5761).
157    #[must_use]
158    pub fn with_rtcp_mux(mut self) -> Self {
159        self.rtcp_mux = true;
160        self
161    }
162
163    /// Limit the DTLS setup roles this capability set may negotiate.
164    #[must_use]
165    pub fn with_dtls_setup_capabilities(
166        mut self,
167        setup: crate::fingerprint::SetupCapabilities,
168    ) -> Self {
169        self.dtls_setup = setup;
170        self
171    }
172
173    /// The media transport this side offers.
174    ///
175    /// `UDP/TLS/RTP/SAVP` for DTLS-SRTP (RFC 5764 §8), `RTP/SAVP` for SDES, `RTP/AVP` otherwise.
176    /// The token is not decoration: it is what tells the far end which keying to expect, and an
177    /// `RTP/SAVP` line with an `a=fingerprint` describes a stream nobody can key.
178    #[must_use]
179    pub fn protocol(&self) -> &'static str {
180        if self.dtls.is_some() {
181            "UDP/TLS/RTP/SAVP"
182        } else if self.crypto.is_some() {
183            "RTP/SAVP"
184        } else {
185            "RTP/AVP"
186        }
187    }
188
189    /// The fingerprint this side offers, if it is offering DTLS-SRTP.
190    #[must_use]
191    pub fn dtls(&self) -> Option<&crate::fingerprint::Fingerprint> {
192        self.dtls.as_ref()
193    }
194
195    fn rtpmap_for(&self, format: &str) -> Option<&str> {
196        self.rtpmaps
197            .iter()
198            .find(|(payload, _)| payload == format)
199            .map(|(_, value)| value.as_str())
200    }
201}
202
203/// Build an answer to an offer.
204///
205/// Returns a description whose media lines correspond one to one with the offer's.
206#[must_use]
207pub fn answer(offer: &SessionDescription, capabilities: &Capabilities) -> SessionDescription {
208    let mut media = Vec::with_capacity(offer.media.len());
209
210    for offered in &offer.media {
211        media.push(answer_stream(offer, offered, capabilities));
212    }
213
214    SessionDescription {
215        origin: Origin::new(
216            capabilities.address,
217            capabilities.session_id,
218            capabilities.session_version,
219        ),
220        session_name: "-".to_owned(),
221        connection: Some(Connection::new(capabilities.address)),
222        timing: vec![Timing::default()],
223        attributes: Vec::new(),
224        media,
225        other: Vec::new(),
226    }
227}
228
229fn answer_stream(
230    offer: &SessionDescription,
231    offered: &MediaDescription,
232    capabilities: &Capabilities,
233) -> MediaDescription {
234    // An offer that already rejected the stream is answered with a rejection. Reviving it
235    // would be answering a question that was not asked.
236    if offered.is_rejected() {
237        return rejected(offered);
238    }
239
240    // sipx handles audio. Anything else is declined rather than ignored — declining keeps the
241    // media lines aligned, which is the whole point.
242    if offered.media != "audio" || capabilities.audio_port == 0 {
243        return rejected(offered);
244    }
245
246    // A secure offer answered without a key would be answered with encryption neither side can
247    // perform; a secure offer answered in the clear would be a downgrade this side chose. Both
248    // are worse than declining the stream, which is what RFC 4568 §7.1 leaves as the option.
249    //
250    // Three keyings, decided by the `m=` protocol token because that is what the token is for.
251    // `UDP/TLS/RTP/SAVP` is DTLS-SRTP (RFC 5764 §8), a bare `SAVP` is SDES (RFC 4568), and
252    // anything else is plain RTP.
253    let dtls_offer = offered.protocol.contains("TLS");
254    let secure_offer = offered.protocol.contains("SAVP");
255
256    let answering_dtls = match (dtls_offer, capabilities.dtls.as_ref()) {
257        // A DTLS offer carries a fingerprint, at media or session level. Without one there is
258        // nothing to check the certificate against, and RFC 8122's guarantee is exactly that
259        // check — so this is refused rather than answered with an unverifiable handshake.
260        (true, Some(ours)) if fingerprint_of(offer, offered).is_some() => Some(ours),
261        (true, _) => return rejected(offered),
262        (false, _) => None,
263    };
264
265    // The attribute accepted is the first offered one sipx can perform (RFC 4568 §5.1.2: "the
266    // answerer MUST accept exactly one"), and the answer carries **its** tag and suite with this
267    // side's own key. Emitting a tag of our own choosing is what makes a conformant offerer fail
268    // §5.1.3's check on the way back.
269    let answering_crypto = match (secure_offer && !dtls_offer, capabilities.crypto.as_ref()) {
270        (true, Some(ours)) => match offered.crypto().and_then(|theirs| ours.accepting(&theirs)) {
271            Some(accepted) => Some(accepted),
272            None => return rejected(offered),
273        },
274        (true, None) => return rejected(offered),
275        // A plain offer is answered plainly, even when this side would have preferred a key.
276        // Answering `RTP/AVP` with `a=crypto` is how a stream ends up encrypted at one end only.
277        (false, _) => None,
278    };
279
280    // RFC 3264 §6.1: the answer lists the formats both sides support. The order is the
281    // *offerer's*, because the offerer's first choice is the one it most wants used, and the
282    // answerer expressing its own preference here is how two endpoints end up transcoding for
283    // no reason.
284    let mux_agreed = capabilities.rtcp_mux && offered.rtcp_mux();
285    let common: Vec<String> = offered
286        .formats
287        .iter()
288        .filter(|format| {
289            !mux_agreed
290                || format
291                    .parse::<u8>()
292                    .map_or(true, |payload| !(64..=95).contains(&payload))
293        })
294        .filter(|format| supports(capabilities, offered, format))
295        .cloned()
296        .collect();
297
298    // A stream with nothing in common is rejected. Answering with an empty format list is not
299    // an alternative: it is syntactically invalid and says nothing.
300    if common.is_empty() || common.iter().all(|f| is_telephone_event(offered, f)) {
301        return rejected(offered);
302    }
303
304    let mut attributes = Vec::new();
305    for format in &common {
306        // Prefer the offerer's own spelling of the rtpmap when it gave one: it is
307        // authoritative for dynamic payload types, where the number means nothing on its own.
308        let rtpmap = offered
309            .rtpmap(format)
310            .or_else(|| capabilities.rtpmap_for(format));
311        if let Some(rtpmap) = rtpmap {
312            attributes.push(Attribute::valued("rtpmap", format!("{format} {rtpmap}")));
313        }
314        // RFC 4733 §2.5.1.2: the fmtp event list is per-direction — each party declares the
315        // events *it* is willing to receive, so copying the offer's list would claim events
316        // this side cannot handle. The DTMF digits and controls are events 0–15.
317        if is_telephone_event(offered, format) {
318            attributes.push(Attribute::valued("fmtp", format!("{format} 0-15")));
319        }
320    }
321
322    // The direction is a negotiation, not a copy. The answer can only narrow what the offer
323    // proposed: an offer of `sendonly` cannot be answered `sendrecv`. What was proposed is
324    // read with RFC 8866 §6.7's fallback — a stream without its own direction attribute
325    // takes the session-level one, which is exactly how hold is usually signalled.
326    let offered_direction = offered
327        .declared_direction()
328        .unwrap_or_else(|| offer.direction());
329    let direction = negotiate_direction(offered_direction, capabilities.direction);
330    attributes.push(Attribute::flag(direction.as_str()));
331
332    // RFC 5761 §5.1.3: an answer includes the flag only when it was offered and this side can
333    // honour it. Otherwise omission selects the separate-port fallback without another exchange.
334    if mux_agreed {
335        attributes.push(Attribute::flag("rtcp-mux"));
336    }
337
338    if let Some(crypto) = &answering_crypto {
339        attributes.push(Attribute::valued("crypto", crypto.to_value()));
340    }
341
342    if let Some(fingerprint) = answering_dtls {
343        attributes.push(Attribute::valued("fingerprint", fingerprint.to_value()));
344        // RFC 4145 §4.1, via RFC 5763 §5. The role is *answered*, never copied: two endpoints
345        // that both say `active` both send a `ClientHello` and neither answers one, and two that
346        // both say `passive` wait for each other until the call times out.
347        let offered_role = setup_of(offer, offered)
348            // RFC 5763 §5 requires `actpass`, but the established interoperability behavior
349            // tolerates an omitted offer role as that value.
350            .unwrap_or(crate::fingerprint::Setup::ActPass);
351        let Ok(role) = capabilities.dtls_setup.answer_to(offered_role) else {
352            return rejected(offered);
353        };
354        attributes.push(Attribute::valued("setup", role.as_str().to_owned()));
355    }
356
357    MediaDescription {
358        media: offered.media.clone(),
359        port: capabilities.audio_port,
360        protocol: offered.protocol.clone(),
361        formats: common,
362        connection: None,
363        attributes,
364        other: Vec::new(),
365    }
366}
367
368/// The fingerprint that applies to a stream: the media-level one, or the session-level fallback.
369///
370/// RFC 8122 §5 allows the attribute at either level, and a session-level value applies to every
371/// stream that does not override it. Reading only the media level is the reason a stack fails
372/// against a peer that puts one `a=fingerprint` at the top and none on the `m=` lines — which is
373/// what a browser does.
374#[must_use]
375pub fn fingerprint_of(
376    offer: &SessionDescription,
377    stream: &MediaDescription,
378) -> Option<crate::fingerprint::Fingerprint> {
379    stream.fingerprint().or_else(|| offer.fingerprint())
380}
381
382/// The setup role that applies to a stream: media level first, then the session default.
383///
384/// RFC 4145 allows `a=setup` at either level. Offer/answer and the eventual handshake must use
385/// this same resolver or a session-level answer can be signalled correctly and acted on as though
386/// it were missing.
387#[must_use]
388pub fn setup_of(
389    description: &SessionDescription,
390    stream: &MediaDescription,
391) -> Option<crate::fingerprint::Setup> {
392    stream.setup().or_else(|| {
393        description
394            .attributes
395            .iter()
396            .find(|attribute| attribute.name == "setup")
397            .and_then(|attribute| attribute.value.as_deref())
398            .and_then(crate::fingerprint::Setup::parse)
399    })
400}
401
402/// The direction an answer may carry, given what was offered and what this side wants.
403///
404/// The mirror of the offer is the *most* the answer may claim; the local preference can only
405/// narrow it further.
406#[must_use]
407pub fn negotiate_direction(offered: Direction, wanted: Direction) -> Direction {
408    let allowed = offered.mirrored();
409    let sends = allowed.sends() && wanted.sends();
410    let receives = allowed.receives() && wanted.receives();
411    match (sends, receives) {
412        (true, true) => Direction::SendRecv,
413        (true, false) => Direction::SendOnly,
414        (false, true) => Direction::RecvOnly,
415        (false, false) => Direction::Inactive,
416    }
417}
418
419fn rejected(offered: &MediaDescription) -> MediaDescription {
420    MediaDescription {
421        media: offered.media.clone(),
422        port: 0,
423        protocol: offered.protocol.clone(),
424        // A rejected stream keeps a format so the line stays well-formed; the RFC allows any
425        // single one, and echoing the offer's first is the least surprising.
426        formats: offered.formats.first().cloned().into_iter().collect(),
427        connection: None,
428        attributes: Vec::new(),
429        other: Vec::new(),
430    }
431}
432
433/// Whether this side supports a payload type.
434///
435/// A payload type means what its `rtpmap` says, and RFC 8866 §6.6 lets an offer remap even a
436/// static number — so an explicit rtpmap is authoritative whatever the number. Only a bare
437/// static type (0–95) is matched by number alone; comparing numbers when a map disagrees is
438/// how a stack agrees to a codec it cannot decode.
439///
440/// Whether two rtpmaps name the same format is [`crate::rtpmap::same_format`]'s question and not
441/// this function's. It used to be answered here too, with the clock rate compared as text while
442/// `sipx-call` parsed the same field to a number — two rules for one question, which disagreed on
443/// every spelling that is numerically equal and textually different (`M-31`).
444fn supports(capabilities: &Capabilities, offered: &MediaDescription, format: &str) -> bool {
445    if let Some(offered_map) = offered.rtpmap(format) {
446        return capabilities
447            .rtpmaps
448            .iter()
449            .any(|(_, mapping)| crate::rtpmap::same_format(offered_map, mapping));
450    }
451
452    let is_dynamic = format
453        .parse::<u8>()
454        .is_ok_and(|payload| (96..=127).contains(&payload));
455    if is_dynamic {
456        // A dynamic type with no rtpmap is uninterpretable, whatever the number.
457        return false;
458    }
459    capabilities.audio_formats.iter().any(|f| f == format)
460}
461
462fn encoding_of(rtpmap: &str) -> &str {
463    rtpmap.split('/').next().unwrap_or(rtpmap)
464}
465
466fn is_telephone_event(offered: &MediaDescription, format: &str) -> bool {
467    offered
468        .rtpmap(format)
469        .is_some_and(|mapping| encoding_of(mapping).eq_ignore_ascii_case("telephone-event"))
470}
471
472#[cfg(test)]
473#[allow(
474    clippy::unwrap_used,
475    clippy::expect_used,
476    clippy::panic,
477    clippy::indexing_slicing
478)]
479mod tests {
480    use super::*;
481    use crate::parse::parse;
482
483    fn local() -> IpAddr {
484        "192.0.2.20".parse().expect("valid")
485    }
486
487    fn offer(body: &str) -> SessionDescription {
488        parse(body).expect("the offer parses")
489    }
490
491    const AUDIO_OFFER: &str = "v=0\r\n\
492        o=alice 1 1 IN IP4 192.0.2.10\r\n\
493        s=-\r\n\
494        c=IN IP4 192.0.2.10\r\n\
495        t=0 0\r\n\
496        m=audio 49170 RTP/AVP 0 8 101\r\n\
497        a=rtpmap:0 PCMU/8000\r\n\
498        a=rtpmap:8 PCMA/8000\r\n\
499        a=rtpmap:101 telephone-event/8000\r\n\
500        a=fmtp:101 0-15\r\n\
501        a=sendrecv\r\n";
502
503    #[test]
504    fn a_plain_audio_offer_is_answered_with_the_common_codecs() {
505        let answered = answer(&offer(AUDIO_OFFER), &Capabilities::g711(local(), 40000));
506        assert_eq!(answered.media.len(), 1);
507        let audio = &answered.media[0];
508        assert_eq!(audio.port, 40000);
509        assert_eq!(audio.formats, vec!["0", "8", "101"]);
510        assert_eq!(audio.rtpmap("0"), Some("PCMU/8000"));
511        assert_eq!(audio.direction(), Direction::SendRecv);
512    }
513
514    /// The failing-first test for this story. A rejected stream stays in place with port 0;
515    /// omitting it would shift every later stream and make the two ends disagree about which
516    /// stream is which.
517    #[test]
518    fn an_answer_keeps_the_offers_media_order_and_rejects_with_port_zero() {
519        let offered = offer(
520            "v=0\r\n\
521             o=alice 1 1 IN IP4 192.0.2.10\r\n\
522             s=-\r\n\
523             c=IN IP4 192.0.2.10\r\n\
524             t=0 0\r\n\
525             m=video 49172 RTP/AVP 96\r\n\
526             a=rtpmap:96 H264/90000\r\n\
527             m=audio 49170 RTP/AVP 0\r\n\
528             a=rtpmap:0 PCMU/8000\r\n\
529             m=application 49174 udp wb\r\n",
530        );
531        let answered = answer(&offered, &Capabilities::g711(local(), 40000));
532
533        assert_eq!(answered.media.len(), 3, "one answer per offered stream");
534        assert_eq!(answered.media[0].media, "video");
535        assert_eq!(answered.media[0].port, 0, "video is declined");
536        assert_eq!(answered.media[1].media, "audio");
537        assert_eq!(answered.media[1].port, 40000, "audio is accepted, in place");
538        assert_eq!(answered.media[2].media, "application");
539        assert_eq!(answered.media[2].port, 0);
540    }
541
542    /// The order is the offerer's. An answerer that imposes its own preference is how two
543    /// endpoints end up transcoding for no reason.
544    #[test]
545    fn the_codec_order_is_the_offerers_not_ours() {
546        let offered = offer(
547            "v=0\r\no=a 1 1 IN IP4 192.0.2.10\r\ns=-\r\nc=IN IP4 192.0.2.10\r\nt=0 0\r\n\
548             m=audio 49170 RTP/AVP 8 0\r\n\
549             a=rtpmap:8 PCMA/8000\r\n\
550             a=rtpmap:0 PCMU/8000\r\n",
551        );
552        // Our own list prefers PCMU (0) first.
553        let answered = answer(&offered, &Capabilities::g711(local(), 40000));
554        assert_eq!(
555            answered.media[0].formats,
556            vec!["8", "0"],
557            "the offerer asked for PCMA first, so PCMA comes first"
558        );
559    }
560
561    #[test]
562    fn a_codec_we_do_not_have_is_left_out_of_the_answer() {
563        let offered = offer(
564            "v=0\r\no=a 1 1 IN IP4 192.0.2.10\r\ns=-\r\nc=IN IP4 192.0.2.10\r\nt=0 0\r\n\
565             m=audio 49170 RTP/AVP 0 9\r\n\
566             a=rtpmap:0 PCMU/8000\r\n\
567             a=rtpmap:9 G722/8000\r\n",
568        );
569        let answered = answer(&offered, &Capabilities::g711(local(), 40000));
570        assert_eq!(answered.media[0].formats, vec!["0"], "G.722 is not ours");
571    }
572
573    /// Nothing in common is a rejection. An answer with an empty format list is not an
574    /// alternative — it is invalid and says nothing.
575    #[test]
576    fn a_stream_with_no_common_codec_is_rejected() {
577        let offered = offer(
578            "v=0\r\no=a 1 1 IN IP4 192.0.2.10\r\ns=-\r\nc=IN IP4 192.0.2.10\r\nt=0 0\r\n\
579             m=audio 49170 RTP/AVP 9\r\n\
580             a=rtpmap:9 G722/8000\r\n",
581        );
582        let answered = answer(&offered, &Capabilities::g711(local(), 40000));
583        assert!(answered.media[0].is_rejected());
584        assert!(!answered.media[0].formats.is_empty(), "still well-formed");
585    }
586
587    /// DTMF alone is not a call. A stream offering only telephone-event has no audio codec, so
588    /// accepting it would establish a session that can never carry speech.
589    #[test]
590    fn a_stream_offering_only_dtmf_is_rejected() {
591        let offered = offer(
592            "v=0\r\no=a 1 1 IN IP4 192.0.2.10\r\ns=-\r\nc=IN IP4 192.0.2.10\r\nt=0 0\r\n\
593             m=audio 49170 RTP/AVP 101\r\n\
594             a=rtpmap:101 telephone-event/8000\r\n",
595        );
596        assert!(answer(&offered, &Capabilities::g711(local(), 40000)).media[0].is_rejected());
597    }
598
599    /// The direction is mirrored, not copied. Copying produces a call where both ends wait for
600    /// audio that never comes.
601    #[test]
602    fn directions_are_mirrored_rather_than_copied() {
603        for (offered_direction, expected) in [
604            ("sendrecv", Direction::SendRecv),
605            ("sendonly", Direction::RecvOnly),
606            ("recvonly", Direction::SendOnly),
607            ("inactive", Direction::Inactive),
608        ] {
609            let offered = offer(&format!(
610                "v=0\r\no=a 1 1 IN IP4 192.0.2.10\r\ns=-\r\nc=IN IP4 192.0.2.10\r\nt=0 0\r\n\
611                 m=audio 49170 RTP/AVP 0\r\n\
612                 a=rtpmap:0 PCMU/8000\r\n\
613                 a={offered_direction}\r\n"
614            ));
615            assert_eq!(
616                answer(&offered, &Capabilities::g711(local(), 40000)).media[0].direction(),
617                expected,
618                "offer of {offered_direction}"
619            );
620        }
621    }
622
623    /// RFC 8866 §6.7: a session-level direction applies to every stream that does not
624    /// override it at media level. RFC 3264 §6.1: a stream offered `sendonly` MUST be
625    /// answered `recvonly` or `inactive` — and hold is signalled exactly this way, with a
626    /// single session-level `a=sendonly`.
627    #[test]
628    fn a_session_level_direction_governs_streams_without_their_own() {
629        let offered = offer(
630            "v=0\r\no=a 1 1 IN IP4 192.0.2.10\r\ns=-\r\nc=IN IP4 192.0.2.10\r\nt=0 0\r\n\
631             a=sendonly\r\n\
632             m=audio 49170 RTP/AVP 0\r\n\
633             a=rtpmap:0 PCMU/8000\r\n",
634        );
635        assert_eq!(
636            answer(&offered, &Capabilities::g711(local(), 40000)).media[0].direction(),
637            Direction::RecvOnly,
638            "the session-level sendonly is what this stream offered"
639        );
640    }
641
642    /// A media-level direction overrides the session-level one for that stream alone
643    /// (RFC 8866 §6.7).
644    #[test]
645    fn a_media_level_direction_overrides_the_session_level_one() {
646        let offered = offer(
647            "v=0\r\no=a 1 1 IN IP4 192.0.2.10\r\ns=-\r\nc=IN IP4 192.0.2.10\r\nt=0 0\r\n\
648             a=sendonly\r\n\
649             m=audio 49170 RTP/AVP 0\r\n\
650             a=rtpmap:0 PCMU/8000\r\n\
651             a=sendrecv\r\n",
652        );
653        assert_eq!(
654            answer(&offered, &Capabilities::g711(local(), 40000)).media[0].direction(),
655            Direction::SendRecv
656        );
657    }
658
659    /// The answer may only narrow what was offered. An offer of `sendonly` cannot be answered
660    /// `sendrecv` however much this side would like to send.
661    #[test]
662    fn the_answer_cannot_widen_what_was_offered() {
663        assert_eq!(
664            negotiate_direction(Direction::SendOnly, Direction::SendRecv),
665            Direction::RecvOnly
666        );
667        assert_eq!(
668            negotiate_direction(Direction::SendRecv, Direction::RecvOnly),
669            Direction::RecvOnly
670        );
671        assert_eq!(
672            negotiate_direction(Direction::Inactive, Direction::SendRecv),
673            Direction::Inactive
674        );
675        assert_eq!(
676            negotiate_direction(Direction::RecvOnly, Direction::RecvOnly),
677            Direction::Inactive,
678            "the offerer will only receive and so will we: nothing flows"
679        );
680    }
681
682    /// A dynamic payload type means whatever its `rtpmap` says. Matching on the number alone
683    /// is how a stack agrees to a codec it cannot decode.
684    #[test]
685    fn dynamic_payload_types_are_matched_by_name_not_number() {
686        let mut capabilities = Capabilities::g711(local(), 40000);
687        capabilities.audio_formats.push("96".to_owned());
688        capabilities
689            .rtpmaps
690            .push(("96".to_owned(), "opus/48000/2".to_owned()));
691
692        // The far end uses 96 for something else entirely.
693        let offered = offer(
694            "v=0\r\no=a 1 1 IN IP4 192.0.2.10\r\ns=-\r\nc=IN IP4 192.0.2.10\r\nt=0 0\r\n\
695             m=audio 49170 RTP/AVP 96 0\r\n\
696             a=rtpmap:96 SPEEX/8000\r\n\
697             a=rtpmap:0 PCMU/8000\r\n",
698        );
699        let answered = answer(&offered, &capabilities);
700        assert_eq!(
701            answered.media[0].formats,
702            vec!["0"],
703            "96 is Speex there and Opus here; the numbers agreeing means nothing"
704        );
705
706        // And when the names do match, it is accepted.
707        let matching = offer(
708            "v=0\r\no=a 1 1 IN IP4 192.0.2.10\r\ns=-\r\nc=IN IP4 192.0.2.10\r\nt=0 0\r\n\
709             m=audio 49170 RTP/AVP 96 0\r\n\
710             a=rtpmap:96 opus/48000/2\r\n\
711             a=rtpmap:0 PCMU/8000\r\n",
712        );
713        assert_eq!(
714            answer(&matching, &capabilities).media[0].formats,
715            vec!["96", "0"]
716        );
717    }
718
719    /// RFC 8866 §6.6: an rtpmap is `<name>/<clock rate>[/<channels>]` and the rate is part
720    /// of the format's identity. Matching on the name alone agrees to a 16 kHz event stream
721    /// this side cannot decode.
722    #[test]
723    fn an_rtpmap_only_matches_when_the_clock_rate_agrees() {
724        let offered = offer(
725            "v=0\r\no=a 1 1 IN IP4 192.0.2.10\r\ns=-\r\nc=IN IP4 192.0.2.10\r\nt=0 0\r\n\
726             m=audio 49170 RTP/AVP 0 101\r\n\
727             a=rtpmap:0 PCMU/8000\r\n\
728             a=rtpmap:101 telephone-event/16000\r\n",
729        );
730        assert_eq!(
731            answer(&offered, &Capabilities::g711(local(), 40000)).media[0].formats,
732            vec!["0"],
733            "telephone-event at 16000 is not the 8000 we support"
734        );
735    }
736
737    /// RFC 8866 §6.6: the channel count defaults to one when omitted, so writing it out is
738    /// not a different format — but a different count is.
739    #[test]
740    fn a_missing_channel_count_means_one_channel() {
741        let mut capabilities = Capabilities::g711(local(), 40000);
742        capabilities.audio_formats.push("96".to_owned());
743        capabilities
744            .rtpmaps
745            .push(("96".to_owned(), "opus/48000".to_owned()));
746
747        let explicit_one = offer(
748            "v=0\r\no=a 1 1 IN IP4 192.0.2.10\r\ns=-\r\nc=IN IP4 192.0.2.10\r\nt=0 0\r\n\
749             m=audio 49170 RTP/AVP 96\r\n\
750             a=rtpmap:96 opus/48000/1\r\n",
751        );
752        assert_eq!(
753            answer(&explicit_one, &capabilities).media[0].formats,
754            vec!["96"],
755            "opus/48000 and opus/48000/1 are the same format"
756        );
757
758        let stereo = offer(
759            "v=0\r\no=a 1 1 IN IP4 192.0.2.10\r\ns=-\r\nc=IN IP4 192.0.2.10\r\nt=0 0\r\n\
760             m=audio 49170 RTP/AVP 96 0\r\n\
761             a=rtpmap:96 opus/48000/2\r\n\
762             a=rtpmap:0 PCMU/8000\r\n",
763        );
764        assert_eq!(
765            answer(&stereo, &capabilities).media[0].formats,
766            vec!["0"],
767            "two channels are not the one we support"
768        );
769    }
770
771    #[test]
772    fn a_dynamic_payload_type_without_an_rtpmap_is_not_accepted() {
773        let offered = offer(
774            "v=0\r\no=a 1 1 IN IP4 192.0.2.10\r\ns=-\r\nc=IN IP4 192.0.2.10\r\nt=0 0\r\n\
775             m=audio 49170 RTP/AVP 96 0\r\n\
776             a=rtpmap:0 PCMU/8000\r\n",
777        );
778        assert_eq!(
779            answer(&offered, &Capabilities::g711(local(), 40000)).media[0].formats,
780            vec!["0"]
781        );
782    }
783
784    /// RFC 8866 §6.6 lets an rtpmap remap even a static payload type, and the map is
785    /// authoritative when present. Taking the number alone accepts a codec this side does
786    /// not have while its RTP stack keeps treating the number as the static assignment.
787    #[test]
788    fn a_static_payload_type_remapped_by_the_offer_is_not_taken_on_the_number() {
789        let offered = offer(
790            "v=0\r\no=a 1 1 IN IP4 192.0.2.10\r\ns=-\r\nc=IN IP4 192.0.2.10\r\nt=0 0\r\n\
791             m=audio 49170 RTP/AVP 8 0\r\n\
792             a=rtpmap:8 iLBC/8000\r\n\
793             a=rtpmap:0 PCMU/8000\r\n",
794        );
795        assert_eq!(
796            answer(&offered, &Capabilities::g711(local(), 40000)).media[0].formats,
797            vec!["0"],
798            "8 means iLBC in this offer, and iLBC is not ours"
799        );
800    }
801
802    /// An offer that already rejected a stream is answered with a rejection; reviving it would
803    /// answer a question nobody asked.
804    #[test]
805    fn a_stream_the_offer_already_rejected_stays_rejected() {
806        let offered = offer(
807            "v=0\r\no=a 1 1 IN IP4 192.0.2.10\r\ns=-\r\nc=IN IP4 192.0.2.10\r\nt=0 0\r\n\
808             m=audio 0 RTP/AVP 0\r\n\
809             a=rtpmap:0 PCMU/8000\r\n",
810        );
811        assert!(answer(&offered, &Capabilities::g711(local(), 40000)).media[0].is_rejected());
812    }
813
814    #[test]
815    fn the_dtmf_fmtp_declares_the_events_this_side_receives() {
816        let answered = answer(&offer(AUDIO_OFFER), &Capabilities::g711(local(), 40000));
817        let fmtp = answered.media[0]
818            .attributes
819            .iter()
820            .find(|a| a.name == "fmtp")
821            .and_then(|a| a.value.clone())
822            .expect("an fmtp for DTMF");
823        assert_eq!(fmtp, "101 0-15");
824    }
825
826    /// RFC 4733 §2.5.1.2: the fmtp event list is per-direction — each party declares the
827    /// events *it* is willing to receive. Echoing the offer's list claims events this side
828    /// cannot handle.
829    #[test]
830    fn the_dtmf_fmtp_is_not_an_echo_of_the_offers() {
831        let offered = offer(
832            "v=0\r\no=a 1 1 IN IP4 192.0.2.10\r\ns=-\r\nc=IN IP4 192.0.2.10\r\nt=0 0\r\n\
833             m=audio 49170 RTP/AVP 0 101\r\n\
834             a=rtpmap:0 PCMU/8000\r\n\
835             a=rtpmap:101 telephone-event/8000\r\n\
836             a=fmtp:101 0-15,32-36\r\n",
837        );
838        let answered = answer(&offered, &Capabilities::g711(local(), 40000));
839        let fmtp = answered.media[0]
840            .attributes
841            .iter()
842            .find(|a| a.name == "fmtp")
843            .and_then(|a| a.value.clone())
844            .expect("an fmtp for DTMF");
845        assert_eq!(fmtp, "101 0-15", "32-36 are events we never handle");
846    }
847
848    #[test]
849    fn the_answer_advertises_our_address_and_port() {
850        let answered = answer(&offer(AUDIO_OFFER), &Capabilities::g711(local(), 40000));
851        assert_eq!(
852            answered.connection.expect("a connection").address.ip(),
853            Some(local())
854        );
855        assert_eq!(answered.origin.address.ip(), Some(local()));
856        assert_eq!(answered.media[0].port, 40000);
857    }
858
859    #[test]
860    fn an_answer_reparses_to_itself() {
861        let answered = answer(&offer(AUDIO_OFFER), &Capabilities::g711(local(), 40000));
862        let round_tripped = parse(&answered.to_string_sdp()).expect("the answer parses");
863        assert_eq!(answered, round_tripped);
864    }
865
866    // ---------------------------------------------------------------------------------------
867    // DTLS-SRTP (RFC 5763 / 5764 / 8122)
868    // ---------------------------------------------------------------------------------------
869
870    fn our_fingerprint() -> crate::fingerprint::Fingerprint {
871        crate::fingerprint::Fingerprint::of(
872            b"our certificate",
873            crate::fingerprint::HashFunc::Sha256,
874        )
875    }
876
877    fn their_fingerprint() -> crate::fingerprint::Fingerprint {
878        crate::fingerprint::Fingerprint::of(
879            b"their certificate",
880            crate::fingerprint::HashFunc::Sha256,
881        )
882    }
883
884    fn dtls_offer(extra_media: &str, session_level: &str) -> SessionDescription {
885        offer(&format!(
886            "v=0\r\n\
887             o=- 1 1 IN IP4 192.0.2.10\r\n\
888             s=-\r\n\
889             c=IN IP4 192.0.2.10\r\n\
890             t=0 0\r\n\
891             {session_level}\
892             m=audio 49170 UDP/TLS/RTP/SAVP 0 8\r\n\
893             a=rtpmap:0 PCMU/8000\r\n\
894             a=rtpmap:8 PCMA/8000\r\n\
895             {extra_media}"
896        ))
897    }
898
899    /// A DTLS offer is answered with this side's fingerprint and a role, on the same protocol.
900    #[test]
901    fn a_dtls_offer_is_answered_with_a_fingerprint_and_a_role() {
902        let offered = dtls_offer(
903            &format!(
904                "a=fingerprint:{}\r\na=setup:actpass\r\n",
905                their_fingerprint().to_value()
906            ),
907            "",
908        );
909        let answered = answer(
910            &offered,
911            &Capabilities::g711(local(), 40000).with_dtls_srtp(our_fingerprint()),
912        );
913        let audio = answered.media.first().expect("an audio stream");
914        assert_ne!(audio.port, 0, "the stream should not be rejected");
915        assert_eq!(
916            audio.protocol, "UDP/TLS/RTP/SAVP",
917            "the answer's protocol must match the offer's, or neither side knows how to key it"
918        );
919        assert_eq!(
920            audio.fingerprint(),
921            Some(our_fingerprint()),
922            "the answer carries *our* fingerprint, not an echo of theirs"
923        );
924        assert_eq!(
925            audio.setup(),
926            Some(crate::fingerprint::Setup::Active),
927            "RFC 5763 §5: the answerer takes `active`, so its `ClientHello` opens its own NAT"
928        );
929        assert!(
930            audio.crypto().is_none(),
931            "a DTLS stream must not also carry an SDES key"
932        );
933    }
934
935    /// A browser puts one `a=fingerprint` at session level and none on the `m=` line.
936    #[test]
937    fn a_session_level_fingerprint_is_found() {
938        let offered = dtls_offer(
939            "a=setup:actpass\r\n",
940            &format!("a=fingerprint:{}\r\n", their_fingerprint().to_value()),
941        );
942        let stream = offered.media.first().expect("an audio stream");
943        assert!(stream.fingerprint().is_none(), "none on the m= line");
944        assert_eq!(
945            fingerprint_of(&offered, stream),
946            Some(their_fingerprint()),
947            "the session-level value applies to a stream that does not override it"
948        );
949        let answered = answer(
950            &offered,
951            &Capabilities::g711(local(), 40000).with_dtls_srtp(our_fingerprint()),
952        );
953        assert_ne!(
954            answered.media.first().expect("a stream").port,
955            0,
956            "an offer whose fingerprint is at session level is still answerable"
957        );
958    }
959
960    /// A media-level fingerprint overrides the session-level one.
961    #[test]
962    fn a_media_level_fingerprint_wins_over_the_session_level_one() {
963        let other = crate::fingerprint::Fingerprint::of(
964            b"a third certificate",
965            crate::fingerprint::HashFunc::Sha256,
966        );
967        let offered = dtls_offer(
968            &format!(
969                "a=fingerprint:{}\r\na=setup:actpass\r\n",
970                their_fingerprint().to_value()
971            ),
972            &format!("a=fingerprint:{}\r\n", other.to_value()),
973        );
974        let stream = offered.media.first().expect("an audio stream");
975        assert_eq!(fingerprint_of(&offered, stream), Some(their_fingerprint()));
976    }
977
978    /// RFC 8122's whole guarantee is the fingerprint. An offer without one describes a handshake
979    /// whose certificate nothing can be checked against, so the stream is refused rather than
980    /// answered with encryption that authenticates nobody.
981    #[test]
982    fn a_dtls_offer_with_no_fingerprint_is_rejected() {
983        let offered = dtls_offer("a=setup:actpass\r\n", "");
984        let answered = answer(
985            &offered,
986            &Capabilities::g711(local(), 40000).with_dtls_srtp(our_fingerprint()),
987        );
988        assert_eq!(
989            answered.media.first().expect("a stream").port,
990            0,
991            "an unverifiable DTLS offer must be declined, not answered"
992        );
993    }
994
995    /// And an endpoint that cannot do DTLS declines rather than answering in the clear — the same
996    /// rule SDES already had, for the same reason.
997    #[test]
998    fn a_dtls_offer_to_an_endpoint_without_dtls_is_rejected() {
999        let offered = dtls_offer(
1000            &format!(
1001                "a=fingerprint:{}\r\na=setup:actpass\r\n",
1002                their_fingerprint().to_value()
1003            ),
1004            "",
1005        );
1006        let answered = answer(&offered, &Capabilities::g711(local(), 40000));
1007        assert_eq!(
1008            answered.media.first().expect("a stream").port,
1009            0,
1010            "answering a DTLS offer in the clear would be a downgrade this side chose"
1011        );
1012    }
1013
1014    /// An offerer that names `passive` is answered `active`, and one that names `active` gets
1015    /// `passive`. Copying the role is how both ends wait for each other.
1016    #[test]
1017    fn the_role_is_answered_rather_than_copied() {
1018        for (offered_role, expected) in [
1019            ("actpass", crate::fingerprint::Setup::Active),
1020            ("passive", crate::fingerprint::Setup::Active),
1021            ("active", crate::fingerprint::Setup::Passive),
1022        ] {
1023            let offered = dtls_offer(
1024                &format!(
1025                    "a=fingerprint:{}\r\na=setup:{offered_role}\r\n",
1026                    their_fingerprint().to_value()
1027                ),
1028                "",
1029            );
1030            let answered = answer(
1031                &offered,
1032                &Capabilities::g711(local(), 40000).with_dtls_srtp(our_fingerprint()),
1033            );
1034            assert_eq!(
1035                answered.media.first().expect("a stream").setup(),
1036                Some(expected),
1037                "offered {offered_role}"
1038            );
1039        }
1040    }
1041
1042    /// Offering DTLS replaces any SDES key rather than adding to it. They are different `m=`
1043    /// protocols, and a leftover `a=crypto` would put a master key in an SDP whose entire purpose
1044    /// is not to carry one.
1045    #[test]
1046    fn offering_dtls_srtp_clears_any_sdes_key() {
1047        let capabilities = Capabilities::g711(local(), 40000)
1048            .with_srtp(true)
1049            .with_dtls_srtp(our_fingerprint());
1050        assert!(capabilities.crypto.is_none());
1051        assert_eq!(capabilities.protocol(), "UDP/TLS/RTP/SAVP");
1052    }
1053
1054    /// A plain offer is answered plainly even by an endpoint that would rather use DTLS — the
1055    /// answerer does not get to upgrade the keying unilaterally.
1056    #[test]
1057    fn a_plain_offer_is_not_upgraded_to_dtls() {
1058        let answered = answer(
1059            &offer(AUDIO_OFFER),
1060            &Capabilities::g711(local(), 40000).with_dtls_srtp(our_fingerprint()),
1061        );
1062        let audio = answered.media.first().expect("a stream");
1063        assert_ne!(audio.port, 0, "a plain offer is still answerable");
1064        assert_eq!(audio.protocol, "RTP/AVP");
1065        assert!(audio.fingerprint().is_none());
1066    }
1067}