With the netdev configured, the script runs imsd-pcscf against the
netdev's rmnet mux id (and the ims profile index when it knows one) and
hands the IPv6 addresses it prints to imsd as PCSCF_DISCOVERED= in
/run/imsd.env and gives the first — the daemon's fresh target — a host
route (the SA peer of a registration imsd resumes is routed by imsd itself,
as the configured address's arm already left it); IPv4 addresses and names
are logged and dropped (the PDN is IPv6-only), at most eight are handed
over, and nothing usable is logged with its cause and is not the
script's failure: imsd resumes a registration of its own when it can, else
says so and exits, and the restart runs discovery again. PCSCF= still wins and skips discovery; a list in it is refused.
The script does not read the variable from its environment: the unit
hands it the previous run's file, and a stale discovered value must not
pass for the admin's setting. The reader's path is the IMSD_PCSCF seam
the mock bench uses.
Tested: on the mock bench against this commit's script (the twelve 0.3.10
scenarios; this step's discovery arms S13–S17 and S19–S22, with S18 the
pre-existing non-IPv6 refusal; S55, the reader's rc-0 answer without an
address), and in builds of this series on the dev phone (KPN), whose PCO —
two IPv6 addresses, nothing else — reaches the plain found arm; the
configured skip, the bracketed refusal and, through the IMSD_PCSCF seam, the
reader's failure and its exit 2 ran on the phone too; the v4/FQDN line, the
eight-address cap, the IPv4-only and no-mux-id skips, the comma-list refusal
and the rc-0 arm on the mock only. On a tip build of this series (daemon
2ec1a02f62831edb; bench 40, 2026-10-13) a fresh registration reached a target
this script had not routed — the reader stubbed through the IMSD_PCSCF seam to
find none, the address in /etc/imsd.env as PCSCF_DISCOVERED= — over imsd's own
route, and a resume stood with the peer's route removed first.
The README had grown to 422 lines: the device and carrier table came
after 61 lines of architecture, and the configuration section after 237
lines of architecture, that table, build, run and packaging notes and
GNOME Calls detail.
AGENTS.md, a file for coding agents, takes that detail and adds to it: a
reference to the tree at this commit, written from the code and the
bench record. The README is now what a person needs: what imsd is and
its status, the verified device/carrier table, what it needs, how to
install and enable it, the two dialers, a status check for the installed
daemon, a `Not there yet` list, and where the rest is.
In-tree comments that sent readers to README sections which moved — the
cross-compiling flow (the CI workflow, build-package.sh,
APKBUILD.binary), GNOME Calls' known gaps (imsd-ofono-restart-calls.sh),
the D-Bus ABI (main.cpp) and the Configuration section (imsd.service) —
point at AGENTS.md's sections now.
An upgrade left the old imsd and imsd-ofonod running until they were
next restarted, while a changed bus policy is live the moment apk writes
it: dbus-broker-launch reloads on its own. A policy that relies on a
check inside the daemons would face the old daemons in between.
Mark imsd.service and imsd-ofonod.service needs-restart in post-upgrade
scripts; the systemd trigger restarts them once the transaction is done
(enqueue-marked). A call in progress ends. The same scripts run as
post-install: `apk del` removes a package's files and leaves its daemon
running, so a later `apk add` of the package is this upgrade for that
daemon — and apk runs the post-install script, not post-upgrade, for a
package it adds. GNOME Calls 50.0 dials nothing after imsd-ofonod
restarts under it until it restarts too, so imsd-ofonod's ExecStartPost
restarts the Calls daemon of each user running one — not at the unit's
first start since boot, when a Calls that runs already is left alone.
A daemon older than 0.3.10 kept its bring-up throttle wait in memory
only, so a restart lost it. imsd's post-upgrade records that daemon's
invocation, and imsd-handover.sh, an ExecStartPre after ims-pdn-up.sh,
reads the invocation's journal and, while the fresh-SA throttle's wait
it finds there still runs (`spi-s=0`; the old daemon's other deferrals
are not handed over), writes what is left of it to the holds file when
this start's LOCAL is the address the wait was earned on, or when that
address cannot be read; when LOCAL is another address, only the verdict
that the old registration is gone.
Tested on the dev phone (KPN) in builds of this series; not every path
has run there.
imsd took the first global IPv6 on the ims netdev as its address, and
the kernel lists the temporary (privacy) SLAAC address first: `temporary
dynamic`, preferred for a day and valid for two by its lifetimes (read
2026-09-24). Contact, Via and the four SAs were bound to it, so once the
kernel removes it the registration has lost its address. The address
ModemManager assigned — the one the script installs — sat third on the
list. Seen on the dev phone; a field unit's journal shows the split too
— imsd registered on an address other than the one the script installed
(ims ledger F18).
Hand the assigned address to the daemon as LOCAL through /run/imsd.env,
next to DEV. For a manual run without it, prefer a global address that
is neither `temporary` nor `dynamic`, then any non-temporary one.
A restart after the upgrade finds the registration's SAs on the old
address. It resumes there while that address is still on the link, and
moves to the PDN's address in place: with its next fresh registration,
or when the old address leaves the link first. When the address it is
registered on leaves the netdev and the PDN's own address is not there
to move to, a registered imsd exits at its next recovery step, non-zero,
for a systemd restart, which runs the PDN bring-up again.
Tested on the dev phone (KPN) in builds of this series; not every path
has run there.
Three hardenings, each of which cost a field-debugging round-trip:
- Every mmcli failure now lands in the journal verbatim (create-bearer
and connect stderr were swallowed, making 'connect attempt N failed'
undecodable — no-service and interface-in-use looked identical).
- Connect attempts are gated on network registration (up to
IMS_REG_TIMEOUT, default 300 s): the 10x10 s window is shorter than
some carriers' post-boot attach (~2 min measured), so every attempt
could burn out before the network was even attached. The settled
modem/packet-service state is logged either way.
- Bearer ip-type is configurable via IMS_IP_TYPE in /etc/imsd.env
(default ipv6); the bearer find/reuse now matches ip-type too, so a
config change cannot silently reuse a stale bearer of the old type.
TimeoutStartSec grows 600->900 to cover the registration gate.
Tested on the FP6 dev phone (KPN): clean-state bring-up in 4 s;
gate + timeout path with modem disabled; per-attempt error lines on a
real contested-PDN failure (second ims PDN while one is connected);
idempotent reuse + REGISTERED (fresh) via systemd.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Userspace VoLTE/IMS daemon for mainline Linux phones (developed on the
Fairphone 6): a GLib-free core (SIP, SDP, USIM AKA, IPsec SA setup, RTP
media, call engine) behind a GDBus control daemon, a standalone AMR-WB
media leg, and a Plasma Dialer backend. C++26 modules built with Crafter
Build; the tree is clean under the project's house-style linter
(crafter-build lint) across all three build products.
Assisted-by: Claude:claude-fable-5
Signed-off-by: Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>