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(¶ms, "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}