Expand description
RFC 4028 session timers — keep long calls from outliving a dead dialog.
Without session timers, a call whose dialog silently died (peer crashed, NAT binding dropped, proxy lost state) lives forever: nothing on the signaling path re-validates the dialog, so an unattended consumer (voice bot, AI agent) keeps streaming RTP into the void. RFC 4028 bounds that window: one side periodically refreshes the session with a re-INVITE, and the other side tears the call down with BYE if no refresh arrives before the negotiated session interval lapses.
This module has three layers:
- Pure header logic —
SessionExpiresparse/build for theSession-Expiresheader (with its;refresher=uac|uasparam),min_se_inforMin-SE, andsupports_timerfor theSupported: timeroption tag. rsip 0.4 has no typed variants for these, so they are parsed manually fromrsip::Header::Other— same style as the crate’s SDP and RTP parsing. - Negotiation —
negotiate_uac(from a 2xx response, caller side) andnegotiate_uas(from an INVITE, callee side) decide theSessionTimer: the negotiated interval and whether we are the refresher.SessionTimer::refresh_after/SessionTimer::expiry_aftergive the RFC 4028 §10 schedule. - Runtime —
session_timer_loop, shaped likecrate::Registrar::keepalive_loop: aselect!over sleeps and aCancellationTokenthat either sends the periodic refresh re-INVITE (when we are the refresher) or watches for the peer’s refreshes and sends BYE when the session lapses.
§Wiring it up
crate::Caller and crate::Callee already negotiate for you —
crate::AcceptedDial::session_timer /
crate::AcceptedCall::session_timer carry the result. Spawn the
loop after the call confirms:
let accepted = pending.on_confirmed().await?;
if let Some(timer) = accepted.session_timer {
let dialog = accepted.dialog.clone();
let refreshed = Arc::new(Notify::new());
let sdp = build_sdp(local_ip, rtp_port); // repeat our offer
tokio::spawn({
let refreshed = refreshed.clone();
let cancel = cancel.clone();
async move {
let outcome =
session_timer_loop(&dialog, timer, Some(sdp), refreshed, cancel).await;
tracing::info!(?outcome, "session timer finished");
}
});
}When the peer is the refresher, its refresh re-INVITEs arrive on
the dialog state stream as DialogState::Updated(_, request, handle)
(after routing the transaction through
crate::SipEndpoint::dispatch_in_dialog). rsipstack does not
auto-answer them — your state pump must reply and then ping the
watchdog:
DialogState::Updated(_, _request, handle) => {
let echo = SessionExpires {
interval_secs: timer.interval_secs,
refresher: Some(Refresher::Uac), // the peer (that request's UAC) refreshes
};
handle
.respond(
StatusCode::OK,
Some(vec![
rsip::Header::ContentType("application/sdp".into()),
supported_timer_header(),
echo.header(),
]),
Some(sdp_answer.clone()),
)
.await
.ok();
refreshed.notify_one();
}Structs§
- Session
Expires - Parsed
Session-Expiresheader value: interval in seconds plus the optionalrefresherparameter. - Session
Timer - Negotiated session-timer state for one dialog, from our side’s perspective.
- UasSession
Timer - UAS-side negotiation result: the timer to run plus what to put in our 2xx so the peer agrees on it.
Enums§
- Refresher
- Which side of the original INVITE transaction performs refreshes —
the value space of the
Session-Expiresrefresherparameter. - Session
Timer Outcome - How
session_timer_loopended.
Constants§
- DEFAULT_
SESSION_ EXPIRES_ SECS - Session interval requested on outbound INVITEs, in seconds — the RFC 4028 recommended default of 30 minutes.
- MIN_
SESSION_ EXPIRES_ SECS - Smallest session interval we will run a timer at, in seconds.
RFC 4028 §4 fixes 90 s as the absolute minimum (and the default
Min-SE); anything shorter would churn re-INVITEs.
Traits§
- Session
Dialog Ops - The dialog operations
session_timer_loopneeds, abstracted so the loop’s timing logic stays unit-testable without a live dialog.
Functions§
- min_
se_ in - Extract the
Min-SEinterval (seconds) from a header list, ignoring any generic parameters. Malformed values are treated as absent. - negotiate_
uac - UAC-side negotiation: decide the
SessionTimerfrom the 2xx response to our INVITE. - negotiate_
uas - UAS-side negotiation: decide the session timer from an inbound INVITE’s headers.
- require_
timer_ header Require: timer— placed in a 2xx by the answerer when the offerer advertised timer support (RFC 4028 §9).- session_
expires_ in - Extract and parse the
Session-Expiresheader (long or compactxform) from a header list. Malformed values are logged and treated as absent — a broken peer header must not kill the call. - session_
timer_ loop - Drive RFC 4028 session keepalive for one confirmed dialog. Runs
until cancelled or the session dies; same shape as
crate::Registrar::keepalive_loop. - supported_
timer_ header Supported: timer— advertise RFC 4028 support on a request or response.- supports_
timer trueif anySupportedheader (typed, untyped, or compactkform) carries thetimeroption tag.