imsd compares addresses as text against `ip xfrm` output, which prints
RFC 5952 form, while PCSCF= and LOCAL= arrive in whatever spelling the
user typed. CanonicalIp6 produces the §4 form from an IPv6 literal
(brackets accepted; a zone, a port, a dotted-quad tail, a name or an IPv4
address are refused; an IPv4-mapped address spelled in hex comes back in
hex, where inet_ntop writes it dotted), CanonicalAddr applies it to IPv6 only, and
ParseAddressList turns a comma-separated list into canonical,
deduplicated IPv6 literals in order, keeping the entries CanonicalIp6
accepts and naming every entry it refuses — an IPv4 address, a name, an
IPv6 literal with a zone or a dotted-quad tail — to a caller who asks for
them (duplicates and blanks are dropped
silently). Pure std, so the partitions stay free of inet_pton; nothing calls them yet.
Tested: crafter-build test, 7 passed.
0.3.12's rule (§11) said the major number stays 0 and that what 1.0 would
mark is not decided. It is now: the major number moves when a release
changes what an installed phone does without being asked — not with an
ABI change (the ABI is frozen) and not with the size of a release. The
rule comes first so the commits that follow can name the release they are
part of.
0.3.12 is a fix release and stays in 0.3. The patch number counts
releases, and the minor number last moved with emergency calling
(f2a4048, 0.3.0). Nothing in the repository said so; §11 now does, so a
release's number is not re-decided each time.
A warm resume rebuilt its Security-Verify from the kernel's SA: one
mechanism with hmac-sha-1-96 and aes-cbc written in, where RFC 3329
§2.3.1 has the client mirror the server's whole list. The fresh
registration kept the 401's list in memory, so a registration this
process made fresh echoed it; after a restart, the resumed
registration's re-REGISTER, its keepalives, its reg-event SUBSCRIBE and
its INVITEs carried the rebuilt entry until the next fresh registration.
KPN's one-entry answer to the default offer is the rebuilt entry byte
for byte; against a six-entry list (O2's) the mirror is broken, which
the RFC has the server answer with a 494, and a refused resume
registers fresh.
The fresh registration now stores the joined Security-Server value in the
state file (security_server, next to the challenge answer) and the resume
takes it from there, logging the mechanism count, when the entry
carrying its SPIs names the SA pair in the kernel: a file an attempt the
process did not live to record left behind (its SAs installed, its 200
not written) names another. A file from before this change holds none.
In both cases the resume falls back to the rebuilt entry, says which,
and the registration's next state write stores that entry, which is what
it echoes; the choice is Imsd:Ipsec's, pinned in tests/Ipsec with O2's
recorded list and KPN's. A value with a quote, a backslash or a newline in it is
written empty, which the resume treats as absent; RFC 3329's token
syntax produces none of these, a quoted-string extension parameter
could carry the first two.
Tested on the dev phone (KPN) with the build of this tree: a resume from
a state file the 0.3.11 daemon wrote logs the fallback and succeeds, so
do one from a file whose list names another SA pair and one whose list
is empty; two keepalives at a 120-s interval; a fresh registration
stores KPN's list byte-for-byte as the journal shows it received; a
restart resumes with the list from the file; an outgoing call.
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.
RegRecovery retried a refused refresh once over the same registration
before escalating, on the reading that one 503 may be a hiccup. A
refusal whose Retry-After is at least a second is the network's
considered answer: on O2 UK (2026-10-08, imsd 0.3.10) the keepalive was
refused 503 with Retry-After 414 and the refresh after that wait 503
with Retry-After 772, so 0.3.10 scheduled the fresh registration no
earlier than 414 + 772 s after the keepalive's refusal. Refusals
without such a Retry-After (none, 0, unparsable) keep the two-refusal
rule, so KPN's bare 403 is unchanged.
Tested with the Engine suite, whose checks of a refusal carrying a
Retry-After now expect the fresh step.
With the modem's radio off (airplane mode) the bring-up waited five
minutes for registration and then tried to connect the ims bearer
anyway. That attempt fails with no-service and leaves ModemManager's
modem state `enabled` while the modem stays in low power; switching the
radio back on then no longer brings the modem up, and the phone has no
mobile service until the modem is disabled and enabled again (dev phone
2026-10-06: not registered 150 s after the radio came on, against 5 s
without the attempt).
0.3.9's bring-up made the same attempts, but it only ran at boot or at
a manual restart. Since this series a registered imsd exits when its
address leaves the netdev and systemd runs the bring-up again, and an
upgrade restarts it: the bring-up now also runs during a radio-off
period.
A modem still disabled after the registration wait, or disabled when a
connect attempt is due, now makes the script exit non-zero without
touching the modem; systemd retries after RestartSec.
Tested on the dev phone (KPN) with this script: a registered imsd, the
radio switched off — imsd exited at its next refresh, the bring-up gave
up after the wait with the modem still disabled, and when the radio came
back the modem registered in 5 s and imsd registered by itself two
minutes later; the same with an upgrade's restart during the radio-off
period. The twelve mock scenarios of the bring-up are unchanged.
lint-rules.h was a project-local copy of the Catcrafts house-style
rules. The rules have their own repository now, and project.cpp imports
them from it (`#pragma crafter dependency`), as Crafter.Build's own
project.cpp does. The dependency is pinned to a commit; crafter-build
clones it whenever it loads project.cpp, so every build fetches it,
CI's included.
The tree does not pass the shared rules yet: `crafter-build lint`
reports 72 findings where the copy reported 20. The shared `naming`
wants camelCase constants and statics (44), `const-local` is new (10),
and `single-declaration` reports two files the copy did not. They are
left for a later change; CI does not run lint.
Tested: build-package.sh run end to end on this commit in a privileged
alpine:edge container (podman) on the workstation, and `crafter-build
lint` on a built copy of this commit.
imsd had two build descriptions to keep in sync: project.cpp for
crafter-build, and a Makefile mirroring it for distros that build from
source, used by packaging/APKBUILD. CI has not used that route since
2026-09-02 — its apks are a crafter-build binary wrapped by
APKBUILD.binary — and the recipe had not followed the releases: its
pkgver was still 0.3.0.
crafter-build is now the only build. AGENTS.md loses the make route
(§4.2) and describes one recipe (§9.1).
Tested: build-package.sh run end to end on this commit in a privileged
alpine:edge container (podman) on the workstation.
The README had been cut down to an introduction and the configuration
variables moved to AGENTS.md, a file for coding agents. A user who has to
set a variable should not need that file.
The README is the user documentation again: installing from the
Catcrafts Alpine repository or the apks, the configuration tables for
imsd and imsd-media, the dialers and the D-Bus ABI. Its build
instructions go: building is in AGENTS.md (§4), with the tests, the
packaging and how the daemon works. AGENTS.md §10 points to the README
for the tables and keeps the two imsd-media pairs that are not
configuration.
Crafter.Build's CI publishes a Flatpak and nothing else since
2026-10-02: the rolling `latest` release's
`crafter-build-linux-x86_64-musl-v2.tar.gz` that build-package.sh
fetched is gone (404 since 2026-10-03), so a push of a release would
fail at `fetch crafter-build` and publish nothing.
Install the Flatpak (`net.catcrafts.CrafterBuild//stable` from
flatpak.catcrafts.net) in the job container and run it as crafter-build.
bubblewrap inside the container needs a privileged runner, so the job
runs on the `flatpak` label. The sysroot stays Alpine edge and gains
llvm-runtimes, the package with the std module source of its own libc++
(libc++-dev ships none). make-sysroot.sh adds its list with --upgrade:
run over a sysroot from before this change (libc++ 22.1.8, no std module
source) a plain add installed llvm-runtimes 23.1.2 alone, and its
std.cppm did not compile against the 22.1.8 headers (`'text_encoding'
file not found`); with --upgrade libc++, libc++-dev and llvm-runtimes
came out at 23.1.2 and it compiled.
AGENTS.md §4.1, §4.3, §8.3 and §9.4 describe the route as it runs, and
the README's Building section carries the install lines. Three comments
in the files this touches said what is not so and are corrected:
package.yml had fp6-img install the package by a pinned version and
sha256 (it takes the newest in the registry), build-package.sh had
fp6-img's image build run `make check` on the aarch64 binaries (it
builds no imsd: no suite runs on them), and make-sysroot.sh's usage line
lacked the --march without which the cross build fails on an x86 host.
Tested: build-package.sh run end to end on this commit in a privileged
alpine:edge container (podman) on the workstation.
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.