packaging, ims-pdn-up, imsd, README: imsd runs without /etc/imsd.env and idles without a SIM

With the P-CSCF coming from the PDN's PCO, the configuration file has no
required line left, so the apk's ConditionPathExists=/etc/imsd.env drop-in
goes: the unit starts on every phone with the package, and VoLTE is on by
default. Installing the package and inserting a SIM is the opt-in; the file
holds overrides only, and its existence changes nothing. By the versioning
rule (AGENTS §11, the first commit of this series) that is what moves the
major number: the release is 1.0.0, and §11 now records it. What used to be the
file's job — keeping a phone nobody configured out of a failing unit — is now
a rule on facts, the same for every phone: a modem in the failed state with
a SIM reason makes the script write IMSD_IDLE=no-sim to /run/imsd.env and
exit 0; a profile-indexed connect series the network refuses ten times in a
row — each a persistent 3GPP cause in ModemManager's typed error (not
subscribed, unknown APN, barred, not authorised in the PLMN, authentication
failed, the service option not supported, the wrong PDN type) with packet
service at the time; counted only while the modem reads registered,
connecting or connected with packet service not detached — stricter than
the daemon's PacketServiceUp; any other read, a timeout or a transient cause
starts the count again — writes IMSD_IDLE=no-ims-pdn; and a bring-up that
ends with no P-CSCF — none configured, none discovered: no IPv6 P-CSCF read
from the PCO (PCSCF_DISCOVERY=none in the hand-off, the reader's exit 2: the
list absent, empty, unreadable or IPv4/FQDN only), discovery skipped (no
rmnet mux id) or no hand-off at all (a manual run) — and no registration of
imsd's own to resume is the daemon's own idle, decided before it takes the bus name (SaPeer and the
holds' old_gone say what can be resumed; a pair the holds wrote off is taken
out of the kernel there) and at the no-target bring-up, where a pair nothing
can resume is written off and taken out of the kernel first. imsd reads the key, logs idle: … and exits 0, which Restart=on-failure
leaves alone, so the unit stays inactive until the next boot, a systemctl
start or a start of imsd-ofonod where that runs (its Wants starts an idle
imsd again). What is not a fact keeps the retry: no coverage, a SIM still
waiting for its PIN, a modem without an ims profile (the APN-string connect
is tried), an APN-string series (the modem's own wall), a resumable pair
whose attempt failed (the fatal), and a P-CSCF reader that did not answer
(PCSCF_DISCOVERY=failed: the daemon exits 1 and the restart runs discovery
again). A phone that never had the file keeps its stopped unit through the
upgrade and starts at its next boot, registering where the rule above lets
it — at the upgrade itself where imsd-ofonod runs, whose restart pulls imsd
in through its Wants (systemd queues the marked unit's try-restart, collapses
it to a restart and starts what the unit Wants in the same transaction:
job_type_collapse, transaction_add_job_and_dependencies, 262~rc3); the
opt-out is systemctl mask --now imsd, since a plain disable is undone by
that Wants. imsd-ofonod.service loses its After=imsd.service: with imsd
starting on every phone, GNOME Calls' backend would otherwise wait behind
the ims PDN bring-up at every boot and at its marked restart at an upgrade,
for the whole bring-up (up to its TimeoutStartSec=900); it starts at once and republishes when imsd's name
appears (OnImsdAppeared).
The configuration file's path is the IMSD_ENV seam the mock bench uses.
AGENTS §6's lint figures are this tree's (109 findings in 16 of 20 files,
2026-10-13; the 72 of 2026-10-06 predate the Qmi files).

Tested: crafter-build test, 8 passed; on the host build the daemon's idle
exit for the script's keys and, with no pair, for every discovery verdict
but failed (none, skipped, no hand-off) — exit 0 — and the exit 1 for
failed; the script's arms on the mock bench; on the dev phone (KPN), in
builds of this series: the daemon's idle at the no-target bring-up with the
dead pair removed and before the bus name for a pair the holds had written
off, the discovery-failed exit 1 and its restart, the start without the file, the pull-in by an imsd-ofonod restart
and the mask holding against it and a reboot, and imsd-ofonod starting at
once with imsd stopped and republishing when imsd's name appeared.
This commit is contained in:
Jorijn van der Graaf 2026-10-11 16:20:07 +02:00
commit 7a3dcc8951
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
7 changed files with 476 additions and 149 deletions

View file

@ -48,17 +48,6 @@ Both can be found in the packages tab:
https://forgejo.catcrafts.net/Catcrafts/-/packages/alpine/imsd
```
The apk's `imsd.service` is enabled by preset and starts at boot once
`/etc/imsd.env` exists (`touch /etc/imsd.env` is enough; the condition is checked when
the unit starts, so after creating the file run `systemctl start imsd`). It first brings the `ims` PDN up through ModemManager, then registers:
resuming an existing registration when it can, registering fresh otherwise,
and waiting out whatever the network tells it to wait out. Check on it with:
```sh
journalctl -u imsd -f
busctl --system call net.catcrafts.IMS1 /net/catcrafts/IMS1 net.catcrafts.IMS1 GetStatus
```
## Dialers
imsd itself is desktop-agnostic: everything a dialer needs is on the system
@ -91,8 +80,8 @@ Configuration is environment variables in `/etc/imsd.env`, none are required.
| Variable | Default | Meaning |
|---|---|---|
| `PCSCF` | *(the first of `PCSCF_DISCOVERED`)* | P-CSCF address (IPv6), exactly one — not a comma-separated list — in RFC 5952 §4 form (lowercase, leading zeros dropped, the longest zero run compressed, no brackets — what `ip` prints for a plain IPv6 address; an IPv4-mapped address only as hex, `::ffff:a00:1` — the dotted spelling RFC 5952 §5 and `ip` use for it is refused); upper case, leading zeros, an uncompressed run or brackets are refused at start with the spelling to use (in the packaged unit the bring-up script refuses brackets first, with the rule), a dotted-quad tail with the rule; before 1.0.0 an upper-case, zero-padded or uncompressed `PCSCF=` matched no SA in `ip xfrm`'s text and registered fresh at every restart — an upgraded phone with such a setting stops at start with the spelling to use. Set, it wins over `PCSCF_DISCOVERED`. It is where a fresh registration goes; a registration resumed from the kernel's SAs stays on the P-CSCF it was made to until its next fresh registration. With neither this nor `PCSCF_DISCOVERED`, a fresh registration fails with a clear error |
| `PCSCF_DISCOVERED` | *(from the PDN's PCO)* | the IPv6 P-CSCF list the network put in the ims PDN's PCO, comma-separated in the order the modem reports it; the first entry `CanonicalIp6` accepts is used when `PCSCF` is unset. The packaged service gets it via `/run/imsd.env` from `ims-pdn-up.sh`, which reads it from the modem with `imsd-pcscf` (at most eight; it routes the first; imsd routes the P-CSCF it uses — the fresh target, the resumed SA peer — itself as well); entries `CanonicalIp6` does not read — an IPv4 address, a name, an IPv6 literal with a zone or a dotted-quad tail (an IPv4-mapped entry in hex only) — are logged and ignored. In `/etc/imsd.env` it is a bench seam |
| `PCSCF` | *(the first of `PCSCF_DISCOVERED`)* | P-CSCF address (IPv6), exactly one — not a comma-separated list — in RFC 5952 §4 form (lowercase, leading zeros dropped, the longest zero run compressed, no brackets — what `ip` prints for a plain IPv6 address; an IPv4-mapped address only as hex, `::ffff:a00:1` — the dotted spelling RFC 5952 §5 and `ip` use for it is refused); upper case, leading zeros, an uncompressed run or brackets are refused at start with the spelling to use (in the packaged unit the bring-up script refuses brackets first, with the rule), a dotted-quad tail with the rule; before 1.0.0 an upper-case, zero-padded or uncompressed `PCSCF=` matched no SA in `ip xfrm`'s text and registered fresh at every restart — an upgraded phone with such a setting stops at start with the spelling to use. Set, it wins over `PCSCF_DISCOVERED`. It is where a fresh registration goes; a registration resumed from the kernel's SAs stays on the P-CSCF it was made to until its next fresh registration. With neither this nor `PCSCF_DISCOVERED` there is no fresh registration: a phone with nothing to resume idles (`idle: no P-CSCF …`), unless a resume failed with its pair still resumable, or the reader did not answer (`PCSCF_DISCOVERY=failed`): those exit non-zero for systemd's restart |
| `PCSCF_DISCOVERED` | *(from the PDN's PCO)* | the IPv6 P-CSCF list the network put in the ims PDN's PCO, comma-separated in the order the modem reports it; the first entry `CanonicalIp6` accepts is used when `PCSCF` is unset. The packaged service gets it via `/run/imsd.env` from `ims-pdn-up.sh`, which reads it from the modem with `imsd-pcscf` (at most eight; it routes the first; imsd routes the P-CSCF it uses — the fresh target, the resumed SA peer — itself as well); entries `CanonicalIp6` does not read — an IPv4 address, a name, an IPv6 literal with a zone or a dotted-quad tail (an IPv4-mapped entry in hex only) — are ignored (logged by the engine when the daemon goes on to register); `PCSCF_DISCOVERY=found|none|failed|skipped` beside it says how the list came to be — `failed` (the reader did not answer) makes a daemon with nothing to resume exit 1 for the restart instead of idling. In `/etc/imsd.env` it is a bench seam |
| `IMSD_PCSCF` | `/usr/libexec/imsd-pcscf` | the P-CSCF reader `ims-pdn-up.sh` runs — a bench seam (the mock bench's fake reader; a stub through a unit drop-in on the dev phone), from the environment |
| `PCSCF_PORT` | `5060` | unprotected SIP port for the initial REGISTER |
| `DEV` | `qmapmux0.0` | ims-PDN netdev; the packaged service passes the real one via `/run/imsd.env` |
@ -111,6 +100,7 @@ Configuration is environment variables in `/etc/imsd.env`, none are required.
| `DUMP_SIP` | `0` | `1` writes raw REGISTER-200/SUBSCRIBE-200 dumps, the last refused REGISTER or SUBSCRIBE final (`imsd-register-fail.raw`, `imsd-subscribe-fail.raw`), the last inbound INVITE (`imsd-invite-in.raw`) and the last outgoing INVITE (`imsd-invite-out.raw`), mode 0600, for debugging — they contain your IMSI/MSISDN and addresses. A refused REGISTER or SUBSCRIBE is logged with its status line and any Retry-After / Warning / Reason / Server headers |
| `DUMP_DIR`, `OUT_DIR` | state dir | where dumps / per-call media stats land |
| `IMSD_MEDIA` | *(auto)* | path to `imsd-media` (default: next to `imsd`, else `/usr/libexec/imsd-media`) |
| `IMSD_ENV` | `/etc/imsd.env` | the configuration file's path — a bench seam for manual runs of `ims-pdn-up.sh` (the packaged unit reads `/etc/imsd.env` through `EnvironmentFile=`). The file's existence changes nothing |
The protected client/server ports are fixed at 45061/45062
(`imsd::util::PortUc`/`PortUs`) — changing them in source also requires