Emergency calling, stage 1: urn:service:sos with a digits fallback

Classify 112/911 (plus EMERGENCY_NUMBERS) at Dial. A classified call
INVITEs urn:service:sos over the existing registration and, on any
non-2xx final not caused by the user hanging up (including the
setup-timeout CANCEL paths), retries once as a plain INVITE of the
dialled digits — the pre-0.3.0 behavior, so classification can never
place a call worse than the status quo. The reverse edge: a 380
Alternative Service whose body carries the emergency indication
upgrades an unclassified call to the sos URN; a bare 380 stays an
error, since promoting an arbitrary redirect would put a
non-emergency call through to a PSAP. An emergency dial preempts an
in-progress call, and an answer racing a deadline-initiated CANCEL is
taken instead of BYE'd (user-initiated CANCEL races still BYE).

No emergency registration, no emergency PDN, no CS fallback, no
SIM-less calling, no AML — and no carrier has confirmed the sos path
end-to-end. The README warning states exactly that.

Assisted-by: Claude:claude-fable-5
This commit is contained in:
Jorijn van der Graaf 2026-08-01 22:29:12 +02:00
commit f2a404855f
8 changed files with 365 additions and 42 deletions

View file

@ -222,6 +222,44 @@ int main() {
Eq(BuildInvite(c, d, Sdp, /*precond=*/false), Invite, "INVITE + AMR-WB SDP offer");
}
// ---- emergency INVITE: byte-identical to the plain INVITE except the
// Request-URI and To carry the service URN (RFC 5031 / TS 24.229 5.1.6)
{
Dialog d;
d.ruri = std::string(SosUrn);
d.callid = "CALLID789@2001:db8::db43";
d.itag = "ITAG123456";
d.invBranch = "INVBRANCH0000000000";
std::string want(Invite);
auto swap = [&](std::string_view from, std::string_view to) {
std::size_t at = want.find(from);
Check(at != std::string::npos, "sos pin template line present");
if (at != std::string::npos) want.replace(at, from.size(), to);
};
swap("INVITE sip:1233;phone-context=ims.mnc001.mcc001.3gppnetwork.org@ims.mnc001.mcc001.3gppnetwork.org;user=phone SIP/2.0\r\n",
"INVITE urn:service:sos SIP/2.0\r\n");
swap("To: <sip:1233;phone-context=ims.mnc001.mcc001.3gppnetwork.org@ims.mnc001.mcc001.3gppnetwork.org;user=phone>\r\n",
"To: <urn:service:sos>\r\n");
Eq(BuildInvite(c, d, Sdp, /*precond=*/false), want, "emergency INVITE (urn:service:sos)");
}
// ---- IsEmergency classification
Check(IsEmergency("112"), "112 is emergency");
Check(IsEmergency("911"), "911 is emergency");
Check(IsEmergency("1 1 2"), "space separators stripped");
Check(IsEmergency("1-1-2"), "dash separators stripped");
Check(!IsEmergency("0112"), "prefixed 112 is not emergency");
Check(!IsEmergency("1120"), "suffixed 112 is not emergency");
Check(!IsEmergency("+31612345678"), "E.164 number is not emergency");
Check(!IsEmergency(""), "empty string is not emergency");
Check(!IsEmergency("199"), "extra number not classified by default");
{
std::vector<std::string> extra = {"199", "088"};
Check(IsEmergency("199", extra), "EMERGENCY_NUMBERS extra matches");
Check(IsEmergency("088", extra), "second extra matches");
Check(!IsEmergency("19", extra), "extra requires exact match");
}
// ---- in-dialog PRACK/BYE + ACK + CANCEL + 200 (structural checks)
{
Dialog d;