Skip to main content

sipx_sip/
session.rs

1//! Session timers (RFC 4028).
2//!
3//! A SIP dialog has no keepalive. If the far end loses power, its socket never closes and no BYE
4//! is ever sent, so both sides sit in a call that no longer exists — one of them streaming audio
5//! into the void, the other gone. Session timers are the periodic "are you still there" that
6//! makes that detectable: the two ends agree an interval, one of them refreshes inside it, and
7//! whoever stops seeing refreshes tears the call down locally.
8//!
9//! Everything here is pure. The interval negotiation, the choice of who refreshes and the
10//! deadlines that follow from them are values computed from headers; the waiting and the sending
11//! happen a layer up, where there is a clock.
12
13use std::time::Duration;
14
15use crate::error::HeaderError;
16use crate::headers::grammar::{find_param_start, parse_params, parse_u64, trim};
17use crate::message::TypedHeader;
18use crate::name::HeaderName;
19
20/// The option tag that advertises support, in `Supported` and `Require` (RFC 4028 §4).
21pub const OPTION_TAG: &str = "timer";
22
23/// The floor the RFC puts under any minimum interval (RFC 4028 §9).
24///
25/// A UAS "MUST NOT" advertise a `Min-SE` below this, and the reason is an amplification attack:
26/// a short interval is a way to make a compliant peer send requests as fast as the attacker
27/// likes. Ninety seconds is the RFC's own bound on how much amplification is allowed.
28pub const ABSOLUTE_MIN_INTERVAL: Duration = Duration::from_secs(90);
29
30/// What sipx asks for when nothing else is configured (RFC 4028 §4's example value).
31///
32/// Half an hour is long enough that the refresh traffic is negligible and short enough that a
33/// dead call is not billed for an afternoon.
34pub const DEFAULT_INTERVAL: Duration = Duration::from_secs(1800);
35
36/// How far before expiry the side that is *not* refreshing gives up (RFC 4028 §10).
37///
38/// The BYE goes out slightly early rather than exactly on time, because the RFC's concern is
39/// middleboxes: a NAT or firewall that has already dropped the pinhole at the expiry instant
40/// will not pass a BYE sent after it, and the call would be torn down on one side only.
41const EARLY_BYE_CAP: Duration = Duration::from_secs(32);
42
43/// Split a header value into its numeric part and its parameter tail.
44///
45/// `get` rather than a slice index. The offset comes from [`find_param_start`] and is in range,
46/// but "in range because of what the caller did" is exactly the reasoning that stops being true
47/// after an edit, and this crate parses hostile input.
48fn split_at_params(value: &[u8]) -> (&[u8], &[u8]) {
49    let at = find_param_start(value).unwrap_or(value.len());
50    (
51        value.get(..at).unwrap_or(value),
52        value.get(at..).unwrap_or_default(),
53    )
54}
55
56/// Who refreshes the session.
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum Refresher {
59    /// The party that sent the INVITE.
60    Uac,
61    /// The party that answered it.
62    Uas,
63}
64
65impl Refresher {
66    /// The token as it appears in the `refresher` parameter.
67    #[must_use]
68    pub const fn as_str(self) -> &'static str {
69        match self {
70            Self::Uac => "uac",
71            Self::Uas => "uas",
72        }
73    }
74}
75
76/// The `Session-Expires` header (RFC 4028 §4), also spelled `x`.
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub struct SessionExpires {
79    /// The interval within which the session must be refreshed.
80    pub interval: Duration,
81    /// Who does the refreshing, when the parameter is present.
82    ///
83    /// Absent in a request means "I have no preference"; absent in a response is a peer that
84    /// has not read RFC 4028 §9, which requires it.
85    pub refresher: Option<Refresher>,
86}
87
88impl TypedHeader for SessionExpires {
89    const NAME: HeaderName = HeaderName::SessionExpires;
90
91    fn decode(value: &[u8]) -> Result<Self, HeaderError> {
92        let (delta, tail) = split_at_params(value);
93        let seconds = parse_u64(trim(delta), "Session-Expires")?;
94        let params = parse_params(tail, "Session-Expires")?;
95        let refresher = crate::headers::grammar::param(&params, "refresher")
96            .and_then(|p| p.value.as_deref())
97            .map(|v| match v {
98                v if v.eq_ignore_ascii_case(b"uac") => Ok(Refresher::Uac),
99                v if v.eq_ignore_ascii_case(b"uas") => Ok(Refresher::Uas),
100                // A refresher we do not recognise is not the same as none: "none" means the
101                // peer left the choice open, and treating an unknown token that way would let
102                // us appoint ourselves refresher against an instruction we failed to read.
103                _ => Err(HeaderError::Syntax {
104                    header: "Session-Expires",
105                }),
106            })
107            .transpose()?;
108        Ok(Self {
109            interval: Duration::from_secs(seconds),
110            refresher,
111        })
112    }
113}
114
115impl std::fmt::Display for SessionExpires {
116    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117        write!(f, "{}", self.interval.as_secs())?;
118        if let Some(refresher) = self.refresher {
119            write!(f, ";refresher={}", refresher.as_str())?;
120        }
121        Ok(())
122    }
123}
124
125/// The `Min-SE` header (RFC 4028 §5): the shortest interval the sender will accept.
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
127pub struct MinSe(pub Duration);
128
129impl TypedHeader for MinSe {
130    const NAME: HeaderName = HeaderName::MinSe;
131
132    fn decode(value: &[u8]) -> Result<Self, HeaderError> {
133        // The ABNF allows generic parameters after the value; none are defined, and an
134        // unknown one is not a reason to reject a header we otherwise understand.
135        let (delta, _) = split_at_params(value);
136        parse_u64(trim(delta), "Min-SE").map(|s| Self(Duration::from_secs(s)))
137    }
138}
139
140/// What a UAS should do about the session timer on an incoming request.
141#[derive(Debug, Clone, Copy, PartialEq, Eq)]
142pub enum Answer {
143    /// No timer on this dialog: the peer neither asked for one nor said it could run one.
144    None,
145    /// Run a timer on these terms, and say so in the 2xx.
146    Accept(Accepted),
147    /// Refuse with `422 Session Interval Too Small`, carrying this `Min-SE`.
148    TooBrief(Duration),
149}
150
151/// Terms the UAS accepted, ready to be written into the 2xx.
152#[derive(Debug, Clone, Copy, PartialEq, Eq)]
153pub struct Accepted {
154    /// The agreed interval.
155    pub interval: Duration,
156    /// Who will refresh.
157    pub refresher: Refresher,
158    /// Whether the 2xx should carry `Require: timer` (RFC 4028 §9).
159    pub require: bool,
160}
161
162/// Decide the UAS side of the negotiation (RFC 4028 §9, Table 2).
163///
164/// `floor` is local policy — the shortest interval this side is willing to be driven at. It is
165/// raised to [`ABSOLUTE_MIN_INTERVAL`] rather than trusted, because a floor below ninety seconds
166/// is exactly the amplification the RFC forbids, and a configuration mistake should not become a
167/// protocol violation.
168#[must_use]
169pub fn answer(
170    peer_supports: bool,
171    requested: Option<SessionExpires>,
172    peer_min_se: Option<Duration>,
173    floor: Duration,
174) -> Answer {
175    let floor = floor.max(ABSOLUTE_MIN_INTERVAL);
176
177    let Some(requested) = requested else {
178        // §9: `Supported: timer` without `Session-Expires` means the peer can run a timer but
179        // is not asking for one. We may still ask. A peer that said nothing at all gets
180        // nothing: putting a timer on a dialog with a UA that cannot refresh, and cannot read
181        // the response saying so, would arm a teardown the far end has no way to prevent.
182        if !peer_supports {
183            return Answer::None;
184        }
185        return Answer::Accept(Accepted {
186            interval: DEFAULT_INTERVAL.max(peer_min_se.unwrap_or(Duration::ZERO)),
187            refresher: Refresher::Uas,
188            require: true,
189        });
190    };
191
192    if requested.interval < floor {
193        return Answer::TooBrief(floor);
194    }
195
196    // §9: the UAS may reduce the interval but never increase it, and never below the peer's
197    // own `Min-SE`. sipx keeps what was asked for — reducing it only makes both sides work
198    // harder for a detection window the peer already said it was happy with.
199    let refresher = match requested.refresher {
200        // "the UAS cannot override the UAC's choice of refresher, if it made one."
201        Some(chosen) => chosen,
202        // Table 2 row 4 leaves the choice to us when both support the extension. sipx takes
203        // the job. The refresher learns of a dead peer in one transaction timeout; the other
204        // side has to wait out the whole interval, so refreshing is the faster detector.
205        None => Refresher::Uas,
206    };
207    Answer::Accept(Accepted {
208        interval: requested.interval,
209        // §9: `Require: timer` is mandatory when the UAC refreshes, because the UAC has to
210        // read the response to learn that. When we refresh it is only a SHOULD, and only
211        // meaningful to a peer that understands the tag.
212        require: refresher == Refresher::Uac || peer_supports,
213        refresher,
214    })
215}
216
217/// What a UAC learns from the 2xx to its own session refresh request (RFC 4028 §7.2).
218///
219/// `asked_for` is the interval this side put in the request, if any. It matters because a 2xx
220/// with no `Session-Expires` from a peer that never claimed to support timers does not mean "no
221/// timer" — §7.2 says the UAC may run one anyway, as refresher, purely for its own benefit.
222///
223/// The agreed interval is floored at [`ABSOLUTE_MIN_INTERVAL`]. §9 forbids a UAS from returning
224/// anything shorter, and §11.2 explains what a shorter one would be: a way to make this side
225/// emit requests as fast as the far end likes. Trusting the number because it arrived in a 2xx
226/// would leave the defence entirely in the hands of the party it defends against.
227#[must_use]
228pub fn adopt(response: Option<SessionExpires>, asked_for: Option<Duration>) -> Option<Session> {
229    match (response, asked_for) {
230        (Some(agreed), _) => Some(Session {
231            interval: agreed.interval.max(ABSOLUTE_MIN_INTERVAL),
232            // §7.2 says the parameter "will always be present" when Require: timer is. A peer
233            // that omits it anyway has told us an interval and not told us whose job it is;
234            // taking the job is the only reading that cannot leave the call unrefreshed.
235            we_refresh: agreed.refresher != Some(Refresher::Uas),
236        }),
237        (None, Some(interval)) => Some(Session {
238            interval: interval.max(ABSOLUTE_MIN_INTERVAL),
239            we_refresh: true,
240        }),
241        // §7.2: "If the 2xx response did not contain a Session-Expires header field, there is
242        // no session expiration." A timer can be switched off mid-dialog this way.
243        (None, None) => None,
244    }
245}
246
247/// A live session timer: the agreed interval and which side keeps it alive.
248#[derive(Debug, Clone, Copy, PartialEq, Eq)]
249pub struct Session {
250    /// The negotiated interval.
251    pub interval: Duration,
252    /// Whether this side sends the refreshes.
253    pub we_refresh: bool,
254}
255
256impl Session {
257    /// How long after the last refresh this side should act.
258    ///
259    /// Two different deadlines, because the two roles do different things. The refresher sends
260    /// at half the interval (RFC 4028 §7.2), which leaves a whole half-interval to notice a
261    /// failure and retry. The other side waits nearly the whole interval and then hangs up,
262    /// stopping short by `min(32s, interval/3)` so the BYE goes out before any middlebox on
263    /// the path decides the session is over (§10).
264    #[must_use]
265    pub fn act_after(self) -> Duration {
266        if self.we_refresh {
267            self.interval / 2
268        } else {
269            self.interval
270                .saturating_sub(EARLY_BYE_CAP.min(self.interval / 3))
271        }
272    }
273}
274
275#[cfg(test)]
276#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
277mod tests {
278    use super::*;
279
280    fn parse(value: &str) -> SessionExpires {
281        SessionExpires::decode(value.as_bytes()).expect("parses")
282    }
283
284    #[test]
285    fn a_session_expires_carries_its_interval_and_refresher() {
286        assert_eq!(
287            parse("1800;refresher=uas"),
288            SessionExpires {
289                interval: Duration::from_secs(1800),
290                refresher: Some(Refresher::Uas),
291            }
292        );
293        assert_eq!(parse("90").refresher, None);
294        assert_eq!(parse("1800;REFRESHER=UAC").refresher, Some(Refresher::Uac));
295    }
296
297    #[test]
298    fn a_refresher_that_is_neither_side_is_rejected() {
299        // Not pedantry: defaulting an unreadable value to "no preference" would let us appoint
300        // ourselves refresher while the peer believes it holds the job, and then neither side
301        // refreshes when both think the other does.
302        assert!(SessionExpires::decode(b"1800;refresher=proxy").is_err());
303    }
304
305    #[test]
306    fn a_session_expires_round_trips() {
307        for value in ["1800;refresher=uac", "90;refresher=uas", "600"] {
308            assert_eq!(parse(value).to_string(), value);
309        }
310    }
311
312    #[test]
313    fn a_min_se_survives_parameters_it_does_not_define() {
314        assert_eq!(
315            MinSe::decode(b"90").expect("parses").0,
316            Duration::from_secs(90)
317        );
318        assert_eq!(
319            MinSe::decode(b"120;ext=1").expect("parses").0,
320            Duration::from_secs(120)
321        );
322    }
323
324    #[test]
325    fn an_interval_under_the_floor_is_refused_with_the_floor() {
326        let asked = SessionExpires {
327            interval: Duration::from_secs(60),
328            refresher: None,
329        };
330        assert_eq!(
331            answer(true, Some(asked), None, Duration::from_secs(120)),
332            Answer::TooBrief(Duration::from_secs(120))
333        );
334    }
335
336    #[test]
337    fn a_floor_below_the_rfc_minimum_is_raised_to_it() {
338        // Local policy cannot opt into being an amplifier. A floor of ten seconds would let a
339        // peer drive us at six requests a minute per call.
340        let asked = SessionExpires {
341            interval: Duration::from_secs(30),
342            refresher: None,
343        };
344        assert_eq!(
345            answer(true, Some(asked), None, Duration::from_secs(10)),
346            Answer::TooBrief(ABSOLUTE_MIN_INTERVAL)
347        );
348    }
349
350    #[test]
351    fn table_2_governs_who_refreshes() {
352        let with = |refresher| {
353            let asked = SessionExpires {
354                interval: Duration::from_secs(600),
355                refresher,
356            };
357            match answer(true, Some(asked), None, ABSOLUTE_MIN_INTERVAL) {
358                Answer::Accept(accepted) => accepted,
359                other => panic!("expected acceptance, got {other:?}"),
360            }
361        };
362        // Rows 5 and 6: the UAC's choice stands, whichever way it went.
363        assert_eq!(with(Some(Refresher::Uac)).refresher, Refresher::Uac);
364        assert_eq!(with(Some(Refresher::Uas)).refresher, Refresher::Uas);
365        // Row 4: no choice made, so it is ours.
366        assert_eq!(with(None).refresher, Refresher::Uas);
367        // §9: Require is mandatory when the UAC refreshes, because it has to read the
368        // response to find that out.
369        assert!(with(Some(Refresher::Uac)).require);
370    }
371
372    #[test]
373    fn a_peer_that_never_mentioned_timers_gets_none() {
374        assert_eq!(
375            answer(false, None, None, ABSOLUTE_MIN_INTERVAL),
376            Answer::None
377        );
378    }
379
380    #[test]
381    fn support_without_a_request_lets_the_uas_ask() {
382        let Answer::Accept(accepted) = answer(true, None, None, ABSOLUTE_MIN_INTERVAL) else {
383            panic!("expected the uas to be able to ask for a timer");
384        };
385        assert_eq!(accepted.interval, DEFAULT_INTERVAL);
386        assert_eq!(accepted.refresher, Refresher::Uas);
387    }
388
389    #[test]
390    fn a_2xx_without_a_session_expires_leaves_the_asker_refreshing() {
391        // RFC 4028 §7.2: the peer does not support timers, but we asked, so the timer is ours
392        // to run — its whole benefit is ours too.
393        let session = adopt(None, Some(Duration::from_secs(600))).expect("a timer");
394        assert!(session.we_refresh);
395        assert_eq!(session.interval, Duration::from_secs(600));
396        // And with nothing asked for, there is no timer at all.
397        assert_eq!(adopt(None, None), None);
398    }
399
400    #[test]
401    fn a_2xx_cannot_drive_us_faster_than_the_floor() {
402        // §11.2's rogue UAS: a very small interval in the 2xx is how a far end turns one call
403        // into a request flood. The floor is ours to enforce, because it exists to protect us.
404        let agreed = SessionExpires {
405            interval: Duration::from_secs(5),
406            refresher: Some(Refresher::Uac),
407        };
408        let session = adopt(Some(agreed), Some(DEFAULT_INTERVAL)).expect("a timer");
409        assert_eq!(session.interval, ABSOLUTE_MIN_INTERVAL);
410        // And the refresh still goes at half of *that*, not half of five seconds.
411        assert_eq!(session.act_after(), Duration::from_secs(45));
412    }
413
414    #[test]
415    fn the_two_roles_act_at_different_times() {
416        let refreshing = Session {
417            interval: Duration::from_secs(1800),
418            we_refresh: true,
419        };
420        // Half the interval leaves the other half to notice a failure and retry.
421        assert_eq!(refreshing.act_after(), Duration::from_secs(900));
422
423        let waiting = Session {
424            we_refresh: false,
425            ..refreshing
426        };
427        // 1800 - min(32, 600) = 1768: just early enough to beat a middlebox to the punch.
428        assert_eq!(waiting.act_after(), Duration::from_secs(1768));
429
430        // On a short interval the cap does not apply and a third of it is used instead, so
431        // the BYE never overtakes the refresh it is waiting for.
432        let short = Session {
433            interval: Duration::from_secs(90),
434            we_refresh: false,
435        };
436        assert_eq!(short.act_after(), Duration::from_secs(60));
437    }
438}