Skip to main content

sipx_ua/
outbound.rs

1//! Client-initiated connections — Outbound (RFC 5626).
2//!
3//! The problem it solves: a UA behind a NAT registers a `Contact` naming an address that only
4//! exists inside the NAT, and the binding the registrar records is unroutable the moment the
5//! mapping lapses. Outbound's answer is to stop routing to an *address* and route down a **flow**
6//! instead — the connection the client itself opened — identified by a token the registrar puts in
7//! `Path` and hands back to itself later.
8//!
9//! Everything here is a decision, not an action: which parameters a REGISTER carries, whether the
10//! registrar accepted the mechanism, how long to wait before the next keep-alive, and how long to
11//! wait before retrying a flow that failed. The randomised choices take the fraction as an
12//! argument so a test can pin them, with a thin `rand` wrapper for callers that do not care.
13
14use std::time::Duration;
15
16use sipx_sip::{HeaderName, Response};
17
18/// The option tag, registered in RFC 5626 §11.4.
19pub const OPTION_TAG: &str = "outbound";
20
21/// The largest `reg-id` RFC 5626 §4.2 allows: values run from 1 to 2^31 - 1.
22pub const MAX_REG_ID: u32 = 0x7fff_ffff;
23
24/// A device identity that outlives a reboot, a re-address and a change of network (§4.1).
25///
26/// §4.1 requires the value to be "persistent", which is the whole point: the registrar uses it to
27/// recognise that a new registration replaces an old one *for the same device* rather than adding
28/// a second contact for it. A UA that mints a fresh instance on every start accumulates dead
29/// bindings at the registrar and looks, to it, like a growing crowd of identical phones.
30///
31/// §4.1 says a UA "SHOULD" use a UUID URN (RFC 4122), which is what [`InstanceId::generate`]
32/// makes — but the type accepts any URN, because §4.1 permits other schemes and a UA that
33/// persisted one has to be able to present it again.
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct InstanceId(String);
36
37impl InstanceId {
38    /// A fresh random UUID URN (§4.1, RFC 4122 §4.4 — version 4).
39    ///
40    /// **Generate this once and store it.** Calling it on every start satisfies the syntax and
41    /// defeats the mechanism.
42    #[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        // RFC 4122 §4.4: version 4 in the high nibble of octet 6, variant 10 in the top bits of
49        // octet 8. Without these a peer is entitled to read the value as some other version and
50        // decide it is malformed.
51        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    /// Adopt an instance ID a UA persisted earlier.
77    ///
78    /// Rejects anything that is not a URN: §4.1's grammar is `instance-val = urn`, and a value
79    /// that is not one would be quoted into the `Contact` and rejected by the registrar rather
80    /// than by us.
81    #[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    /// The URN itself.
89    #[must_use]
90    pub fn urn(&self) -> &str {
91        &self.0
92    }
93
94    /// The `Contact` header parameter, quoted and bracketed as §4.1's grammar requires:
95    /// `+sip.instance="<urn:uuid:…>"`.
96    ///
97    /// The angle brackets are inside the quotes. Both are load-bearing — the URN contains colons,
98    /// which would otherwise terminate the parameter value.
99    #[must_use]
100    pub fn contact_param(&self) -> String {
101        format!("+sip.instance=\"<{}>\"", self.0)
102    }
103}
104
105/// Which flow a registration is for (§4.2).
106///
107/// One `reg-id` per flow, and the *same* number when that flow is refreshed or re-established —
108/// which is what tells the registrar "this replaces the binding for flow 2" rather than "here is
109/// another contact". §4.2 also requires the sequence to be stable across reboots, so these are
110/// numbered from the outbound proxy set's order rather than allocated as flows come up.
111#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
112pub struct RegId(u32);
113
114impl RegId {
115    /// A `reg-id`, if the value is one RFC 5626 §4.2 permits.
116    ///
117    /// Zero is excluded by the RFC explicitly. That is not pedantry: a registrar that receives
118    /// `reg-id=0` is entitled to reject the registration, and the failure looks like a
119    /// credentials problem rather than an off-by-one.
120    #[must_use]
121    pub fn new(value: u32) -> Option<Self> {
122        (1..=MAX_REG_ID).contains(&value).then_some(Self(value))
123    }
124
125    /// The number.
126    #[must_use]
127    pub fn value(self) -> u32 {
128        self.0
129    }
130}
131
132/// The `Contact` a REGISTER for one flow carries.
133///
134/// `base` is the contact as it would be without Outbound — `<sip:alice@192.0.2.5:5060>`. The two
135/// parameters are appended as *header* parameters, after the angle brackets, because that is where
136/// §4.1's and §4.2's grammars put them: `+sip.instance` is a `contact-param` and `reg-id` is too.
137/// Putting either inside the brackets makes it a URI parameter the registrar will not look at.
138#[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/// Add the `ob` URI parameter to a contact for a dialog-forming request (§4.3).
148///
149/// §4.3: a UA sending a dialog-forming request "MUST include the 'ob' parameter in its Contact
150/// header field" when it has no GRUU. It marks the contact as one that is only reachable back down
151/// *this flow*, so a mid-dialog request is sent over the flow rather than to the address — which,
152/// behind a NAT, is the difference between a re-INVITE arriving and vanishing.
153///
154/// `ob` is a URI parameter, so it goes inside the angle brackets, unlike `reg-id`.
155#[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        // No angle brackets: a bare URI, so the whole value is the URI and the parameter goes on
167        // the end. Bracketing it here would change what the header means if it has parameters.
168        _ => format!("{trimmed};ob"),
169    }
170}
171
172/// Whether the registrar actually performed an *outbound* registration (§6).
173///
174/// §6 requires a registrar that did to "include the 'outbound' option tag in a Require header
175/// field" of the 2xx. Checking it is what stops a UA from running keep-alives on a flow nothing is
176/// routing down, and from believing a `NAT`ed binding is durable when the registrar recorded an
177/// ordinary one.
178#[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/// Whether a registrar demands Outbound of its clients — `Require: outbound` on a failure.
187///
188/// Distinguished from [`accepted`] by where it appears rather than by the header: the same tag in
189/// the same header means "I did this" on a 2xx and "you must do this" on a 4xx.
190#[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/// The `Flow-Timer` the registrar named, if any (§6, §4.4).
214///
215/// When present it replaces the UA's own choice of keep-alive interval: the registrar is saying
216/// how long it will hold the flow open without traffic, and a UA that pings less often than that
217/// loses the flow between pings.
218#[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/// How a flow is kept alive, which depends on the transport (§4.4).
226#[derive(Debug, Clone, Copy, PartialEq, Eq)]
227pub enum Keepalive {
228    /// Double-CRLF ping, single-CRLF pong (§4.4.1). Required for connection-oriented transports.
229    Crlf,
230    /// STUN Binding Requests over the same flow (§4.4.2). Required for UDP.
231    Stun,
232}
233
234/// Whether the device is one where a keep-alive every two minutes is a battery problem (§4.4.1).
235#[derive(Debug, Clone, Copy, PartialEq, Eq)]
236pub enum Power {
237    /// Mains, or a battery large enough not to care.
238    Unconstrained,
239    /// A phone. §4.4.1 raises the interval by a factor of seven for these.
240    Constrained,
241}
242
243/// The identity of one Outbound flow: the device, and which of its flows this is (RFC 5626 §4.1,
244/// §4.2).
245///
246/// The instance ID belongs to the *device* and must outlive a reboot; the `reg-id` belongs to the
247/// flow and must be the same number every time that flow is re-established, which is what makes a
248/// new registration replace the old binding rather than add a second one.
249///
250/// Lives here rather than in `agent` because it is a pair of the two identifiers this module
251/// defines and needs no runtime to be one — `agent` re-exports it, so it is `agent::Flow` as well.
252#[derive(Debug, Clone, PartialEq, Eq)]
253pub struct Flow {
254    /// The device identity, stable across reboots.
255    pub instance: InstanceId,
256    /// Which flow of that device this registration is for.
257    pub reg_id: RegId,
258}
259
260/// Pick the keep-alive technique §4.4 mandates for a transport.
261#[cfg(feature = "runtime")]
262#[must_use]
263pub fn keepalive_for(transport: sipx_transport::TransportKind) -> Keepalive {
264    match transport {
265        // §4.4.2: "All SIP UAs MUST support the STUN keep-alive technique for UDP flows."
266        sipx_transport::TransportKind::Udp => Keepalive::Stun,
267        // §4.4.1: the CRLF technique, for the connection-oriented transports. It is deliberately
268        // the *SIP* framing rather than a transport-level ping, so it proves the SIP peer is
269        // reading rather than only that the socket is open.
270        _ => Keepalive::Crlf,
271    }
272}
273
274/// How long to wait before the next keep-alive (§4.4.1, §4.4.2).
275///
276/// `fraction` selects within the range and must be in `0.0..=1.0`; every ping re-draws it, because
277/// §4.4.1 says "the random number will be different for each keep-alive ping". Randomising is not
278/// decoration: a fleet that pings on a fixed period synchronises after any shared outage and
279/// arrives at the registrar as one spike.
280///
281/// A `Flow-Timer` from the registrar wins outright — it is a statement about how long *it* will
282/// hold the flow, so a UA's own preference is not a competing opinion.
283///
284/// The published defaults are used verbatim: 95–120 seconds, or 672–840 where battery matters, and
285/// 24–29 for STUN. §4.4.1 describes the lower bound as "20% less than the upper bound" and then
286/// gives 95 for an upper bound of 120, which is 20.8% less rather than 20%. The literal numbers
287/// are what interoperates, so those are the ones here.
288#[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
306/// How long a UA waits before pronouncing a CRLF-kept flow dead (§4.4.1).
307///
308/// "If a pong is not received within 10 seconds after sending a ping ... then the client MUST
309/// treat the flow as failed."
310pub const PONG_TIMEOUT: Duration = Duration::from_secs(10);
311
312/// The longest §4.5 will ever have a UA wait between attempts to re-establish a flow.
313pub const MAX_RECOVERY_WAIT: Duration = Duration::from_secs(1800);
314
315/// How long to wait before trying to re-establish a failed flow (§4.5).
316///
317/// `W = min(max-time, base-time * 2^consecutive-failures)`, then "a uniform random time between
318/// 50 and 100% of the upper-bound wait time".
319///
320/// `any_active` picks the base: §4.5 gives 30 seconds when every flow has failed and 90 when at
321/// least one is still up. The asymmetry is the interesting part — a UA that has *no* working flow
322/// is unreachable and should hurry; one that still has a flow is reachable already, and hurrying
323/// only adds load to a registrar that is plainly having a bad day.
324#[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
332/// Draw within an inclusive range of seconds, clamping the fraction rather than trusting it.
333fn 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/// Draw a fraction for the randomised choices above.
347#[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        // RFC 4122 §4.4: version 4 and variant 10xx. A peer is entitled to reject a UUID that
396        // claims no version.
397        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        // §4.1's grammar is `instance-val = urn`. A UA that let this through would be rejected by
430        // the registrar instead, which is a much worse place to find out.
431        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        // And it must not disturb header parameters that follow.
469        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        // §6: a registrar that performed an outbound registration MUST say so in Require.
483        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        // Silence means an ordinary registration, and running keep-alives on it would be pinging
487        // a flow nothing routes down.
488        assert!(!accepted(&response("", "200 OK")));
489        assert!(!accepted(&response("Supported: outbound\r\n", "200 OK")));
490        // `outbound` inside another tag's name is not the tag.
491        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    /// The literal defaults from §4.4.1 and §4.4.2.
528    #[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        // A fraction outside the range is clamped rather than trusted: an out-of-range draw
540        // would otherwise produce an interval outside what the registrar tolerates.
541        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    /// §4.5: `W = min(max-time, base-time * 2^consecutive-failures)`, retried after 50–100% of W.
557    #[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        // base-time 30 when every flow has failed.
561        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        // The jitter floor is half of W, never zero: retrying immediately is how a UA turns one
567        // registrar hiccup into a flood.
568        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        // §4.5's base-time is 90 when at least one flow is up and 30 when none is. A UA with no
575        // flow is unreachable and should hurry; one that is still reachable is only adding load.
576        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}