1use std::time::Duration;
15
16use sipx_sip::{HeaderName, Response};
17
18pub const OPTION_TAG: &str = "outbound";
20
21pub const MAX_REG_ID: u32 = 0x7fff_ffff;
23
24#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct InstanceId(String);
36
37impl InstanceId {
38 #[must_use]
43 pub fn generate() -> Self {
44 use rand::Rng as _;
45 use std::fmt::Write as _;
46 let mut bytes = [0u8; 16];
47 rand::rng().fill(&mut bytes);
48 if let Some(octet) = bytes.get_mut(6) {
52 *octet = (*octet & 0x0f) | 0x40;
53 }
54 if let Some(octet) = bytes.get_mut(8) {
55 *octet = (*octet & 0x3f) | 0x80;
56 }
57 let hex = bytes
58 .iter()
59 .fold(String::with_capacity(32), |mut out, byte| {
60 let _ = write!(out, "{byte:02x}");
61 out
62 });
63 let mut uuid = String::with_capacity(36);
64 for (index, chunk) in [0..8, 8..12, 12..16, 16..20, 20..32]
65 .into_iter()
66 .enumerate()
67 {
68 if index > 0 {
69 uuid.push('-');
70 }
71 uuid.push_str(hex.get(chunk).unwrap_or_default());
72 }
73 Self(format!("urn:uuid:{uuid}"))
74 }
75
76 #[must_use]
82 pub fn parse(value: &str) -> Option<Self> {
83 let value = value.trim().trim_start_matches('<').trim_end_matches('>');
84 (value.len() > 4 && value.get(..4)?.eq_ignore_ascii_case("urn:"))
85 .then(|| Self(value.to_owned()))
86 }
87
88 #[must_use]
90 pub fn urn(&self) -> &str {
91 &self.0
92 }
93
94 #[must_use]
100 pub fn contact_param(&self) -> String {
101 format!("+sip.instance=\"<{}>\"", self.0)
102 }
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
112pub struct RegId(u32);
113
114impl RegId {
115 #[must_use]
121 pub fn new(value: u32) -> Option<Self> {
122 (1..=MAX_REG_ID).contains(&value).then_some(Self(value))
123 }
124
125 #[must_use]
127 pub fn value(self) -> u32 {
128 self.0
129 }
130}
131
132#[must_use]
139pub fn contact(base: &str, instance: &InstanceId, reg_id: RegId) -> String {
140 format!(
141 "{base};reg-id={};{}",
142 reg_id.value(),
143 instance.contact_param()
144 )
145}
146
147#[must_use]
156pub fn with_ob(contact: &str) -> String {
157 let trimmed = contact.trim();
158 match (trimmed.find('<'), trimmed.rfind('>')) {
159 (Some(open), Some(close)) if open < close => {
160 let mut out = String::with_capacity(trimmed.len() + 3);
161 out.push_str(trimmed.get(..close).unwrap_or_default());
162 out.push_str(";ob");
163 out.push_str(trimmed.get(close..).unwrap_or_default());
164 out
165 }
166 _ => format!("{trimmed};ob"),
169 }
170}
171
172#[must_use]
179pub fn accepted(response: &Response) -> bool {
180 response
181 .headers
182 .get_all(&HeaderName::Require)
183 .any(|header| contains_tag(&header.value(), OPTION_TAG.as_bytes()))
184}
185
186#[must_use]
191pub fn required_by(response: &Response) -> bool {
192 !response.status.is_success() && accepted(response)
193}
194
195fn contains_tag(value: &[u8], tag: &[u8]) -> bool {
196 value
197 .split(|&b| b == b',')
198 .any(|item| trim_ascii(item).eq_ignore_ascii_case(tag))
199}
200
201fn trim_ascii(value: &[u8]) -> &[u8] {
202 let start = value
203 .iter()
204 .position(|b| !b.is_ascii_whitespace())
205 .unwrap_or(value.len());
206 let end = value
207 .iter()
208 .rposition(|b| !b.is_ascii_whitespace())
209 .map_or(start, |last| last + 1);
210 value.get(start..end).unwrap_or_default()
211}
212
213#[must_use]
219pub fn flow_timer(response: &Response) -> Option<Duration> {
220 let value = response.headers.value(&HeaderName::FlowTimer)?;
221 let text = String::from_utf8_lossy(&value);
222 text.trim().parse::<u64>().ok().map(Duration::from_secs)
223}
224
225#[derive(Debug, Clone, Copy, PartialEq, Eq)]
227pub enum Keepalive {
228 Crlf,
230 Stun,
232}
233
234#[derive(Debug, Clone, Copy, PartialEq, Eq)]
236pub enum Power {
237 Unconstrained,
239 Constrained,
241}
242
243#[derive(Debug, Clone, PartialEq, Eq)]
253pub struct Flow {
254 pub instance: InstanceId,
256 pub reg_id: RegId,
258}
259
260#[cfg(feature = "runtime")]
262#[must_use]
263pub fn keepalive_for(transport: sipx_transport::TransportKind) -> Keepalive {
264 match transport {
265 sipx_transport::TransportKind::Udp => Keepalive::Stun,
267 _ => Keepalive::Crlf,
271 }
272}
273
274#[must_use]
289pub fn keepalive_interval(
290 flow_timer: Option<Duration>,
291 keepalive: Keepalive,
292 power: Power,
293 fraction: f64,
294) -> Duration {
295 if let Some(timer) = flow_timer {
296 return timer;
297 }
298 let (low, high) = match (keepalive, power) {
299 (Keepalive::Stun, _) => (24u64, 29u64),
300 (Keepalive::Crlf, Power::Unconstrained) => (95, 120),
301 (Keepalive::Crlf, Power::Constrained) => (672, 840),
302 };
303 Duration::from_secs(within(low, high, fraction))
304}
305
306pub const PONG_TIMEOUT: Duration = Duration::from_secs(10);
311
312pub const MAX_RECOVERY_WAIT: Duration = Duration::from_secs(1800);
314
315#[must_use]
325pub fn recovery_delay(consecutive_failures: u32, any_active: bool, fraction: f64) -> Duration {
326 let base = if any_active { 90u64 } else { 30 };
327 let doubled = base.saturating_mul(1u64 << consecutive_failures.min(32));
328 let upper = doubled.min(MAX_RECOVERY_WAIT.as_secs());
329 Duration::from_secs(within(upper / 2, upper, fraction))
330}
331
332fn within(low: u64, high: u64, fraction: f64) -> u64 {
334 let fraction = fraction.clamp(0.0, 1.0);
335 let span = high.saturating_sub(low);
336 #[expect(
337 clippy::cast_possible_truncation,
338 clippy::cast_precision_loss,
339 clippy::cast_sign_loss,
340 reason = "span is a small number of seconds and the fraction is clamped to 0..=1"
341 )]
342 let offset = (span as f64 * fraction).round() as u64;
343 low.saturating_add(offset)
344}
345
346#[must_use]
348pub fn fraction() -> f64 {
349 use rand::Rng as _;
350 rand::rng().random_range(0.0..=1.0)
351}
352
353#[cfg(test)]
354#[allow(
355 clippy::unwrap_used,
356 clippy::expect_used,
357 clippy::panic,
358 clippy::indexing_slicing
359)]
360mod tests {
361 use super::*;
362 use bytes::Bytes;
363 use sipx_sip::{Limits, Message, parse_datagram};
364
365 fn response(extra: &str, status: &str) -> Response {
366 let text = format!(
367 "SIP/2.0 {status}\r\n\
368 Via: SIP/2.0/UDP 192.0.2.5:5060;branch=z9hG4bKx\r\n\
369 To: <sip:alice@example.com>;tag=r\r\n\
370 From: <sip:alice@example.com>;tag=1\r\n\
371 Call-ID: reg-1@192.0.2.5\r\n\
372 CSeq: 1 REGISTER\r\n\
373 {extra}\
374 Content-Length: 0\r\n\r\n"
375 );
376 match parse_datagram(Bytes::from(text), &Limits::datagram()).expect("parses") {
377 Message::Response(r) => r,
378 Message::Request(_) => panic!("a response"),
379 }
380 }
381
382 #[test]
383 fn a_generated_instance_id_is_a_version_4_uuid_urn() {
384 let id = InstanceId::generate();
385 let urn = id.urn();
386 assert!(urn.starts_with("urn:uuid:"), "{urn}");
387 let uuid = urn.trim_start_matches("urn:uuid:");
388 assert_eq!(uuid.len(), 36, "{uuid}");
389 let groups: Vec<&str> = uuid.split('-').collect();
390 assert_eq!(
391 groups.iter().map(|g| g.len()).collect::<Vec<_>>(),
392 vec![8, 4, 4, 4, 12],
393 "{uuid}"
394 );
395 assert!(groups[2].starts_with('4'), "version nibble: {uuid}");
398 assert!(
399 matches!(groups[3].as_bytes()[0], b'8' | b'9' | b'a' | b'b'),
400 "variant nibble: {uuid}"
401 );
402 }
403
404 #[test]
405 fn two_generated_instance_ids_differ() {
406 assert_ne!(InstanceId::generate(), InstanceId::generate());
407 }
408
409 #[test]
410 fn an_instance_id_is_quoted_and_bracketed_as_the_grammar_requires() {
411 let id = InstanceId::parse("urn:uuid:00000000-0000-4000-8000-000000000000").expect("a urn");
412 assert_eq!(
413 id.contact_param(),
414 "+sip.instance=\"<urn:uuid:00000000-0000-4000-8000-000000000000>\"",
415 "the angle brackets go inside the quotes; the URN's colons would otherwise end the \
416 parameter value"
417 );
418 }
419
420 #[test]
421 fn a_persisted_instance_id_is_accepted_with_or_without_its_brackets() {
422 let bare = InstanceId::parse("urn:uuid:1234").expect("a urn");
423 let bracketed = InstanceId::parse("<urn:uuid:1234>").expect("a urn");
424 assert_eq!(bare, bracketed);
425 }
426
427 #[test]
428 fn something_that_is_not_a_urn_is_not_an_instance_id() {
429 assert!(InstanceId::parse("sip:alice@example.com").is_none());
432 assert!(InstanceId::parse("").is_none());
433 assert!(InstanceId::parse("urn:").is_none());
434 }
435
436 #[test]
437 fn reg_id_zero_is_refused_because_the_rfc_excludes_it() {
438 assert!(RegId::new(0).is_none(), "§4.2: reg-id runs from 1");
439 assert_eq!(RegId::new(1).expect("valid").value(), 1);
440 assert_eq!(RegId::new(MAX_REG_ID).expect("valid").value(), MAX_REG_ID);
441 assert!(
442 RegId::new(MAX_REG_ID + 1).is_none(),
443 "§4.2 caps at 2^31 - 1"
444 );
445 }
446
447 #[test]
448 fn a_registers_contact_carries_both_parameters_outside_the_brackets() {
449 let id = InstanceId::parse("urn:uuid:abc").expect("a urn");
450 let contact = contact(
451 "<sip:alice@192.0.2.5:5060>",
452 &id,
453 RegId::new(2).expect("valid"),
454 );
455 assert_eq!(
456 contact, "<sip:alice@192.0.2.5:5060>;reg-id=2;+sip.instance=\"<urn:uuid:abc>\"",
457 "both are contact-params, so they follow the closing bracket; inside it they would be \
458 URI parameters the registrar does not read"
459 );
460 }
461
462 #[test]
463 fn ob_goes_inside_the_brackets_because_it_is_a_uri_parameter() {
464 assert_eq!(
465 with_ob("<sip:alice@192.0.2.5:5060>"),
466 "<sip:alice@192.0.2.5:5060;ob>"
467 );
468 assert_eq!(
470 with_ob("<sip:alice@192.0.2.5:5060>;expires=600"),
471 "<sip:alice@192.0.2.5:5060;ob>;expires=600"
472 );
473 }
474
475 #[test]
476 fn a_bare_contact_uri_still_gets_ob() {
477 assert_eq!(with_ob("sip:alice@192.0.2.5"), "sip:alice@192.0.2.5;ob");
478 }
479
480 #[test]
481 fn the_registrar_says_it_did_an_outbound_registration_in_require() {
482 assert!(accepted(&response("Require: outbound\r\n", "200 OK")));
484 assert!(accepted(&response("Require: path, outbound\r\n", "200 OK")));
485 assert!(accepted(&response("Require: OUTBOUND\r\n", "200 OK")));
486 assert!(!accepted(&response("", "200 OK")));
489 assert!(!accepted(&response("Supported: outbound\r\n", "200 OK")));
490 assert!(!accepted(&response("Require: outbounded\r\n", "200 OK")));
492 }
493
494 #[test]
495 fn the_same_tag_means_demanded_on_a_failure_and_done_on_a_success() {
496 let refused = response("Require: outbound\r\n", "420 Bad Extension");
497 assert!(required_by(&refused), "a 4xx requiring it is a demand");
498 let ok = response("Require: outbound\r\n", "200 OK");
499 assert!(
500 !required_by(&ok),
501 "the same header on a 2xx is the registrar reporting what it did"
502 );
503 }
504
505 #[test]
506 fn a_flow_timer_from_the_registrar_replaces_our_own_choice() {
507 let with = response("Flow-Timer: 25\r\n", "200 OK");
508 assert_eq!(flow_timer(&with), Some(Duration::from_secs(25)));
509 assert_eq!(
510 keepalive_interval(
511 flow_timer(&with),
512 Keepalive::Crlf,
513 Power::Unconstrained,
514 0.5
515 ),
516 Duration::from_secs(25),
517 "the registrar's number is a statement about how long it holds the flow, not a \
518 preference to be averaged with ours"
519 );
520 assert_eq!(flow_timer(&response("", "200 OK")), None);
521 assert_eq!(
522 flow_timer(&response("Flow-Timer: soon\r\n", "200 OK")),
523 None
524 );
525 }
526
527 #[test]
529 fn the_keepalive_ranges_are_the_ones_the_rfc_publishes() {
530 let interval = |keepalive, power, fraction| {
531 keepalive_interval(None, keepalive, power, fraction).as_secs()
532 };
533 assert_eq!(interval(Keepalive::Crlf, Power::Unconstrained, 0.0), 95);
534 assert_eq!(interval(Keepalive::Crlf, Power::Unconstrained, 1.0), 120);
535 assert_eq!(interval(Keepalive::Crlf, Power::Constrained, 0.0), 672);
536 assert_eq!(interval(Keepalive::Crlf, Power::Constrained, 1.0), 840);
537 assert_eq!(interval(Keepalive::Stun, Power::Unconstrained, 0.0), 24);
538 assert_eq!(interval(Keepalive::Stun, Power::Unconstrained, 1.0), 29);
539 assert_eq!(interval(Keepalive::Stun, Power::Unconstrained, -3.0), 24);
542 assert_eq!(interval(Keepalive::Stun, Power::Unconstrained, 9.0), 29);
543 }
544
545 #[cfg(feature = "runtime")]
546 #[test]
547 fn udp_is_kept_alive_with_stun_and_everything_else_with_crlf() {
548 use sipx_transport::TransportKind;
549 assert_eq!(keepalive_for(TransportKind::Udp), Keepalive::Stun);
550 assert_eq!(keepalive_for(TransportKind::Tcp), Keepalive::Crlf);
551 assert_eq!(keepalive_for(TransportKind::Tls), Keepalive::Crlf);
552 assert_eq!(keepalive_for(TransportKind::Ws), Keepalive::Crlf);
553 assert_eq!(keepalive_for(TransportKind::Wss), Keepalive::Crlf);
554 }
555
556 #[test]
558 fn flow_recovery_backs_off_by_doubling_and_stops_at_half_an_hour() {
559 let all_failed = |failures, fraction| recovery_delay(failures, false, fraction).as_secs();
560 assert_eq!(all_failed(0, 1.0), 30);
562 assert_eq!(all_failed(1, 1.0), 60);
563 assert_eq!(all_failed(2, 1.0), 120);
564 assert_eq!(all_failed(6, 1.0), 1800, "30 * 64 is past max-time");
565 assert_eq!(all_failed(30, 1.0), 1800, "and it stays there");
566 assert_eq!(all_failed(2, 0.0), 60);
569 assert_eq!(all_failed(0, 0.0), 15);
570 }
571
572 #[test]
573 fn a_ua_with_a_working_flow_waits_three_times_as_long_before_retrying() {
574 assert_eq!(recovery_delay(0, true, 1.0).as_secs(), 90);
577 assert_eq!(recovery_delay(0, false, 1.0).as_secs(), 30);
578 assert_eq!(recovery_delay(3, true, 1.0).as_secs(), 720);
579 }
580
581 #[test]
582 fn the_drawn_fraction_stays_in_range() {
583 for _ in 0..64 {
584 let drawn = fraction();
585 assert!((0.0..=1.0).contains(&drawn), "{drawn}");
586 }
587 }
588}