Skip to main content

Module session_timer

Module session_timer 

Source
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:

  1. Pure header logicSessionExpires parse/build for the Session-Expires header (with its ;refresher=uac|uas param), min_se_in for Min-SE, and supports_timer for the Supported: timer option tag. rsip 0.4 has no typed variants for these, so they are parsed manually from rsip::Header::Other — same style as the crate’s SDP and RTP parsing.
  2. Negotiationnegotiate_uac (from a 2xx response, caller side) and negotiate_uas (from an INVITE, callee side) decide the SessionTimer: the negotiated interval and whether we are the refresher. SessionTimer::refresh_after / SessionTimer::expiry_after give the RFC 4028 §10 schedule.
  3. Runtimesession_timer_loop, shaped like crate::Registrar::keepalive_loop: a select! over sleeps and a CancellationToken that 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§

SessionExpires
Parsed Session-Expires header value: interval in seconds plus the optional refresher parameter.
SessionTimer
Negotiated session-timer state for one dialog, from our side’s perspective.
UasSessionTimer
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-Expires refresher parameter.
SessionTimerOutcome
How session_timer_loop ended.

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§

SessionDialogOps
The dialog operations session_timer_loop needs, abstracted so the loop’s timing logic stays unit-testable without a live dialog.

Functions§

min_se_in
Extract the Min-SE interval (seconds) from a header list, ignoring any generic parameters. Malformed values are treated as absent.
negotiate_uac
UAC-side negotiation: decide the SessionTimer from 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-Expires header (long or compact x form) 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
true if any Supported header (typed, untyped, or compact k form) carries the timer option tag.