1use std::fmt;
9use std::net::SocketAddr;
10use std::sync::Arc;
11use std::sync::atomic::{AtomicBool, Ordering};
12use std::time::Duration;
13
14use bytes::Bytes;
15use sipx_media::{Codec, MediaSession};
16use sipx_sdp::{Direction, RtcpMode};
17use sipx_sip::Uri;
18use sipx_sip::headers::Address;
19use sipx_transport::{Handle, Target};
20use tokio::time::Instant;
21
22use crate::dialog::{DialogId, Role};
23use crate::{Codecs, Keying, MediaAddress, MediaPolicy, MediaProfile, NegotiatedKeying};
24
25const MAGIC: &[u8; 4] = b"SXD1";
26const LEGACY_VERSION: u16 = 1;
27const VERSION: u16 = 2;
28const FLAG_CALLEE: u16 = 1 << 0;
29const FLAG_PROTECTED: u16 = 1 << 1;
30const FLAG_SESSION: u16 = 1 << 2;
31const FLAG_PEER_UPDATE: u16 = 1 << 3;
32const KNOWN_FLAGS: u16 = FLAG_CALLEE | FLAG_PROTECTED | FLAG_SESSION | FLAG_PEER_UPDATE;
33
34pub const MAX_SNAPSHOT_BYTES: usize = 262_144;
36pub const MAX_VARIABLE_BYTES: usize = 131_072;
38pub const MAX_ID_BYTES: usize = 1_024;
40pub const MAX_FIELD_BYTES: usize = 8_192;
42pub const MAX_ROUTES: usize = 64;
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
47#[non_exhaustive]
48pub enum DialogNotQuiescent {
49 #[error("the call has ended")]
51 Ended,
52 #[error("the dialog-forming response is still awaiting ACK")]
54 AwaitingAck,
55 #[error("an offer, answer, or UPDATE remains outstanding")]
57 OfferAnswer,
58 #[error("a replaced media session is still awaiting cleanup")]
60 MediaCleanup,
61 #[error("a transfer usage remains attached")]
63 Transfer,
64 #[error("a live ICE generation cannot be snapshotted")]
66 Ice,
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71#[non_exhaustive]
72pub enum DialogSessionAction {
73 Refresh,
75 Expire,
77}
78
79#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
81#[non_exhaustive]
82pub enum DialogPersistenceError {
83 #[error("dialog snapshot has an invalid magic value")]
85 InvalidMagic,
86 #[error("dialog snapshot schema version {0} is unsupported")]
88 UnsupportedVersion(u16),
89 #[error("dialog snapshot has reserved flags set: {0:#06x}")]
91 ReservedFlags(u16),
92 #[error("dialog snapshot is truncated in {field}")]
94 Truncated {
95 field: &'static str,
97 },
98 #[error("dialog snapshot field {field} is {len} bytes; the limit is {max}")]
100 FieldTooLarge {
101 field: &'static str,
103 len: usize,
105 max: usize,
107 },
108 #[error("dialog snapshot is {len} bytes; the limit is {max}")]
110 InputTooLarge {
111 len: usize,
113 max: usize,
115 },
116 #[error("dialog snapshot variable fields exceed {MAX_VARIABLE_BYTES} bytes")]
118 VariableDataTooLarge,
119 #[error("dialog snapshot field {field} is not UTF-8")]
121 InvalidUtf8 {
122 field: &'static str,
124 },
125 #[error("dialog snapshot has an invalid {field}")]
127 InvalidValue {
128 field: &'static str,
130 },
131 #[error("dialog snapshot has a non-canonical presence marker for {field}")]
133 NonCanonicalPresence {
134 field: &'static str,
136 },
137 #[error("dialog snapshot repeats the same local and remote tag")]
139 DuplicateTags,
140 #[error("dialog snapshot has {count} routes; the limit is {MAX_ROUTES}")]
142 TooManyRoutes {
143 count: usize,
145 },
146 #[error("dialog snapshot has trailing bytes")]
148 TrailingBytes,
149 #[error("dialog snapshot local CSeq is exhausted")]
151 CseqExhausted,
152 #[error("dialog is not quiescent: {0}")]
154 NotQuiescent(DialogNotQuiescent),
155 #[error("dialog session action is already due: {0:?}")]
157 SessionActionDue(DialogSessionAction),
158 #[error("dialog snapshot session timer values are contradictory")]
160 SessionContradiction,
161 #[error("dialog session deadline overflows the injected clock")]
163 ClockOverflow,
164 #[error("protected dialog state cannot be restored through clear signalling")]
166 SecurityDowngrade,
167 #[error("fresh media security does not match the dialog snapshot")]
169 MediaSecurityMismatch,
170 #[error("fresh media does not match the snapshot field {field}")]
172 MediaContractMismatch {
173 field: &'static str,
175 },
176 #[error("dialog snapshot {field} {value} exceeds the RTP payload type range 0..=127")]
178 PayloadTypeOutOfRange {
179 field: &'static str,
181 value: u8,
183 },
184 #[error("dialog restore context is already attached to a call")]
186 ContextAlreadyAttached,
187 #[error("dialog snapshot codec id {0} is unavailable in this build")]
189 UnsupportedCodec(u8),
190 #[error("the fresh advertised media address must not be unspecified")]
192 UnspecifiedMediaAddress,
193}
194
195#[derive(Clone, Copy, PartialEq, Eq)]
196pub(crate) struct SessionSnapshot {
197 pub(crate) interval: Duration,
198 pub(crate) we_refresh: bool,
199 pub(crate) remaining: Duration,
200}
201
202#[derive(Clone)]
204pub struct DialogSnapshot {
205 role: Role,
206 id: DialogId,
207 local_party: String,
208 remote_party: String,
209 remote_target: Uri,
210 route_set: Vec<String>,
211 local_cseq: u32,
212 remote_cseq: Option<u32>,
213 protected_signalling: bool,
214 media_keying: NegotiatedKeying,
215 media_profile: MediaProfile,
216 codecs: Codecs,
217 codec: Codec,
218 clock_rate: u32,
220 payload_type: u8,
222 receive_payload_type: u8,
224 dtmf_payload_type: Option<u8>,
225 rtcp_mode: RtcpMode,
226 hold: Direction,
227 peer_allows_update: bool,
228 session: Option<SessionSnapshot>,
229}
230
231impl fmt::Debug for DialogSnapshot {
232 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
233 formatter
234 .debug_struct("DialogSnapshot")
235 .field("version", &VERSION)
236 .field("role", &self.role)
237 .field("call_id_bytes", &self.id.call_id.len())
238 .field("local_tag_bytes", &self.id.local_tag.len())
239 .field("remote_tag_bytes", &self.id.remote_tag.len())
240 .field("local_party_bytes", &self.local_party.len())
241 .field("remote_party_bytes", &self.remote_party.len())
242 .field("remote_target_bytes", &self.remote_target.to_bytes().len())
243 .field("routes", &self.route_set.len())
244 .field("local_cseq", &self.local_cseq)
245 .field("remote_cseq", &self.remote_cseq)
246 .field("protected_signalling", &self.protected_signalling)
247 .field("media_keying", &self.media_keying)
248 .field("media_profile", &self.media_profile)
249 .field("codecs", &self.codecs)
250 .field("codec", &self.codec)
251 .field("clock_rate", &self.clock_rate)
252 .field("payload_type", &self.payload_type)
253 .field("receive_payload_type", &self.receive_payload_type)
254 .field("dtmf_payload_type", &self.dtmf_payload_type)
255 .field("rtcp_mode", &self.rtcp_mode)
256 .field("hold", &self.hold)
257 .field("peer_allows_update", &self.peer_allows_update)
258 .field("has_session_timer", &self.session.is_some())
259 .finish()
260 }
261}
262
263impl DialogSnapshot {
264 #[must_use]
266 pub const fn version(&self) -> u16 {
267 VERSION
268 }
269
270 #[must_use]
272 pub const fn role(&self) -> Role {
273 self.role
274 }
275
276 #[must_use]
278 pub const fn dialog_id(&self) -> &DialogId {
279 &self.id
280 }
281
282 #[must_use]
284 pub fn local_party(&self) -> &str {
285 &self.local_party
286 }
287
288 #[must_use]
290 pub fn remote_party(&self) -> &str {
291 &self.remote_party
292 }
293
294 #[must_use]
296 pub const fn local_cseq(&self) -> u32 {
297 self.local_cseq
298 }
299
300 #[must_use]
302 pub const fn remote_cseq(&self) -> Option<u32> {
303 self.remote_cseq
304 }
305
306 #[must_use]
308 pub const fn remote_target(&self) -> &Uri {
309 &self.remote_target
310 }
311
312 #[must_use]
314 pub fn route_set(&self) -> &[String] {
315 &self.route_set
316 }
317
318 #[must_use]
320 pub const fn protected_signalling(&self) -> bool {
321 self.protected_signalling
322 }
323
324 #[must_use]
326 pub const fn media_keying(&self) -> NegotiatedKeying {
327 self.media_keying
328 }
329
330 #[must_use]
332 pub const fn media_profile(&self) -> MediaProfile {
333 self.media_profile
334 }
335
336 #[must_use]
338 pub const fn codecs(&self) -> Codecs {
339 self.codecs
340 }
341
342 #[must_use]
344 pub const fn codec(&self) -> Codec {
345 self.codec
346 }
347
348 #[must_use]
350 pub const fn clock_rate(&self) -> u32 {
351 self.clock_rate
352 }
353
354 #[must_use]
356 pub const fn payload_type(&self) -> u8 {
357 self.payload_type
358 }
359
360 #[must_use]
362 pub const fn receive_payload_type(&self) -> u8 {
363 self.receive_payload_type
364 }
365
366 #[must_use]
368 pub const fn dtmf_payload_type(&self) -> Option<u8> {
369 self.dtmf_payload_type
370 }
371
372 #[must_use]
374 pub const fn rtcp_mode(&self) -> RtcpMode {
375 self.rtcp_mode
376 }
377
378 #[must_use]
380 pub const fn direction(&self) -> Direction {
381 self.hold
382 }
383
384 #[must_use]
386 pub const fn peer_allows_update(&self) -> bool {
387 self.peer_allows_update
388 }
389
390 #[must_use]
392 pub const fn session_timer(&self) -> Option<(Duration, bool, Duration)> {
393 match self.session {
394 Some(session) => Some((session.interval, session.we_refresh, session.remaining)),
395 None => None,
396 }
397 }
398
399 #[must_use]
404 pub fn encode(&self) -> Vec<u8> {
405 let mut encoded = Vec::with_capacity(self.encoded_len().min(MAX_SNAPSHOT_BYTES));
406 encoded.extend_from_slice(MAGIC);
407 put_u16(&mut encoded, VERSION);
408 let mut flags = 0u16;
409 if self.role == Role::Callee {
410 flags |= FLAG_CALLEE;
411 }
412 if self.protected_signalling {
413 flags |= FLAG_PROTECTED;
414 }
415 if self.session.is_some() {
416 flags |= FLAG_SESSION;
417 }
418 if self.peer_allows_update {
419 flags |= FLAG_PEER_UPDATE;
420 }
421 put_u16(&mut encoded, flags);
422 put_bytes(&mut encoded, &self.id.call_id);
423 put_bytes(&mut encoded, &self.id.local_tag);
424 put_bytes(&mut encoded, &self.id.remote_tag);
425 put_bytes(&mut encoded, self.local_party.as_bytes());
426 put_bytes(&mut encoded, self.remote_party.as_bytes());
427 put_bytes(&mut encoded, &self.remote_target.to_bytes());
428 put_u16(
429 &mut encoded,
430 u16::try_from(self.route_set.len()).unwrap_or(u16::MAX),
431 );
432 for route in &self.route_set {
433 put_bytes(&mut encoded, route.as_bytes());
434 }
435 put_u32(&mut encoded, self.local_cseq);
436 put_optional_u32(&mut encoded, self.remote_cseq);
437 encoded.push(keying_id(self.media_keying));
438 encoded.push(profile_id(self.media_profile));
439 let preferences: Vec<_> = self.codecs.preferences().collect();
440 encoded.push(u8::try_from(preferences.len()).unwrap_or(u8::MAX));
441 for preference in preferences {
442 encoded.push(preference_id(preference));
443 }
444 encoded.push(codec_id(self.codec));
445 put_u32(&mut encoded, self.clock_rate);
446 encoded.push(self.payload_type);
447 encoded.push(self.receive_payload_type);
448 put_optional_u8(&mut encoded, self.dtmf_payload_type);
449 encoded.push(rtcp_id(self.rtcp_mode));
450 encoded.push(direction_id(self.hold));
451 encoded.push(0);
454 if let Some(session) = self.session {
455 put_u64(&mut encoded, session.interval.as_secs());
456 encoded.push(u8::from(session.we_refresh));
457 let nanos = u64::try_from(session.remaining.as_nanos()).unwrap_or(u64::MAX);
458 put_u64(&mut encoded, nanos);
459 }
460 encoded
461 }
462
463 #[allow(
469 clippy::too_many_lines,
470 reason = "one ordered read keeps the canonical field order and pre-allocation checks auditable"
471 )]
472 pub fn decode(input: &[u8]) -> Result<Self, DialogPersistenceError> {
473 if input.len() > MAX_SNAPSHOT_BYTES {
474 return Err(DialogPersistenceError::InputTooLarge {
475 len: input.len(),
476 max: MAX_SNAPSHOT_BYTES,
477 });
478 }
479 let mut reader = Reader::new(input);
480 if reader.exact(4, "magic")? != MAGIC {
481 return Err(DialogPersistenceError::InvalidMagic);
482 }
483 let version = reader.u16("version")?;
484 if !matches!(version, LEGACY_VERSION | VERSION) {
485 return Err(DialogPersistenceError::UnsupportedVersion(version));
486 }
487 let flags = reader.u16("flags")?;
488 if flags & !KNOWN_FLAGS != 0 {
489 return Err(DialogPersistenceError::ReservedFlags(flags & !KNOWN_FLAGS));
490 }
491 let role = if flags & FLAG_CALLEE == 0 {
492 Role::Caller
493 } else {
494 Role::Callee
495 };
496 let call_id = reader.bytes("Call-ID", MAX_ID_BYTES)?;
497 let local_tag = reader.bytes("local tag", MAX_ID_BYTES)?;
498 let remote_tag = reader.bytes("remote tag", MAX_ID_BYTES)?;
499 let local_party = reader.string("local party", MAX_FIELD_BYTES)?;
500 let remote_party = reader.string("remote party", MAX_FIELD_BYTES)?;
501 let target_bytes = reader.bytes("remote target", MAX_FIELD_BYTES)?;
502 let remote_target = Uri::parse(Bytes::from(target_bytes)).map_err(|_| {
503 DialogPersistenceError::InvalidValue {
504 field: "remote target",
505 }
506 })?;
507 let route_count = usize::from(reader.u16("route count")?);
508 if route_count > MAX_ROUTES {
509 return Err(DialogPersistenceError::TooManyRoutes { count: route_count });
510 }
511 let mut route_set = Vec::with_capacity(route_count);
512 for _ in 0..route_count {
513 route_set.push(reader.string("route", MAX_FIELD_BYTES)?);
514 }
515 let local_cseq = reader.u32("local CSeq")?;
516 let remote_cseq = reader.optional_u32("remote CSeq")?;
517 let media_keying = decode_keying(reader.u8("media keying")?)?;
518 let media_profile = decode_profile(reader.u8("media profile")?)?;
519 let preference_count = usize::from(reader.u8("codec preference count")?);
520 if preference_count == 0 || preference_count > 4 {
521 return Err(DialogPersistenceError::InvalidValue {
522 field: "codec preference count",
523 });
524 }
525 let mut preferences = Vec::with_capacity(preference_count);
526 for _ in 0..preference_count {
527 preferences.push(decode_preference(reader.u8("codec preference")?)?);
528 }
529 let codecs =
530 Codecs::ordered(&preferences).map_err(|_| DialogPersistenceError::InvalidValue {
531 field: "codec preferences",
532 })?;
533 let codec = decode_codec(reader.u8("codec")?)?;
534 let clock_rate = if version == LEGACY_VERSION {
535 codec.clock_rate()
536 } else {
537 reader.u32("media clock rate")?
538 };
539 let payload_type = reader.u8("payload type")?;
540 let receive_payload_type = if version == LEGACY_VERSION {
541 payload_type
542 } else {
543 reader.u8("receive payload type")?
544 };
545 let dtmf_payload_type = reader.optional_u8("DTMF payload type")?;
546 let rtcp_mode = decode_rtcp(reader.u8("RTCP mode")?)?;
547 let hold = decode_direction(reader.u8("hold direction")?)?;
548 if reader.u8("offer state")? != 0 {
549 return Err(DialogPersistenceError::InvalidValue {
550 field: "offer state",
551 });
552 }
553 let session = if flags & FLAG_SESSION == 0 {
554 None
555 } else {
556 let interval = Duration::from_secs(reader.u64("session interval")?);
557 let we_refresh = match reader.u8("session refresher")? {
558 0 => false,
559 1 => true,
560 _ => {
561 return Err(DialogPersistenceError::NonCanonicalPresence {
562 field: "session refresher",
563 });
564 }
565 };
566 let remaining = Duration::from_nanos(reader.u64("session remaining")?);
567 Some(SessionSnapshot {
568 interval,
569 we_refresh,
570 remaining,
571 })
572 };
573 if !reader.is_empty() {
574 return Err(DialogPersistenceError::TrailingBytes);
575 }
576 let snapshot = Self {
577 role,
578 id: DialogId {
579 call_id,
580 local_tag,
581 remote_tag,
582 },
583 local_party,
584 remote_party,
585 remote_target,
586 route_set,
587 local_cseq,
588 remote_cseq,
589 protected_signalling: flags & FLAG_PROTECTED != 0,
590 media_keying,
591 media_profile,
592 codecs,
593 codec,
594 clock_rate,
595 payload_type,
596 receive_payload_type,
597 dtmf_payload_type,
598 rtcp_mode,
599 hold,
600 peer_allows_update: flags & FLAG_PEER_UPDATE != 0,
601 session,
602 };
603 snapshot.validate()?;
604 Ok(snapshot)
605 }
606
607 pub(crate) fn from_parts(parts: SnapshotParts) -> Result<Self, DialogPersistenceError> {
608 let snapshot = Self {
609 role: parts.role,
610 id: parts.id,
611 local_party: parts.local_party,
612 remote_party: parts.remote_party,
613 remote_target: parts.remote_target,
614 route_set: parts.route_set,
615 local_cseq: parts.local_cseq,
616 remote_cseq: parts.remote_cseq,
617 protected_signalling: parts.protected_signalling,
618 media_keying: parts.media_keying,
619 media_profile: parts.media_profile,
620 codecs: parts.codecs,
621 codec: parts.codec,
622 clock_rate: parts.clock_rate,
623 payload_type: parts.payload_type,
624 receive_payload_type: parts.receive_payload_type,
625 dtmf_payload_type: parts.dtmf_payload_type,
626 rtcp_mode: parts.rtcp_mode,
627 hold: parts.hold,
628 peer_allows_update: parts.peer_allows_update,
629 session: parts.session,
630 };
631 snapshot.validate()?;
632 Ok(snapshot)
633 }
634
635 pub(crate) fn validate_restore(
636 &self,
637 context: &DialogRestoreContext,
638 ) -> Result<Option<(Duration, bool, Instant)>, DialogPersistenceError> {
639 self.validate()?;
640 if self.protected_signalling && !context.target.transport.is_secure() {
641 return Err(DialogPersistenceError::SecurityDowngrade);
642 }
643 if requires_secure_route(&self.remote_target, &self.route_set)?
644 && !context.target.transport.is_secure()
645 {
646 return Err(DialogPersistenceError::SecurityDowngrade);
647 }
648 validate_media(self, context)?;
649 match self.session {
650 None => Ok(None),
651 Some(session) if session.remaining.is_zero() => {
652 Err(DialogPersistenceError::SessionActionDue(session.action()))
653 }
654 Some(session) => {
655 let remaining = session
656 .remaining
657 .checked_sub(context.elapsed_since_capture)
658 .filter(|remaining| !remaining.is_zero())
659 .ok_or_else(|| DialogPersistenceError::SessionActionDue(session.action()))?;
660 let deadline = context
661 .now
662 .checked_add(remaining)
663 .ok_or(DialogPersistenceError::ClockOverflow)?;
664 Ok(Some((session.interval, session.we_refresh, deadline)))
665 }
666 }
667 }
668
669 pub(crate) fn dialog(&self) -> crate::Dialog {
670 crate::Dialog {
671 role: self.role,
672 id: self.id.clone(),
673 local_uri: self.local_party.clone(),
674 remote_uri: self.remote_party.clone(),
675 remote_target: self.remote_target.clone(),
676 local_cseq: self.local_cseq,
677 remote_cseq: self.remote_cseq,
678 route_set: self.route_set.clone(),
679 }
680 }
681
682 pub(crate) const fn media_profile_value(&self) -> MediaProfile {
683 self.media_profile
684 }
685
686 pub(crate) const fn codecs_value(&self) -> Codecs {
687 self.codecs
688 }
689
690 pub(crate) const fn negotiated(&self, remote: SocketAddr) -> crate::call::Negotiated {
691 crate::call::Negotiated {
692 remote,
693 codec: self.codec,
694 clock_rate: self.clock_rate,
695 payload_type: Some(self.payload_type),
696 receive_payload_type: Some(self.receive_payload_type),
697 dtmf: self.dtmf_payload_type,
698 rtcp_mode: self.rtcp_mode,
699 }
700 }
701
702 pub(crate) const fn peer_allows_update_value(&self) -> bool {
703 self.peer_allows_update
704 }
705
706 fn encoded_len(&self) -> usize {
707 64usize
708 .saturating_add(self.variable_len())
709 .saturating_add(self.route_set.len().saturating_mul(4))
710 }
711
712 fn variable_len(&self) -> usize {
713 self.id
714 .call_id
715 .len()
716 .saturating_add(self.id.local_tag.len())
717 .saturating_add(self.id.remote_tag.len())
718 .saturating_add(self.local_party.len())
719 .saturating_add(self.remote_party.len())
720 .saturating_add(self.remote_target.to_bytes().len())
721 .saturating_add(
722 self.route_set
723 .iter()
724 .fold(0usize, |sum, route| sum.saturating_add(route.len())),
725 )
726 }
727
728 fn validate(&self) -> Result<(), DialogPersistenceError> {
729 validate_bytes("Call-ID", &self.id.call_id, MAX_ID_BYTES, false)?;
730 if self.id.call_id.iter().any(|byte| *byte <= 0x20) {
731 return Err(DialogPersistenceError::InvalidValue { field: "Call-ID" });
732 }
733 validate_token("local tag", &self.id.local_tag)?;
734 validate_token("remote tag", &self.id.remote_tag)?;
735 if self.id.local_tag == self.id.remote_tag {
736 return Err(DialogPersistenceError::DuplicateTags);
737 }
738 validate_party("local party", &self.local_party)?;
739 validate_party("remote party", &self.remote_party)?;
740 validate_uri("remote target", &self.remote_target)?;
741 if self.route_set.len() > MAX_ROUTES {
742 return Err(DialogPersistenceError::TooManyRoutes {
743 count: self.route_set.len(),
744 });
745 }
746 for route in &self.route_set {
747 validate_route(route)?;
748 }
749 if self.variable_len() > MAX_VARIABLE_BYTES {
750 return Err(DialogPersistenceError::VariableDataTooLarge);
751 }
752 if self.local_cseq == u32::MAX {
753 return Err(DialogPersistenceError::CseqExhausted);
754 }
755 if requires_secure_route(&self.remote_target, &self.route_set)?
756 && !self.protected_signalling
757 {
758 return Err(DialogPersistenceError::InvalidValue {
759 field: "SIPS security state",
760 });
761 }
762 if self.media_keying == NegotiatedKeying::Sdes && !self.protected_signalling {
763 return Err(DialogPersistenceError::InvalidValue {
764 field: "SDES signalling security",
765 });
766 }
767 if !self.codecs.carries(self.codec) {
768 return Err(DialogPersistenceError::InvalidValue {
769 field: "negotiated codec selection",
770 });
771 }
772 if self.clock_rate == 0 || self.clock_rate > sipx_audio::pcm::MAX_SAMPLE_RATE {
773 return Err(DialogPersistenceError::InvalidValue {
774 field: "media clock rate",
775 });
776 }
777 validate_payload_type("payload type", self.payload_type)?;
778 validate_payload_type("receive payload type", self.receive_payload_type)?;
779 if let Some(payload_type) = self.dtmf_payload_type {
780 validate_payload_type("DTMF payload type", payload_type)?;
781 }
782 if [self.payload_type, self.receive_payload_type]
783 .into_iter()
784 .any(|payload_type| self.dtmf_payload_type == Some(payload_type))
785 {
786 return Err(DialogPersistenceError::InvalidValue {
787 field: "DTMF payload type",
788 });
789 }
790 if self.rtcp_mode == RtcpMode::Mux
791 && [
792 Some(self.payload_type),
793 Some(self.receive_payload_type),
794 self.dtmf_payload_type,
795 ]
796 .into_iter()
797 .flatten()
798 .any(|payload| (64..=95).contains(&payload))
799 {
800 return Err(DialogPersistenceError::InvalidValue {
801 field: "RTCP-mux payload type",
802 });
803 }
804 if self.media_profile != MediaProfile::Standard {
805 return Err(DialogPersistenceError::InvalidValue {
806 field: "restorable media profile",
807 });
808 }
809 if let Some(session) = self.session
810 && (session.interval < sipx_sip::session::ABSOLUTE_MIN_INTERVAL
811 || session.remaining > session.interval)
812 {
813 return Err(DialogPersistenceError::SessionContradiction);
814 }
815 if self.encoded_len() > MAX_SNAPSHOT_BYTES {
816 return Err(DialogPersistenceError::InputTooLarge {
817 len: self.encoded_len(),
818 max: MAX_SNAPSHOT_BYTES,
819 });
820 }
821 Ok(())
822 }
823}
824
825impl SessionSnapshot {
826 pub(crate) const fn action(self) -> DialogSessionAction {
827 if self.we_refresh {
828 DialogSessionAction::Refresh
829 } else {
830 DialogSessionAction::Expire
831 }
832 }
833}
834
835pub(crate) struct SnapshotParts {
836 pub(crate) role: Role,
837 pub(crate) id: DialogId,
838 pub(crate) local_party: String,
839 pub(crate) remote_party: String,
840 pub(crate) remote_target: Uri,
841 pub(crate) route_set: Vec<String>,
842 pub(crate) local_cseq: u32,
843 pub(crate) remote_cseq: Option<u32>,
844 pub(crate) protected_signalling: bool,
845 pub(crate) media_keying: NegotiatedKeying,
846 pub(crate) media_profile: MediaProfile,
847 pub(crate) codecs: Codecs,
848 pub(crate) codec: Codec,
849 pub(crate) clock_rate: u32,
850 pub(crate) payload_type: u8,
851 pub(crate) receive_payload_type: u8,
852 pub(crate) dtmf_payload_type: Option<u8>,
853 pub(crate) rtcp_mode: RtcpMode,
854 pub(crate) hold: Direction,
855 pub(crate) peer_allows_update: bool,
856 pub(crate) session: Option<SessionSnapshot>,
857}
858
859pub struct DialogRestoreContext {
866 pub(crate) endpoint: Handle,
867 pub(crate) target: Target,
868 pub(crate) media: Arc<MediaSession>,
869 pub(crate) media_address: MediaAddress,
870 pub(crate) remote_media: SocketAddr,
871 pub(crate) policy: MediaPolicy,
872 pub(crate) direction: Direction,
873 pub(crate) elapsed_since_capture: Duration,
874 pub(crate) now: Instant,
875 claimed: AtomicBool,
876}
877
878impl DialogRestoreContext {
879 #[must_use]
881 #[allow(clippy::too_many_arguments)]
882 pub fn new(
883 endpoint: Handle,
884 target: Target,
885 media: Arc<MediaSession>,
886 media_address: MediaAddress,
887 remote_media: SocketAddr,
888 policy: MediaPolicy,
889 direction: Direction,
890 elapsed_since_capture: Duration,
891 now: Instant,
892 ) -> Self {
893 Self {
894 endpoint,
895 target,
896 media,
897 media_address,
898 remote_media,
899 policy,
900 direction,
901 elapsed_since_capture,
902 now,
903 claimed: AtomicBool::new(false),
904 }
905 }
906
907 pub(crate) fn claim(&self) -> Result<(), DialogPersistenceError> {
908 self.claimed
909 .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
910 .map(|_| ())
911 .map_err(|_| DialogPersistenceError::ContextAlreadyAttached)
912 }
913}
914
915impl fmt::Debug for DialogRestoreContext {
916 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
917 formatter
918 .debug_struct("DialogRestoreContext")
919 .field("endpoint_local", &self.endpoint.local_addr())
920 .field("target_addr", &self.target.addr)
921 .field("target_transport", &self.target.transport)
922 .field("target_has_path", &self.target.path.is_some())
923 .field("media_local", &self.media.local_addr())
924 .field("media_advertised", &self.media_address.advertised())
925 .field("media_bind", &self.media_address.bind())
926 .field("remote_media", &self.remote_media)
927 .field("policy", &self.policy)
928 .field("direction", &self.direction)
929 .field("elapsed_since_capture", &self.elapsed_since_capture)
930 .field("now", &self.now)
931 .finish_non_exhaustive()
932 }
933}
934
935fn validate_media(
936 snapshot: &DialogSnapshot,
937 context: &DialogRestoreContext,
938) -> Result<(), DialogPersistenceError> {
939 if context.media_address.advertised().is_unspecified() {
940 return Err(DialogPersistenceError::UnspecifiedMediaAddress);
941 }
942 if context.remote_media.ip().is_unspecified() || context.remote_media.port() == 0 {
943 return Err(DialogPersistenceError::MediaContractMismatch {
944 field: "remote media address",
945 });
946 }
947 if context.media.local_addr().ip() != context.media_address.bind() {
948 return Err(DialogPersistenceError::MediaContractMismatch {
949 field: "media bind address",
950 });
951 }
952 if context.media.runs_ice() || context.policy.ice != crate::IcePolicy::Disabled {
953 return Err(DialogPersistenceError::MediaContractMismatch { field: "ICE state" });
954 }
955 if context.policy.profile != snapshot.media_profile {
956 return Err(DialogPersistenceError::MediaContractMismatch {
957 field: "media profile",
958 });
959 }
960 if context.policy.codecs != snapshot.codecs {
961 return Err(DialogPersistenceError::MediaContractMismatch {
962 field: "codec policy",
963 });
964 }
965 if context.direction != snapshot.hold {
966 return Err(DialogPersistenceError::MediaContractMismatch { field: "direction" });
967 }
968 let expected_keying = match context.policy.keying {
969 Keying::Plain => NegotiatedKeying::Plain,
970 Keying::Sdes => NegotiatedKeying::Sdes,
971 Keying::DtlsSrtp => NegotiatedKeying::DtlsSrtp,
972 Keying::Auto => return Err(DialogPersistenceError::MediaSecurityMismatch),
973 };
974 if expected_keying != snapshot.media_keying
975 || context.media.is_encrypted() != (snapshot.media_keying != NegotiatedKeying::Plain)
976 {
977 return Err(DialogPersistenceError::MediaSecurityMismatch);
978 }
979 if snapshot.media_keying == NegotiatedKeying::Sdes && !context.target.transport.is_secure() {
980 return Err(DialogPersistenceError::SecurityDowngrade);
981 }
982 if context.media.codec() != snapshot.codec {
983 return Err(DialogPersistenceError::MediaContractMismatch { field: "codec" });
984 }
985 if context.media.clock_rate() != snapshot.clock_rate {
986 return Err(DialogPersistenceError::MediaContractMismatch {
987 field: "media clock rate",
988 });
989 }
990 if context.media.wire_payload_type() != snapshot.payload_type {
991 return Err(DialogPersistenceError::MediaContractMismatch {
992 field: "payload type",
993 });
994 }
995 if context.media.receive_payload_type() != snapshot.receive_payload_type {
996 return Err(DialogPersistenceError::MediaContractMismatch {
997 field: "receive payload type",
998 });
999 }
1000 if context.media.dtmf_payload_type() != snapshot.dtmf_payload_type {
1001 return Err(DialogPersistenceError::MediaContractMismatch {
1002 field: "DTMF payload type",
1003 });
1004 }
1005 if context.media.rtcp_mode() != snapshot.rtcp_mode {
1006 return Err(DialogPersistenceError::MediaContractMismatch { field: "RTCP mode" });
1007 }
1008 Ok(())
1009}
1010
1011fn validate_payload_type(field: &'static str, value: u8) -> Result<(), DialogPersistenceError> {
1012 if value > 0x7f {
1013 return Err(DialogPersistenceError::PayloadTypeOutOfRange { field, value });
1014 }
1015 Ok(())
1016}
1017
1018fn validate_bytes(
1019 field: &'static str,
1020 value: &[u8],
1021 max: usize,
1022 allow_empty: bool,
1023) -> Result<(), DialogPersistenceError> {
1024 if value.len() > max {
1025 return Err(DialogPersistenceError::FieldTooLarge {
1026 field,
1027 len: value.len(),
1028 max,
1029 });
1030 }
1031 if !allow_empty && value.is_empty() {
1032 return Err(DialogPersistenceError::InvalidValue { field });
1033 }
1034 if !value.iter().all(|byte| (0x20..=0x7e).contains(byte)) {
1035 return Err(DialogPersistenceError::InvalidValue { field });
1036 }
1037 Ok(())
1038}
1039
1040fn validate_token(field: &'static str, value: &[u8]) -> Result<(), DialogPersistenceError> {
1041 validate_bytes(field, value, MAX_ID_BYTES, false)?;
1042 if !value.iter().copied().all(is_token_char) {
1043 return Err(DialogPersistenceError::InvalidValue { field });
1044 }
1045 Ok(())
1046}
1047
1048fn is_token_char(byte: u8) -> bool {
1049 byte.is_ascii_alphanumeric()
1050 || matches!(
1051 byte,
1052 b'-' | b'.' | b'!' | b'%' | b'*' | b'_' | b'+' | b'`' | b'\'' | b'~'
1053 )
1054}
1055
1056fn validate_party(field: &'static str, value: &str) -> Result<(), DialogPersistenceError> {
1057 validate_bytes(field, value.as_bytes(), MAX_FIELD_BYTES, false)?;
1058 let address = Address::parse(value.as_bytes(), field)
1059 .map_err(|_| DialogPersistenceError::InvalidValue { field })?;
1060 if address.tag().is_some() || address.uri.password().is_some() || !address.uri.scheme().is_sip()
1061 {
1062 return Err(DialogPersistenceError::InvalidValue { field });
1063 }
1064 Ok(())
1065}
1066
1067fn validate_uri(field: &'static str, value: &Uri) -> Result<(), DialogPersistenceError> {
1068 let bytes = value.to_bytes();
1069 validate_bytes(field, &bytes, MAX_FIELD_BYTES, false)?;
1070 if !value.scheme().is_sip() || value.password().is_some() {
1071 return Err(DialogPersistenceError::InvalidValue { field });
1072 }
1073 Ok(())
1074}
1075
1076fn validate_route(value: &str) -> Result<(), DialogPersistenceError> {
1077 validate_bytes("route", value.as_bytes(), MAX_FIELD_BYTES, false)?;
1078 let route = Address::parse(value.as_bytes(), "Route")
1079 .map_err(|_| DialogPersistenceError::InvalidValue { field: "route" })?;
1080 if !route.uri.scheme().is_sip() || route.uri.password().is_some() {
1081 return Err(DialogPersistenceError::InvalidValue { field: "route" });
1082 }
1083 Ok(())
1084}
1085
1086fn requires_secure_route(target: &Uri, routes: &[String]) -> Result<bool, DialogPersistenceError> {
1087 if target.scheme().is_secure() {
1088 return Ok(true);
1089 }
1090 for route in routes {
1091 let address = Address::parse(route.as_bytes(), "Route")
1092 .map_err(|_| DialogPersistenceError::InvalidValue { field: "route" })?;
1093 if address.uri.scheme().is_secure() {
1094 return Ok(true);
1095 }
1096 }
1097 Ok(false)
1098}
1099
1100struct Reader<'a> {
1101 remaining: &'a [u8],
1102 variable: usize,
1103}
1104
1105impl<'a> Reader<'a> {
1106 const fn new(input: &'a [u8]) -> Self {
1107 Self {
1108 remaining: input,
1109 variable: 0,
1110 }
1111 }
1112
1113 fn exact(
1114 &mut self,
1115 len: usize,
1116 field: &'static str,
1117 ) -> Result<&'a [u8], DialogPersistenceError> {
1118 let Some(value) = self.remaining.get(..len) else {
1119 return Err(DialogPersistenceError::Truncated { field });
1120 };
1121 self.remaining = self.remaining.get(len..).unwrap_or_default();
1122 Ok(value)
1123 }
1124
1125 fn u8(&mut self, field: &'static str) -> Result<u8, DialogPersistenceError> {
1126 self.exact(1, field)?
1127 .first()
1128 .copied()
1129 .ok_or(DialogPersistenceError::Truncated { field })
1130 }
1131
1132 fn u16(&mut self, field: &'static str) -> Result<u16, DialogPersistenceError> {
1133 let value = self.exact(2, field)?;
1134 let octets: [u8; 2] = value
1135 .try_into()
1136 .map_err(|_| DialogPersistenceError::Truncated { field })?;
1137 Ok(u16::from_be_bytes(octets))
1138 }
1139
1140 fn u32(&mut self, field: &'static str) -> Result<u32, DialogPersistenceError> {
1141 let value = self.exact(4, field)?;
1142 let octets: [u8; 4] = value
1143 .try_into()
1144 .map_err(|_| DialogPersistenceError::Truncated { field })?;
1145 Ok(u32::from_be_bytes(octets))
1146 }
1147
1148 fn u64(&mut self, field: &'static str) -> Result<u64, DialogPersistenceError> {
1149 let value = self.exact(8, field)?;
1150 let octets: [u8; 8] = value
1151 .try_into()
1152 .map_err(|_| DialogPersistenceError::Truncated { field })?;
1153 Ok(u64::from_be_bytes(octets))
1154 }
1155
1156 fn bytes(
1157 &mut self,
1158 field: &'static str,
1159 max: usize,
1160 ) -> Result<Vec<u8>, DialogPersistenceError> {
1161 let declared = usize::try_from(self.u32(field)?).map_err(|_| {
1162 DialogPersistenceError::FieldTooLarge {
1163 field,
1164 len: usize::MAX,
1165 max,
1166 }
1167 })?;
1168 if declared > max {
1169 return Err(DialogPersistenceError::FieldTooLarge {
1170 field,
1171 len: declared,
1172 max,
1173 });
1174 }
1175 let next_total = self
1176 .variable
1177 .checked_add(declared)
1178 .ok_or(DialogPersistenceError::VariableDataTooLarge)?;
1179 if next_total > MAX_VARIABLE_BYTES {
1180 return Err(DialogPersistenceError::VariableDataTooLarge);
1181 }
1182 let value = self.exact(declared, field)?.to_vec();
1183 self.variable = next_total;
1184 Ok(value)
1185 }
1186
1187 fn string(
1188 &mut self,
1189 field: &'static str,
1190 max: usize,
1191 ) -> Result<String, DialogPersistenceError> {
1192 String::from_utf8(self.bytes(field, max)?)
1193 .map_err(|_| DialogPersistenceError::InvalidUtf8 { field })
1194 }
1195
1196 fn optional_u8(&mut self, field: &'static str) -> Result<Option<u8>, DialogPersistenceError> {
1197 match self.u8(field)? {
1198 0 => Ok(None),
1199 1 => self.u8(field).map(Some),
1200 _ => Err(DialogPersistenceError::NonCanonicalPresence { field }),
1201 }
1202 }
1203
1204 fn optional_u32(&mut self, field: &'static str) -> Result<Option<u32>, DialogPersistenceError> {
1205 match self.u8(field)? {
1206 0 => Ok(None),
1207 1 => self.u32(field).map(Some),
1208 _ => Err(DialogPersistenceError::NonCanonicalPresence { field }),
1209 }
1210 }
1211
1212 const fn is_empty(&self) -> bool {
1213 self.remaining.is_empty()
1214 }
1215}
1216
1217fn put_u16(out: &mut Vec<u8>, value: u16) {
1218 out.extend_from_slice(&value.to_be_bytes());
1219}
1220
1221fn put_u32(out: &mut Vec<u8>, value: u32) {
1222 out.extend_from_slice(&value.to_be_bytes());
1223}
1224
1225fn put_u64(out: &mut Vec<u8>, value: u64) {
1226 out.extend_from_slice(&value.to_be_bytes());
1227}
1228
1229fn put_bytes(out: &mut Vec<u8>, value: &[u8]) {
1230 put_u32(out, u32::try_from(value.len()).unwrap_or(u32::MAX));
1231 out.extend_from_slice(value);
1232}
1233
1234fn put_optional_u8(out: &mut Vec<u8>, value: Option<u8>) {
1235 match value {
1236 None => out.push(0),
1237 Some(value) => {
1238 out.push(1);
1239 out.push(value);
1240 }
1241 }
1242}
1243
1244fn put_optional_u32(out: &mut Vec<u8>, value: Option<u32>) {
1245 match value {
1246 None => out.push(0),
1247 Some(value) => {
1248 out.push(1);
1249 put_u32(out, value);
1250 }
1251 }
1252}
1253
1254const fn keying_id(value: NegotiatedKeying) -> u8 {
1255 match value {
1256 NegotiatedKeying::Plain => 0,
1257 NegotiatedKeying::Sdes => 1,
1258 NegotiatedKeying::DtlsSrtp => 2,
1259 }
1260}
1261
1262fn decode_keying(value: u8) -> Result<NegotiatedKeying, DialogPersistenceError> {
1263 match value {
1264 0 => Ok(NegotiatedKeying::Plain),
1265 1 => Ok(NegotiatedKeying::Sdes),
1266 2 => Ok(NegotiatedKeying::DtlsSrtp),
1267 _ => Err(DialogPersistenceError::InvalidValue {
1268 field: "media keying",
1269 }),
1270 }
1271}
1272
1273const fn profile_id(value: MediaProfile) -> u8 {
1274 match value {
1275 MediaProfile::Standard => 0,
1276 MediaProfile::BrowserAudio => 1,
1277 }
1278}
1279
1280fn decode_profile(value: u8) -> Result<MediaProfile, DialogPersistenceError> {
1281 match value {
1282 0 => Ok(MediaProfile::Standard),
1283 1 => Ok(MediaProfile::BrowserAudio),
1284 _ => Err(DialogPersistenceError::InvalidValue {
1285 field: "media profile",
1286 }),
1287 }
1288}
1289
1290const fn preference_id(value: crate::CodecPreference) -> u8 {
1291 match value {
1292 crate::CodecPreference::Pcmu => 0,
1293 crate::CodecPreference::Pcma => 1,
1294 crate::CodecPreference::Opus => 2,
1295 crate::CodecPreference::L16 => 3,
1296 }
1297}
1298
1299fn decode_preference(value: u8) -> Result<crate::CodecPreference, DialogPersistenceError> {
1300 match value {
1301 0 => Ok(crate::CodecPreference::Pcmu),
1302 1 => Ok(crate::CodecPreference::Pcma),
1303 2 => Ok(crate::CodecPreference::Opus),
1304 3 => Ok(crate::CodecPreference::L16),
1305 _ => Err(DialogPersistenceError::InvalidValue {
1306 field: "codec preference",
1307 }),
1308 }
1309}
1310
1311const fn codec_id(value: Codec) -> u8 {
1312 match value {
1313 Codec::Pcmu => 0,
1314 Codec::Pcma => 1,
1315 #[cfg(feature = "opus")]
1316 Codec::Opus => 2,
1317 Codec::L16 => 3,
1318 }
1319}
1320
1321fn decode_codec(value: u8) -> Result<Codec, DialogPersistenceError> {
1322 match value {
1323 0 => Ok(Codec::Pcmu),
1324 1 => Ok(Codec::Pcma),
1325 #[cfg(feature = "opus")]
1326 2 => Ok(Codec::Opus),
1327 #[cfg(not(feature = "opus"))]
1328 2 => Err(DialogPersistenceError::UnsupportedCodec(2)),
1329 3 => Ok(Codec::L16),
1330 other => Err(DialogPersistenceError::UnsupportedCodec(other)),
1331 }
1332}
1333
1334const fn rtcp_id(value: RtcpMode) -> u8 {
1335 match value {
1336 RtcpMode::Separate => 0,
1337 RtcpMode::Mux => 1,
1338 }
1339}
1340
1341fn decode_rtcp(value: u8) -> Result<RtcpMode, DialogPersistenceError> {
1342 match value {
1343 0 => Ok(RtcpMode::Separate),
1344 1 => Ok(RtcpMode::Mux),
1345 _ => Err(DialogPersistenceError::InvalidValue { field: "RTCP mode" }),
1346 }
1347}
1348
1349const fn direction_id(value: Direction) -> u8 {
1350 match value {
1351 Direction::SendRecv => 0,
1352 Direction::SendOnly => 1,
1353 Direction::RecvOnly => 2,
1354 Direction::Inactive => 3,
1355 }
1356}
1357
1358fn decode_direction(value: u8) -> Result<Direction, DialogPersistenceError> {
1359 match value {
1360 0 => Ok(Direction::SendRecv),
1361 1 => Ok(Direction::SendOnly),
1362 2 => Ok(Direction::RecvOnly),
1363 3 => Ok(Direction::Inactive),
1364 _ => Err(DialogPersistenceError::InvalidValue {
1365 field: "hold direction",
1366 }),
1367 }
1368}
1369
1370#[cfg(test)]
1371#[allow(
1372 clippy::unwrap_used,
1373 clippy::expect_used,
1374 clippy::panic,
1375 clippy::indexing_slicing
1376)]
1377mod tests {
1378 use super::*;
1379
1380 fn uri(value: &str) -> Uri {
1381 Uri::parse(Bytes::copy_from_slice(value.as_bytes())).expect("fixture URI")
1382 }
1383
1384 fn parts() -> SnapshotParts {
1385 SnapshotParts {
1386 role: Role::Caller,
1387 id: DialogId {
1388 call_id: b"persist-1@example.net".to_vec(),
1389 local_tag: b"lt".to_vec(),
1390 remote_tag: b"rt".to_vec(),
1391 },
1392 local_party: "Alice <sip:alice@example.net>".to_owned(),
1393 remote_party: "Bob <sip:bob@example.org>".to_owned(),
1394 remote_target: uri("sip:refreshed@192.0.2.20:5070"),
1395 route_set: vec![
1396 "<sip:first.example;lr>".to_owned(),
1397 "<sip:second.example;lr>".to_owned(),
1398 ],
1399 local_cseq: 41,
1400 remote_cseq: Some(9),
1401 protected_signalling: false,
1402 media_keying: NegotiatedKeying::Plain,
1403 media_profile: MediaProfile::Standard,
1404 codecs: Codecs::G711,
1405 codec: Codec::Pcmu,
1406 clock_rate: 8_000,
1407 payload_type: 0,
1408 receive_payload_type: 0,
1409 dtmf_payload_type: Some(101),
1410 rtcp_mode: RtcpMode::Mux,
1411 hold: Direction::SendRecv,
1412 peer_allows_update: true,
1413 session: None,
1414 }
1415 }
1416
1417 fn fixture() -> DialogSnapshot {
1418 DialogSnapshot::from_parts(parts()).expect("valid fixture")
1419 }
1420
1421 fn leading_field(bytes: &[u8], ordinal: usize) -> std::ops::Range<usize> {
1423 let mut at = 8usize;
1424 for index in 0..=ordinal {
1425 let len = u32::from_be_bytes(bytes[at..at + 4].try_into().expect("length")) as usize;
1426 let range = at + 4..at + 4 + len;
1427 if index == ordinal {
1428 return range;
1429 }
1430 at = range.end;
1431 }
1432 panic!("field ordinal exists")
1433 }
1434
1435 fn after_routes(bytes: &[u8]) -> usize {
1436 let target = leading_field(bytes, 5);
1437 let mut at = target.end;
1438 let count = u16::from_be_bytes(bytes[at..at + 2].try_into().expect("route count"));
1439 at += 2;
1440 for _ in 0..count {
1441 let len = u32::from_be_bytes(bytes[at..at + 4].try_into().expect("route length"));
1442 at += 4 + usize::try_from(len).expect("route length fits");
1443 }
1444 at
1445 }
1446
1447 fn media_offsets(bytes: &[u8]) -> (usize, usize, usize, usize) {
1448 let mut at = after_routes(bytes) + 4;
1449 let remote_cseq_present = bytes[at];
1450 at += 1 + if remote_cseq_present == 1 { 4 } else { 0 };
1451 at += 2;
1452 let preference_count = usize::from(bytes[at]);
1453 at += 1 + preference_count;
1454 at += 1;
1455 let clock_rate = at;
1456 at += 4;
1457 let payload_type = at;
1458 let receive_payload_type = payload_type + 1;
1459 let dtmf_marker = receive_payload_type + 1;
1460 assert_eq!(bytes[dtmf_marker], 1, "fixture carries a DTMF payload");
1461 (
1462 clock_rate,
1463 payload_type,
1464 receive_payload_type,
1465 dtmf_marker + 1,
1466 )
1467 }
1468
1469 #[test]
1470 fn dp1_is_canonical_and_preserves_the_complete_dialog_order() {
1471 let snapshot = fixture();
1472 let bytes = snapshot.encode();
1473 let decoded = DialogSnapshot::decode(&bytes).expect("decodes");
1474 assert_eq!(decoded.encode(), bytes);
1475 assert_eq!(decoded.role(), Role::Caller);
1476 assert_eq!(decoded.local_cseq(), 41);
1477 assert_eq!(decoded.remote_cseq(), Some(9));
1478 assert_eq!(
1479 decoded.remote_target().to_bytes(),
1480 snapshot.remote_target().to_bytes()
1481 );
1482 assert_eq!(
1483 decoded.route_set(),
1484 ["<sip:first.example;lr>", "<sip:second.example;lr>",]
1485 );
1486 }
1487
1488 #[test]
1489 fn dp2_rejects_an_unknown_version_before_reading_variable_fields() {
1490 let mut bytes = fixture().encode();
1491 bytes[4..6].copy_from_slice(&3u16.to_be_bytes());
1492 bytes.truncate(6);
1493 assert_eq!(
1494 DialogSnapshot::decode(&bytes).unwrap_err(),
1495 DialogPersistenceError::UnsupportedVersion(3)
1496 );
1497 }
1498
1499 #[test]
1502 fn version_one_snapshots_decode_with_a_symmetric_payload_assignment() {
1503 let mut legacy = fixture().encode();
1504 legacy[4..6].copy_from_slice(&LEGACY_VERSION.to_be_bytes());
1505 let (clock_rate, _, receive_payload_type, _) = media_offsets(&legacy);
1506 legacy.remove(receive_payload_type);
1507 legacy.drain(clock_rate..clock_rate + 4);
1508
1509 let decoded = DialogSnapshot::decode(&legacy).expect("version one remains readable");
1510 assert_eq!(decoded.payload_type(), 0);
1511 assert_eq!(decoded.receive_payload_type(), 0);
1512 assert_eq!(decoded.clock_rate(), 8_000);
1513 assert_eq!(
1514 decoded.version(),
1515 VERSION,
1516 "re-encoding upgrades the schema"
1517 );
1518 }
1519
1520 #[test]
1521 fn dp3_checks_declared_field_bounds_before_allocation_or_copy() {
1522 let mut oversized = fixture().encode();
1523 oversized[8..12].copy_from_slice(&u32::try_from(MAX_ID_BYTES + 1).unwrap().to_be_bytes());
1524 assert_eq!(
1525 DialogSnapshot::decode(&oversized).unwrap_err(),
1526 DialogPersistenceError::FieldTooLarge {
1527 field: "Call-ID",
1528 len: MAX_ID_BYTES + 1,
1529 max: MAX_ID_BYTES,
1530 }
1531 );
1532
1533 let mut truncated = fixture().encode();
1534 truncated[8..12].copy_from_slice(&1000u32.to_be_bytes());
1535 assert_eq!(
1536 DialogSnapshot::decode(&truncated).unwrap_err(),
1537 DialogPersistenceError::Truncated { field: "Call-ID" }
1538 );
1539
1540 let huge = vec![0u8; MAX_SNAPSHOT_BYTES + 1];
1541 assert_eq!(
1542 DialogSnapshot::decode(&huge).unwrap_err(),
1543 DialogPersistenceError::InputTooLarge {
1544 len: MAX_SNAPSHOT_BYTES + 1,
1545 max: MAX_SNAPSHOT_BYTES,
1546 }
1547 );
1548 }
1549
1550 #[test]
1551 fn dp4_rejects_empty_duplicate_and_malformed_identity_fields() {
1552 let bytes = fixture().encode();
1553
1554 let mut empty_tag = bytes.clone();
1555 let local = leading_field(&empty_tag, 1);
1556 empty_tag[local.start - 4..local.start].copy_from_slice(&0u32.to_be_bytes());
1557 empty_tag.drain(local);
1558 assert_eq!(
1559 DialogSnapshot::decode(&empty_tag).unwrap_err(),
1560 DialogPersistenceError::InvalidValue { field: "local tag" }
1561 );
1562
1563 let mut duplicate = bytes.clone();
1564 let local = leading_field(&duplicate, 1);
1565 let remote = leading_field(&duplicate, 2);
1566 let local_value = duplicate[local].to_vec();
1567 duplicate[remote].copy_from_slice(&local_value);
1568 assert_eq!(
1569 DialogSnapshot::decode(&duplicate).unwrap_err(),
1570 DialogPersistenceError::DuplicateTags
1571 );
1572
1573 let mut malformed_target = bytes;
1574 let target = leading_field(&malformed_target, 5);
1575 malformed_target[target.clone()].fill(b'x');
1576 assert_eq!(
1577 DialogSnapshot::decode(&malformed_target).unwrap_err(),
1578 DialogPersistenceError::InvalidValue {
1579 field: "remote target"
1580 }
1581 );
1582 }
1583
1584 #[test]
1585 fn dp4_rejects_route_flag_presence_and_trailing_noncanonical_forms() {
1586 let bytes = fixture().encode();
1587
1588 let mut too_many_routes = bytes.clone();
1589 let route_count = leading_field(&too_many_routes, 5).end;
1590 too_many_routes[route_count..route_count + 2]
1591 .copy_from_slice(&u16::try_from(MAX_ROUTES + 1).unwrap().to_be_bytes());
1592 assert_eq!(
1593 DialogSnapshot::decode(&too_many_routes).unwrap_err(),
1594 DialogPersistenceError::TooManyRoutes {
1595 count: MAX_ROUTES + 1
1596 }
1597 );
1598
1599 let mut reserved = bytes.clone();
1600 reserved[6..8].copy_from_slice(&(1u16 << 15).to_be_bytes());
1601 assert_eq!(
1602 DialogSnapshot::decode(&reserved).unwrap_err(),
1603 DialogPersistenceError::ReservedFlags(1u16 << 15)
1604 );
1605
1606 let mut noncanonical = bytes.clone();
1607 let remote_marker = after_routes(&noncanonical) + 4;
1608 noncanonical[remote_marker] = 2;
1609 assert_eq!(
1610 DialogSnapshot::decode(&noncanonical).unwrap_err(),
1611 DialogPersistenceError::NonCanonicalPresence {
1612 field: "remote CSeq"
1613 }
1614 );
1615
1616 let mut trailing = bytes;
1617 trailing.push(0);
1618 assert_eq!(
1619 DialogSnapshot::decode(&trailing).unwrap_err(),
1620 DialogPersistenceError::TrailingBytes
1621 );
1622 }
1623
1624 #[test]
1625 fn cseq_exhaustion_and_session_contradictions_are_typed() {
1626 let mut exhausted = fixture().encode();
1627 let cseq = after_routes(&exhausted);
1628 exhausted[cseq..cseq + 4].copy_from_slice(&u32::MAX.to_be_bytes());
1629 assert_eq!(
1630 DialogSnapshot::decode(&exhausted).unwrap_err(),
1631 DialogPersistenceError::CseqExhausted
1632 );
1633
1634 let mut contradictory = parts();
1635 contradictory.session = Some(SessionSnapshot {
1636 interval: Duration::from_secs(90),
1637 we_refresh: true,
1638 remaining: Duration::from_secs(91),
1639 });
1640 assert!(matches!(
1641 DialogSnapshot::from_parts(contradictory),
1642 Err(DialogPersistenceError::SessionContradiction)
1643 ));
1644 }
1645
1646 #[test]
1647 fn hostile_payload_types_outside_the_rtp_header_range_are_typed_refusals() {
1648 let canonical = fixture().encode();
1649 let (_, payload_type, receive_payload_type, dtmf_payload_type) = media_offsets(&canonical);
1650 for (offset, field) in [
1651 (payload_type, "payload type"),
1652 (receive_payload_type, "receive payload type"),
1653 (dtmf_payload_type, "DTMF payload type"),
1654 ] {
1655 for value in [128, u8::MAX] {
1656 let mut hostile = canonical.clone();
1657 hostile[offset] = value;
1658 assert_eq!(
1659 DialogSnapshot::decode(&hostile).unwrap_err(),
1660 DialogPersistenceError::PayloadTypeOutOfRange { field, value }
1661 );
1662 }
1663 }
1664 }
1665
1666 #[test]
1667 fn every_hostile_prefix_is_a_value_and_never_a_panic() {
1668 let canonical = fixture().encode();
1669 for length in 0..canonical.len() {
1670 assert!(DialogSnapshot::decode(&canonical[..length]).is_err());
1671 }
1672 for offset in 0..canonical.len() {
1673 let mut hostile = canonical.clone();
1674 hostile[offset] ^= 0xff;
1675 let _ = DialogSnapshot::decode(&hostile);
1676 }
1677 for length in [0usize, 1, 7, 31, 255, 4096] {
1678 let hostile = vec![0xff; length];
1679 let _ = DialogSnapshot::decode(&hostile);
1680 }
1681 }
1682
1683 #[test]
1684 fn debug_output_redacts_party_and_identifier_values() {
1685 let snapshot = fixture();
1686 let rendered = format!("{snapshot:?}");
1687 for secret in ["Alice", "Bob", "persist-1", "refreshed", "first.example"] {
1688 assert!(
1689 !rendered.contains(secret),
1690 "debug leaked {secret}: {rendered}"
1691 );
1692 }
1693 assert!(rendered.contains("call_id_bytes"));
1694 }
1695
1696 #[test]
1697 fn password_bearing_uris_are_never_accepted_as_durable_protocol_facts() {
1698 let mut target = parts();
1699 target.remote_target = uri("sip:alice:credential@example.net");
1700 assert!(matches!(
1701 DialogSnapshot::from_parts(target),
1702 Err(DialogPersistenceError::InvalidValue {
1703 field: "remote target"
1704 })
1705 ));
1706
1707 let mut party = parts();
1708 party.local_party = "<sip:alice:credential@example.net>".to_owned();
1709 assert!(matches!(
1710 DialogSnapshot::from_parts(party),
1711 Err(DialogPersistenceError::InvalidValue {
1712 field: "local party"
1713 })
1714 ));
1715 }
1716
1717 #[tokio::test]
1718 async fn dp6_refuses_mismatched_keying_without_consuming_the_fresh_media() {
1719 let mut secure = parts();
1720 secure.protected_signalling = true;
1721 secure.media_keying = NegotiatedKeying::Sdes;
1722 let snapshot = DialogSnapshot::from_parts(secure).expect("secure snapshot");
1723
1724 let (endpoint, _incoming) = sipx_transport::bind(sipx_transport::Config::new(
1725 "127.0.0.1:0".parse().expect("endpoint address"),
1726 ))
1727 .await
1728 .expect("endpoint binds");
1729 let remote: SocketAddr = "127.0.0.1:40000".parse().expect("media remote");
1730 let mut media_config = sipx_media::Config::new(remote, Codec::Pcmu);
1731 media_config.rtcp_mode = RtcpMode::Mux;
1732 let media = Arc::new(
1733 MediaSession::start("127.0.0.1:0".parse().expect("media bind"), media_config)
1734 .await
1735 .expect("plain media starts"),
1736 );
1737 let context = DialogRestoreContext::new(
1738 endpoint.clone(),
1739 Target::new(endpoint.local_addr(), sipx_transport::TransportKind::Tls),
1740 Arc::clone(&media),
1741 MediaAddress::new("127.0.0.1".parse().expect("media address")),
1742 remote,
1743 MediaPolicy::default().with_keying(Keying::Sdes),
1744 snapshot.direction(),
1745 Duration::ZERO,
1746 Instant::now(),
1747 );
1748 assert_eq!(endpoint.outstanding().await.expect("outstanding"), 0);
1749 assert_eq!(
1750 snapshot.validate_restore(&context).unwrap_err(),
1751 DialogPersistenceError::MediaSecurityMismatch
1752 );
1753 assert_eq!(endpoint.outstanding().await.expect("outstanding"), 0);
1754 assert_eq!(Arc::strong_count(&media), 2);
1755
1756 drop(context);
1757 drop(media);
1758 endpoint.shutdown().await;
1759 }
1760
1761 #[tokio::test]
1762 async fn restore_context_debug_omits_a_secret_bearing_websocket_target_path() {
1763 let (endpoint, _incoming) = sipx_transport::bind(sipx_transport::Config::new(
1764 "127.0.0.1:0".parse().expect("endpoint address"),
1765 ))
1766 .await
1767 .expect("endpoint binds");
1768 let remote: SocketAddr = "127.0.0.1:40000".parse().expect("media remote");
1769 let mut media_config = sipx_media::Config::new(remote, Codec::Pcmu);
1770 media_config.rtcp_mode = RtcpMode::Mux;
1771 let media = Arc::new(
1772 MediaSession::start("127.0.0.1:0".parse().expect("media bind"), media_config)
1773 .await
1774 .expect("media starts"),
1775 );
1776 let secret = "never-print-this-restore-token";
1777 let context = DialogRestoreContext::new(
1778 endpoint.clone(),
1779 Target::new(endpoint.local_addr(), sipx_transport::TransportKind::Wss)
1780 .verifying("internal-signalling.example")
1781 .at_path(format!("/calls?access_token={secret}")),
1782 Arc::clone(&media),
1783 MediaAddress::new("127.0.0.1".parse().expect("media address")),
1784 remote,
1785 MediaPolicy::default(),
1786 Direction::SendRecv,
1787 Duration::ZERO,
1788 Instant::now(),
1789 );
1790
1791 let context_debug = format!("{context:?}");
1792 assert!(!context_debug.contains(secret));
1793 assert!(!context_debug.contains("/calls"));
1794 assert!(!context_debug.contains("internal-signalling.example"));
1795 assert!(context_debug.contains("Wss"));
1796 assert!(context_debug.contains("target_has_path: true"));
1797
1798 drop(context);
1799 drop(media);
1800 endpoint.shutdown().await;
1801 }
1802}