engine, messages, util, imsd: the reregistration carries the last challenge answer and new SA parameters

The keepalive refresh and the resume's re-REGISTER carried an initial
registration's first-REGISTER Authorization — an empty nonce and
response — in the second REGISTER's frame, with a Security-Client
naming the SPIs and ports in use. TS 24.229 5.1.1.4.2 has a
reregistration carry the Authorization
of the last successful authentication — the last received nonce and
the last calculated response — and a Security-Client with the new
parameter values for the two SA pairs a re-authentication would set
up; TS 33.203 7.4.0 keeps the server ports and changes the client
ports. O2 UK refused the refresh of the old shape with 503 on every
occasion on record (alyx, 2026-09-24 to 10-08, a stationary phone
included) and accepted the fresh registrations that followed; KPN
accepted the old shape without a challenge.

The fresh registration now records the 401's nonce, our cnonce and
the response in the registration state (auth_nonce, auth_cnonce,
auth_response in the state file); a reregistration sends them
verbatim, mints new SPIs and proposes PortUcNext (45063) as the
client port with the server port unchanged. A registration resumed from a
state file written before this change has no answer: its
reregistrations carry the empty digest, as before, until its next
fresh registration, and the resume says so. A 401 to a reregistration asks
for re-authentication over temporary SAs on the proposed ports (TS
24.229 5.1.1.5.1), which imsd does not implement: the refresh throws
it as a refusal and RegRecovery makes the next step the fresh
registration, replacing the in-place answer over the old SAs.

Tested with the Messages and Engine suites and on the dev phone (KPN)
with a build of this commit's tree: a resume from the previous state
file, a fresh registration, a resume that sent the stored answer,
three refreshes at a 120-s interval, an outgoing call to a bench line
and an incoming call's ring.
This commit is contained in:
Jorijn van der Graaf 2026-10-10 21:00:28 +02:00
commit 799fd1a802
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
8 changed files with 185 additions and 72 deletions

View file

@ -60,7 +60,9 @@ export namespace imsd::engine {
// bring-up, as a pure reducer: the shell supplies the clock and the
// network, this decides the next step and how long to wait for it. Two
// kinds of failure reach it. A REFUSAL: the network answered a refresh
// (or a fresh REGISTER) with a final other than 200/401. SILENCE: the
// with a final other than 200 — a 401, re-authentication, included — or
// a fresh registration's protected REGISTER with a final other than 200
// (the unprotected one's 401 is its challenge). SILENCE: the
// flow died and reconnecting through the SA, or the refresh over it,
// got no answer. Neither is retried for long on the same registration.
// TS 24.229 5.1.1.4.1 has the UE perform an initial registration after
@ -79,12 +81,15 @@ export namespace imsd::engine {
// a fresh registration after two (one 503 may be a momentary hiccup a
// retried refresh clears); one with such a Retry-After escalates at
// once — it is the network's considered answer (O2 2026-10-08: the
// refresh after the first wait was refused again, 414 s then 772 s).
// Silence escalates after two reconnect cycles
// refresh after the first wait was refused again, 414 s then 772 s) —
// and so does a 401, the network asking to re-authenticate, which imsd
// answers with a fresh registration (TS 24.229 5.1.1.5.1's temporary
// SAs on the proposed client port are not implemented). Silence
// escalates after two reconnect cycles
// that end without an answer — a TCP connect series whose last attempt
// was not a local refusal, a bind() the kernel refused, or a refresh
// that fails short of a refusal: unanswered, an unusable challenge, a
// failed send (the resume at start gives up after two unanswered
// that fails short of a refusal: unanswered, a failed send (the resume
// at start gives up after two unanswered
// connects).
// A fresh registration is the expensive step: an AKA run, a
// new SA pair, and on KPN a refusal (spi-s=0) while the registration on
@ -191,7 +196,7 @@ export namespace imsd::engine {
// cycle; `status` is the refusal's. The backoff paces the next step;
// a Retry-After of at least a second holds every attempt for its own
// length (up to RetryAfterMax), whichever is longer, and makes the
// next step the fresh registration. A 403 once a fresh
// next step the fresh registration, as does a 401. A 403 once a fresh
// registration is due says the old registration is gone (KPN drops
// it the moment an initial REGISTER arrives, throttled or not).
void OnReconnectFailure(double now, Failure f, long retryAfter = 0, int status = 0) {
@ -204,7 +209,7 @@ export namespace imsd::engine {
if (f == Failure::Refused) refusals++;
else silentCycles++;
if (refusals >= RefusalsBeforeFresh || silentCycles >= SilentCyclesBeforeFresh) fresh = true;
if (f == Failure::Refused && retryAfter > 0) fresh = true;
if (f == Failure::Refused && (retryAfter > 0 || status == 401)) fresh = true;
if (status == 403 && fresh) oldGone = true;
backoffUntil = now + reconnectDelay;
reconnectDelay = std::min(reconnectDelay * 2, ReconnectDelayMax);