engine: register fresh when the refresh is refused or nothing answers

After bring-up the daemon recovered a dead protected flow by
reconnecting through the existing SA and refreshing, on a 5 s → 5 min
backoff, forever. That was the whole recovery: a refresh the network
refused, or a P-CSCF that stopped answering, kept it in that loop on the
same Call-ID and SA until someone restarted the service. O2 UK,
2026-09-24: the 30-minute refresh answered 503, 21 refusals over 70 min;
after a manual restart the new registration's flow died 8 s in and no
connect over its SA succeeded for 2 h 39 min; a second restart
registered within two seconds (13:27:55 → 13:27:57 in the journal; ims
ledger F16).

Do in place the rest of what a restart does: when the refresh keeps
being refused, or reconnect cycles keep ending without an answer unless
the modem reports no packet service, register fresh. The fresh
registration is the expensive step, so a failed one waits before the
next: on a schedule (21 min, growing to 60) after a refusal or a
throttle, on a backoff after silence, and a Retry-After from the network
is honoured up to an hour. What the network set — a Retry-After, the
schedule after a refused or throttled attempt — is kept across a restart
in /run/imsd.holds, for the address it was set on. A refused initial
registration at bring-up — a final from 300 up to the unprotected
REGISTER (its 401 is the challenge), any final but 200 to the protected
one, a second 401 (rejected credentials) included — now waits on the
same schedule instead of exiting for a systemd restart every two minutes.

The policy is a pure reducer in Imsd:Engine (RegRecovery), shaped like
the call machine: the daemon supplies the clock and the network, the
reducer holds the deadlines and names the next step, and tests/Engine
pin it. tests/Engine now exits 0 or 1 instead of its failure count: with
more than 255 checks a count of 256 would exit 0, and crafter-build
reads 77 as a skipped suite. The bring-up's deferral is logged in the
policy's form: `bring-up deferred: <why> — attempt N, next fresh
registration in M min` becomes `bring-up deferred: <why>; next: fresh
registration in M min`.

A bring-up writes the verdict that the old registration is gone as well:
a start that goes on to register fresh — its resume failed, or there was
nothing to resume over — records it before the attempt, and when the
attempt then fails without an answer the daemon exits with the file in
place. The next start on the same address and boot does not resume,
where 0.3.9 retried the resume: it registers fresh at once. On KPN, with
the old registration in fact still live — a start during an outage,
staged on the dev phone — that attempt was throttled, and the 21-min
schedule began. A bring-up does not retry its resume through an outage
the way recovery reconnects.

Tested on the dev phone (KPN) in builds of this series; not every path
has run there.
This commit is contained in:
Jorijn van der Graaf 2026-09-24 18:56:34 +02:00
commit 142adf057c
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
4 changed files with 806 additions and 87 deletions

View file

@ -273,6 +273,7 @@ PCSCF=2001:db8::105
| `LOCAL` | *(auto)* | UE address; default = the global IPv6 on `DEV` |
| `USER_AGENT` | `imsd/<version>` | REGISTER User-Agent. Some networks fingerprint UAs; setting your device's stock build string reproduces the stock modem's registration exactly. Empty omits the header |
| `STATE_FILE` | `/var/lib/imsd/imsreg.state` (root) / `$XDG_STATE_HOME/imsd/imsreg.state` | persisted registration context for warm resume |
| `HOLDS_FILE` | `/run/imsd.holds` (root) / `$XDG_RUNTIME_DIR/imsd.holds` (`/tmp/imsd.holds` without a runtime dir) | the waits the network imposed (a `Retry-After`, the refusal schedule) and where that schedule stands, kept across a restart of imsd for the address they were set on — not across a reboot; `/dev/null` switches the file off (only a regular file is ever removed; anything else is emptied) |
| `RESUME` | `auto` | `0` forces a fresh registration (ignores a warm SA) |
| `REFRESH_INTERVAL` | *(auto)* | keepalive re-REGISTER period in s; default = half the granted expiry, clamped to [120, 1800] |
| `EALG` | `aes-cbc` | offered ESP cipher (`aes-cbc`, `des-ede3-cbc`, `null`). From the 401's `Security-Server` list imsd selects the highest-preference `ipsec-3gpp` mechanism that matches this offer (RFC 3329 §2.3.1) and takes the SPIs/ports from the entry carrying them (some cores list every mechanism they support and put the SPIs on the one they applied); when no entry matches, the P-CSCF's SPI-carrying entry is installed as listed (`hmac-md5-96`, an absent `ealg` = null) and the journal says so. The line, the selection and the SA parameters are logged at every fresh registration |
@ -289,6 +290,35 @@ The protected client/server ports are fixed at 45061/45062
(`imsd::util::kPortUc`/`kPortUs`) — changing them in source also requires
updating the firewall rules `ims-pdn-up.sh` installs.
**When the registration stops working** (the refresh is refused, or the
protected flow dies and nothing answers the reconnect) imsd does in place
what a restart does: it reconnects through the existing SA and refreshes;
after two refused refreshes or two unanswered reconnect cycles (such a
cycle is not counted while the modem reports no packet service) it
registers fresh (new Call-ID, new challenge, new SA pair). Reconnects back
off 5 s → 5 min; a refused or throttled fresh attempt waits on the throttle
schedule (21 min, growing to 60), a silent one 30 s → 10 min, and a
network `Retry-After` is honoured, up to an hour (the schedule's longest wait): a longer
one is cut to it and the refusal heard again then. While a fresh registration is deferred, the existing SA is
still retried on the reconnect backoff, unless the old registration is known gone: a `403` once a
fresh registration is due, or an attempt that got past the SA setup. A `Dial`
that reaches imsd while it is unregistered brings the next recovery step
forward (the reconnect, or the fresh attempt once it is due), never a wait
the network imposed (a `Retry-After`, or the schedule after a
refused or throttled fresh attempt); imsd-ofonod refuses a `Dial` then, and
imsd-dialerd lists no modem. A bring-up the network deferred waits the same
schedule out with nothing to reconnect over (the resume just found the
stored SAs unusable), then tries again. The network's holds outlive a
restart of imsd: they are kept in
`/run/imsd.holds` for the address they were set on, with the schedule's place
(a restart does not start it over at 21 min), and waited out at the next
start, before the resume (a refresh's `Retry-After`) or before the fresh
registration (a fresh attempt's schedule or `Retry-After`); a reboot or
another address starts clean.
The journal says which (`…; next: fresh registration in N s`, `registering
fresh (recovery attempt N)`, `fresh registration deferred/refused/failed:
…`, `bring-up deferred: …`).
### imsd-media (per-call, set through imsd's environment)
| Variable | Default | Meaning |