2026-07-22 22:53:28 +02:00
<!-- SPDX-License-Identifier: GPL-3.0-only -->
<!-- SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® -->
# imsd
Userspace IMS/VoLTE daemon for mainline Linux phones.
2026-10-06 16:45:44 +02:00
Most Linux phones drive the modem's internal IMS engine.
imsd implements the whole IMS client in userspace instead:
SIP registration with USIM AKA authentication over kernel
2026-07-22 22:53:28 +02:00
IPsec (ESP), call signalling, and the AMR-WB RTP media plane wired into
2026-10-06 16:45:44 +02:00
PipeWire so a stock mobile dialer like Plasma Dialer or GNOME Calls under Phosh
can place and receive real VoLTE calls on a commercial network.
2026-07-22 22:53:28 +02:00
## Status
README, AGENTS.md: a short README for people, the detail in AGENTS.md
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.
2026-10-03 19:36:52 +02:00
Call-capable, both directions: outgoing calls place, answer, carry full-duplex
2026-10-06 16:45:44 +02:00
AMR-WB audio and tear down. Incoming calls ring, answer and carry media the
README, AGENTS.md: a short README for people, the detail in AGENTS.md
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.
2026-10-03 19:36:52 +02:00
same way, driven end-to-end from a stock dialer over a frozen D-Bus ABI.
Not there yet: DTMF (`SendDtmf` is a stub), hold/multiparty/call waiting,
ims-pdn-up: discover the P-CSCF list once the PDN is up and hand it to imsd
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.
2026-10-11 16:16:46 +02:00
USSD.
ofonod: GNOME Calls backend over org.ofono for Phosh and GNOME Mobile
imsd-ofonod presents imsd on the system bus as an oFono modem — Manager at
/, Modem + VoiceCallManager at /imsd, a VoiceCall object per call — which
is what GNOME Calls' bundled ofono provider drives; Calls, Phosh and GNOME
Shell run unmodified. org.gnome.Calls is not a seam a third party can
provide (Calls exports it, nothing feeds it), and Calls' provider plugins
link private headers, so org.ofono is the only D-Bus contract available.
Pure GDBus translation of net.catcrafts.IMS1, no core, like imsd-dialerd.
Packaged as the opt-in imsd-ofono subpackage: the daemon, its unit
(Conflicts=ofono.service), the system-bus policy and a gschema override
that points Calls at the ofono provider instead of mm (the ModemManager
origin would offer the modem's CS voice path, which carries no audio on
these phones). The policy grants root send_destination=org.ofono: under
dbus-broker a broadcast is checked against the sender's send policy for
every name the receiver owns, so without it imsd's call signals never
reach the daemon.
Verified on a Fairphone 6 with GNOME Calls 50.0: outgoing and incoming
calls with audio both ways, answered and hung up through
org.gnome.Calls.Call. Known Calls-side gap documented in the README: after
the VoiceCallManager interface is withdrawn and re-added, Calls keeps the
old origin's CallAdded handler and eventually crashes.
2026-09-20 16:07:46 +02:00
README, AGENTS.md: a short README for people, the detail in AGENTS.md
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.
2026-10-03 19:36:52 +02:00
Verified by community members to be fully working:
2026-07-22 22:53:28 +02:00
README, AGENTS.md: a short README for people, the detail in AGENTS.md
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.
2026-10-03 19:36:52 +02:00
| Device | OS | Carrier |
2026-08-12 14:25:09 +02:00
|---|---|---|
| The Fairphone (Gen. 6) | postmarketOS, Linux 7.1.2 | KPN NL |
2026-08-12 15:44:52 +02:00
| The Fairphone (Gen. 6) | postmarketOS, Linux 7.1.2 | Telekom Deutschland GER |
2026-08-13 17:33:58 +02:00
| The Fairphone (Gen. 6) | postmarketOS, Linux 7.1.2 | Phonero |
| The Fairphone (Gen. 6) | postmarketOS, Linux 7.1.2 | Telia Norge |
2026-10-16 14:53:08 +00:00
| The Fairphone (Gen. 6) | postmarketOS, Linux 7.2.0 | o2-uk |
2026-07-22 22:53:28 +02:00
README, AGENTS.md: a short README for people, the detail in AGENTS.md
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.
2026-10-03 19:36:52 +02:00
## What it needs
2026-07-22 22:53:28 +02:00
README, AGENTS.md: a short README for people, the detail in AGENTS.md
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.
2026-10-03 19:36:52 +02:00
imsd talks to the modem through ModemManager plus `qmicli` over QRTR, installs
its security associations with `ip xfrm` and plays audio through PipeWire, so
in practice: a Qualcomm modem on a mainline kernel with working
ims-pdn-up: discover the P-CSCF list once the PDN is up and hand it to imsd
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.
2026-10-11 16:16:46 +02:00
WWAN/ModemManager integration, kernel ESP, PipeWire, systemd and polkit.
2026-07-22 22:53:28 +02:00
README, AGENTS.md: a short README for people, the detail in AGENTS.md
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.
2026-10-03 19:36:52 +02:00
## Install and configure
2026-07-22 22:53:28 +02:00
2026-10-06 16:45:44 +02:00
For automatic updates add the Catcrafts Alpine repository. Note this does also pull in other packages.
Otherwise just download the latest `imsd` and `imsd-systemd` APKs and install them.
Both can be found in the packages tab:
```
https://forgejo.catcrafts.net/Catcrafts/-/packages/alpine/imsd
```
2026-08-07 21:05:33 +02:00
README, AGENTS.md: a short README for people, the detail in AGENTS.md
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.
2026-10-03 19:36:52 +02:00
## Dialers
2026-07-22 22:53:28 +02:00
README, AGENTS.md: a short README for people, the detail in AGENTS.md
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.
2026-10-03 19:36:52 +02:00
imsd itself is desktop-agnostic: everything a dialer needs is on the system
bus as `net.catcrafts.IMS1` . A small translation daemon per desktop speaks
the backend protocol its dialer already consumes, so dialer, shell and the
call-history/contacts stack run unmodified.
ofonod: GNOME Calls backend over org.ofono for Phosh and GNOME Mobile
imsd-ofonod presents imsd on the system bus as an oFono modem — Manager at
/, Modem + VoiceCallManager at /imsd, a VoiceCall object per call — which
is what GNOME Calls' bundled ofono provider drives; Calls, Phosh and GNOME
Shell run unmodified. org.gnome.Calls is not a seam a third party can
provide (Calls exports it, nothing feeds it), and Calls' provider plugins
link private headers, so org.ofono is the only D-Bus contract available.
Pure GDBus translation of net.catcrafts.IMS1, no core, like imsd-dialerd.
Packaged as the opt-in imsd-ofono subpackage: the daemon, its unit
(Conflicts=ofono.service), the system-bus policy and a gschema override
that points Calls at the ofono provider instead of mm (the ModemManager
origin would offer the modem's CS voice path, which carries no audio on
these phones). The policy grants root send_destination=org.ofono: under
dbus-broker a broadcast is checked against the sender's send policy for
every name the receiver owns, so without it imsd's call signals never
reach the daemon.
Verified on a Fairphone 6 with GNOME Calls 50.0: outgoing and incoming
calls with audio both ways, answered and hung up through
org.gnome.Calls.Call. Known Calls-side gap documented in the README: after
the VoiceCallManager interface is withdrawn and re-added, Calls keeps the
old origin's CallAdded handler and eventually crashes.
2026-09-20 16:07:46 +02:00
README, AGENTS.md: a short README for people, the detail in AGENTS.md
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.
2026-10-03 19:36:52 +02:00
| Desktop | Daemon | Package | Owns | Consumed by |
|---|---|---|---|---|
| Plasma Mobile | `imsd-dialerd` | `imsd` | `org.kde.telephony.*` (session bus) | plasma-dialer + kde-telephony-daemon, replacing `modem-daemon` |
| Phosh / GNOME Mobile | `imsd-ofonod` | `imsd-ofono` | `org.ofono` (system bus) | GNOME Calls' `ofono` provider plugin |
ofonod: GNOME Calls backend over org.ofono for Phosh and GNOME Mobile
imsd-ofonod presents imsd on the system bus as an oFono modem — Manager at
/, Modem + VoiceCallManager at /imsd, a VoiceCall object per call — which
is what GNOME Calls' bundled ofono provider drives; Calls, Phosh and GNOME
Shell run unmodified. org.gnome.Calls is not a seam a third party can
provide (Calls exports it, nothing feeds it), and Calls' provider plugins
link private headers, so org.ofono is the only D-Bus contract available.
Pure GDBus translation of net.catcrafts.IMS1, no core, like imsd-dialerd.
Packaged as the opt-in imsd-ofono subpackage: the daemon, its unit
(Conflicts=ofono.service), the system-bus policy and a gschema override
that points Calls at the ofono provider instead of mm (the ModemManager
origin would offer the modem's CS voice path, which carries no audio on
these phones). The policy grants root send_destination=org.ofono: under
dbus-broker a broadcast is checked against the sender's send policy for
every name the receiver owns, so without it imsd's call signals never
reach the daemon.
Verified on a Fairphone 6 with GNOME Calls 50.0: outgoing and incoming
calls with audio both ways, answered and hung up through
org.gnome.Calls.Call. Known Calls-side gap documented in the README: after
the VoiceCallManager interface is withdrawn and re-added, Calls keeps the
old origin's CallAdded handler and eventually crashes.
2026-09-20 16:07:46 +02:00
README, AGENTS.md: a short README for people, the detail in AGENTS.md
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.
2026-10-03 19:36:52 +02:00
Plasma Mobile needs nothing beyond the `imsd` package: `imsd-dialerd` is
autostarted in the session in place of plasma-dialer's `modem-daemon` , whose
own autostart must be off — the package hides it for accounts created after
the install (`/etc/skel` ); an existing account hides it itself with a
`Hidden=true` copy of `org.kde.modem.daemon.desktop` in `~/.config/autostart` .
ofonod: GNOME Calls backend over org.ofono for Phosh and GNOME Mobile
imsd-ofonod presents imsd on the system bus as an oFono modem — Manager at
/, Modem + VoiceCallManager at /imsd, a VoiceCall object per call — which
is what GNOME Calls' bundled ofono provider drives; Calls, Phosh and GNOME
Shell run unmodified. org.gnome.Calls is not a seam a third party can
provide (Calls exports it, nothing feeds it), and Calls' provider plugins
link private headers, so org.ofono is the only D-Bus contract available.
Pure GDBus translation of net.catcrafts.IMS1, no core, like imsd-dialerd.
Packaged as the opt-in imsd-ofono subpackage: the daemon, its unit
(Conflicts=ofono.service), the system-bus policy and a gschema override
that points Calls at the ofono provider instead of mm (the ModemManager
origin would offer the modem's CS voice path, which carries no audio on
these phones). The policy grants root send_destination=org.ofono: under
dbus-broker a broadcast is checked against the sender's send policy for
every name the receiver owns, so without it imsd's call signals never
reach the daemon.
Verified on a Fairphone 6 with GNOME Calls 50.0: outgoing and incoming
calls with audio both ways, answered and hung up through
org.gnome.Calls.Call. Known Calls-side gap documented in the README: after
the VoiceCallManager interface is withdrawn and re-added, Calls keeps the
old origin's CallAdded handler and eventually crashes.
2026-09-20 16:07:46 +02:00
README, AGENTS.md: a short README for people, the detail in AGENTS.md
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.
2026-10-03 19:36:52 +02:00
For Phosh and GNOME Mobile, `apk add imsd-ofono` enables `imsd-ofonod` and
points GNOME Calls at its `ofono` provider (a gschema override; the per-user
equivalent is `gsettings set org.gnome.Calls autoload-plugins "['ofono']"` ).
2026-10-06 16:45:44 +02:00
Do not run it alongside a real `ofonod` .
README, AGENTS.md: a short README for people, the detail in AGENTS.md
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.
2026-10-03 19:36:52 +02:00
Call control is polkit's `net.catcrafts.IMS1.Voice` : the active local
session and root may place, answer and end calls; an ssh login may not.
ofonod: GNOME Calls backend over org.ofono for Phosh and GNOME Mobile
imsd-ofonod presents imsd on the system bus as an oFono modem — Manager at
/, Modem + VoiceCallManager at /imsd, a VoiceCall object per call — which
is what GNOME Calls' bundled ofono provider drives; Calls, Phosh and GNOME
Shell run unmodified. org.gnome.Calls is not a seam a third party can
provide (Calls exports it, nothing feeds it), and Calls' provider plugins
link private headers, so org.ofono is the only D-Bus contract available.
Pure GDBus translation of net.catcrafts.IMS1, no core, like imsd-dialerd.
Packaged as the opt-in imsd-ofono subpackage: the daemon, its unit
(Conflicts=ofono.service), the system-bus policy and a gschema override
that points Calls at the ofono provider instead of mm (the ModemManager
origin would offer the modem's CS voice path, which carries no audio on
these phones). The policy grants root send_destination=org.ofono: under
dbus-broker a broadcast is checked against the sender's send policy for
every name the receiver owns, so without it imsd's call signals never
reach the daemon.
Verified on a Fairphone 6 with GNOME Calls 50.0: outgoing and incoming
calls with audio both ways, answered and hung up through
org.gnome.Calls.Call. Known Calls-side gap documented in the README: after
the VoiceCallManager interface is withdrawn and re-added, Calls keeps the
old origin's CallAdded handler and eventually crashes.
2026-09-20 16:07:46 +02:00
2026-10-06 16:45:44 +02:00
## Full config:
2026-07-22 22:53:28 +02:00
ims-pdn-up: discover the P-CSCF list once the PDN is up and hand it to imsd
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.
2026-10-11 16:16:46 +02:00
Configuration is environment variables in `/etc/imsd.env` , none are required.
2026-10-06 16:45:44 +02:00
| Variable | Default | Meaning |
|---|---|---|
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.
2026-10-11 16:20:07 +02:00
| `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 |
ims-pdn-up: discover the P-CSCF list once the PDN is up and hand it to imsd
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.
2026-10-11 16:16:46 +02:00
| `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 |
2026-10-06 16:45:44 +02:00
| `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` |
ipsec, imsd, ims-pdn-up: the SA reader and the holds key compare in canonical form; LOCAL is canonicalised, PCSCF= accepted in RFC 5952 §4 form only
ParseExistingSa and SaLocals compared PCSCF= and LOCAL= as text against
`ip xfrm` output, which prints RFC 5952 form, so an upper-case or
uncompressed PCSCF= found no SAs and the restart registered fresh — on
KPN into the live-SA refusal; the holds file's pdn key compared the same
way. Both sides of each of those compares now go through
imsd::util::CanonicalAddr, LOCAL is canonicalised once on the way in, and PCSCF= is accepted in
that one spelling only — upper case, leading zeros or an uncompressed run
are refused at bring-up with the spelling to use, a dotted-quad tail (the
form ip prints for an IPv4-mapped address, which CanonicalIp6 does not read)
with the rule alone, a bracketed one by the bring-up script, which routes
the value as typed, with the rule (no brackets) — and a comma-separated
PCSCF= (a list) with its own message instead of reaching the socket layer: one spelling,
instead of a leniency both consumers of the setting would have to share.
Tested: crafter-build test, 7 passed; the upper-case PCSCF= refused with the spelling to use, a bracketed one
refused by the script, and the spelling they name resuming, in builds of
this series on the dev phone (KPN).
2026-10-11 15:55:19 +02:00
| `LOCAL` | * (auto) * | UE address (compared in RFC 5952 form). The packaged service gets the PDN address ModemManager assigned, via `/run/imsd.env` from `ims-pdn-up.sh` ; a manual run prefers a global IPv6 on `DEV` that is neither temporary nor `dynamic` (RA-derived), then any non-temporary one, then the first (the kernel's temporary SLAAC address is listed first, and a registration bound to it is lost when the kernel retires it) |
2026-10-06 16:45:44 +02:00
| `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 |
ipsec, engine, imsd: the state file records the P-CSCF and a resume follows the kernel's SA peer
A resume compared PCSCF= against the kernel's SAs and, when they did not
match, registered fresh without a word — a second initial REGISTER from an
address whose registration is live; on KPN a fresh pair to the same P-CSCF
meets the 21-minute refusal that costs the live registration (§13), what
another P-CSCF does with it is unmeasured. The registration was made to
whatever P-CSCF the SAs name (the SA setup takes imsd's previous pair
out first), so that is where a resume belongs: SaPeer reads
it from the dir-out transport policies — the one address a reqid-1 and a
reqid-2 policy both name, each with imsd's fixed protected port as its
selector's sport (reqid and mode alone are anyone's): the state text cannot tell the UE from the
P-CSCF, a lone policy is not ours and a VPN's beside the pair changes
nothing — TryResume resumes there, routes it, and says so when PCSCF=
differs; the next fresh registration moves to PCSCF= and moves back when
it fails before the SA setup and the old registration still stands (its
address still on the link), as it already did between addresses, and
its fallback Route is the P-CSCF it registers at, set before the 200's
Service-Route is looked for — an empty Service-Route value counts as
none — so a move never keeps the previous node's route. A file left by an attempt that got through the SA setup and did not live
to record — to this or another P-CSCF — is recognised by its SA pair: the
file's spi_uc/spi_us (a 0.3.x file carries them too) against the kernel's
(FileNamesAnotherPair, Imsd:Ipsec, pinned in tests/Ipsec), instead of
resumed with the wrong dialog; the state file gains pcscf, named in that
line. Both fresh paths — the bring-up's and recovery's — now set
their target themselves: with no PCSCF=, a bring-up that did not resume
fails with its own message, and since no fresh one follows: a resumable
pair whose attempt failed stays for the restart to resume, a pair nothing
can resume — one the engine's rule wrote off during the attempt among them,
a refresh refused with a 403 that carries a Retry-After or a binding a NOTIFY
terminated under the re-REGISTER wait, decided once after both catch arms by
its verdict (Reconnectable(), and with no fresh target Fresh(): a 401, a
refusal with a Retry-After, the second refusal — with no fresh target
every refusal reaches the reducer and its count travels in the holds, so
the restart's refusal is the second, while with a target only a
Retry-After does, as before — a refusal without one is not counted and the
fresh registration follows at once; without the count a refused no-target resume would re-REGISTER on the
same Call-ID at every RestartSec, the F16 loop) — is removed so the next
bring-up finds nothing — and a recovery step that needs one restarts the daemon; the "PCSCF not set" check at the top of BringUp is gone, so a
registration can be resumed with no setting at all. Three journal lines of
the fresh-target path change with it: `resume connect failed; falling back
to fresh` and `state file transport differs from SIP_TRANSPORT; fresh` end
in `the next registration is a fresh one`, and a resume the engine writes
off (a 403 with a Retry-After, a binding a NOTIFY terminated under the wait)
logs `resume failed: … — written off by the network: the next registration
is a fresh one` (§7.8). `no reg state file; resume falls back to fresh`
keeps its text with a fresh target and ends in `nothing can resume it: the
pair is removed` without one; the new `the state file names another SA pair
(0x…/0x…[ to X]) than the kernel's (0x…/0x… to Y): another attempt's; …`
line takes the same two tails.
Tested: crafter-build test, 7 passed (tests/Engine: the refusal count's travel
through the holds); the resume on the SA peer with
the fresh target absent in builds of this series on the dev
phone (KPN), and the no-target bring-up there with a resume that failed on
a transport mismatch (the pair kept, the restart resumed) and with the
state file removed (the pair removed, the next bring-up found nothing); the
two dead-resume lines' no-target tail — a missing file, a file with another
pair's SPIs — and the pair's removal on the tip's host build (bench 23f: a
fake modem on PATH, a real xfrm in a namespace; a file with the pair's SPIs
but another P-CSCF's address passes the check and the resume runs — `RESUME
via existing SA`, the connect then failing in the namespace); on the dev
phone (bench 41, the tip's build) a resume on a file without its pcscf key,
the live pair's SPIs.
2026-10-11 15:58:49 +02:00
| `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), where that schedule stands whether the old registration is gone (`old_gone` ) and the refusal count (`refusals` : a resume refused without a Retry-After at a start with no fresh target is one, the restart's the second), kept across a restart of imsd for the PDN 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). `imsd-handover.sh` honours it too |
| `RESUME` | `auto` | `0` forces a fresh registration (ignores a warm SA) where there is a fresh target; with none the untried pair is removed and the bring-up ends as AGENTS §7.2 (the daemon's own bring-up) step 1 says |
2026-10-06 16:45:44 +02:00
| `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 |
| `SIP_TRANSPORT` | `auto` | transport of the protected leg (the second REGISTER and everything after it): `tcp` , `udp` , or `auto` = TCP, falling back to a fresh registration over UDP when the P-CSCF leaves two TCP connects unanswered — but only on a phone that has never registered over TCP: once the state file records a TCP registration, a silent connect is treated as an outage, not a policy (delete the file or set `udp` to override, e.g. after a SIM change). The challenge (first REGISTER) is always UDP. Over UDP a REGISTER or INVITE larger than ~1.2 KB leaves as IPv6 fragments at the ims PDN's 1280-byte MTU (the journal says so once per flow); there are no SIP retransmission timers over UDP yet, so a lost datagram costs a timeout. The SAs, the listener and the firewall rule cover both transports |
| `EMERGENCY_NUMBERS` | * (empty) * | comma-separated additions to the builtin 112/911 emergency numbers (e.g. a private test core's short code). SIM `EF_ECC` is not read yet. `imsd-ofonod` reads it too, for the oFono `EmergencyNumbers` property |
| `RTP_PORT` | `50004` | local RTP port advertised in SDP |
| `PRECOND` | `0` | `1` offers SDP QoS preconditions |
| `CODECS` | * (empty — defaults) * | comma-separated codec preference list over `AMR-WB` , `AMR` (or `AMR-NB` ), `PCMA` , `PCMU` : restricts and orders both the codecs offered on an outgoing call and those accepted from an inbound offer (default: offer AMR-WB + AMR, accept all four in that order). A bench knob — a network whose gateway transcodes every caller up to AMR-WB otherwise never lets the narrowband path run |
| `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` ) |
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.
2026-10-11 16:20:07 +02:00
| `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 |
2026-10-06 16:45:44 +02:00
The protected client/server ports are fixed at 45061/45062
(`imsd::util::PortUc` /`PortUs` ) — changing them in source also requires
updating the firewall rules `ims-pdn-up.sh` installs.
### imsd-media (per-call, set through imsd's environment)
| Variable | Default | Meaning |
|---|---|---|
| `MIC` , `PLAY` | `1` , `1` | uplink mic capture / downlink playout via PipeWire, as imsd passes them (`imsd-media` started by hand defaults both to off) |
| `AUDIO_USER` | * (auto) * | account whose PipeWire session carries call audio; imsd sets it per call from logind's seat0 session. Set in imsd's own environment it pins an account instead |
| `GAIN` , `PLAY_GAIN` | `10` , `1.0` | uplink / downlink gain, as imsd passes them (`imsd-media` started by hand defaults `GAIN` to `1.0` ) |
| `AMR_MODE` | `2` | AMR encoder mode of whichever AMR codec is active, as imsd passes it (12.65 kbit/s for AMR-WB; `imsd-media` started by hand defaults to 2 for AMR-WB and 7 for AMR) |
| `DTX` | `0` | encoder discontinuous transmission |
| `MEDIA_TIMEOUT` | `6.0` | seconds of downlink silence before exit 3 (far-end-hangup signal) |
| `RTP_DUMP` | `0` | `1` captures raw downlink RTP next to the stats file |
## D-Bus ABI
`net.catcrafts.IMS1` at `/net/catcrafts/IMS1` . The ABI is frozen.
| Member | Signature |
|---|---|
| `Dial(number)` | `s → s` (callUni) |
| `HangUp(callUni)` | `s` |
| `Accept(callUni)` | `s` |
| `SendDtmf(callUni, tones)` | `ss` |
| `GetCalls()` | `→ aa{sv}` |
| `GetStatus()` | `→ a{sv}` |
| `CallAdded` | `s a{sv}` |
| `CallStateChanged` | `s s s` (callUni, state, reason) |
| `CallDeleted` | `s` |
| `RegistrationChanged` | `b` |
Both `imsd-dialerd` and `imsd-ofonod` are users of this ABI.
A new desktop needs a new translation daemon, not changes here.
ofonod: GNOME Calls backend over org.ofono for Phosh and GNOME Mobile
imsd-ofonod presents imsd on the system bus as an oFono modem — Manager at
/, Modem + VoiceCallManager at /imsd, a VoiceCall object per call — which
is what GNOME Calls' bundled ofono provider drives; Calls, Phosh and GNOME
Shell run unmodified. org.gnome.Calls is not a seam a third party can
provide (Calls exports it, nothing feeds it), and Calls' provider plugins
link private headers, so org.ofono is the only D-Bus contract available.
Pure GDBus translation of net.catcrafts.IMS1, no core, like imsd-dialerd.
Packaged as the opt-in imsd-ofono subpackage: the daemon, its unit
(Conflicts=ofono.service), the system-bus policy and a gschema override
that points Calls at the ofono provider instead of mm (the ModemManager
origin would offer the modem's CS voice path, which carries no audio on
these phones). The policy grants root send_destination=org.ofono: under
dbus-broker a broadcast is checked against the sender's send policy for
every name the receiver owns, so without it imsd's call signals never
reach the daemon.
Verified on a Fairphone 6 with GNOME Calls 50.0: outgoing and incoming
calls with audio both ways, answered and hung up through
org.gnome.Calls.Call. Known Calls-side gap documented in the README: after
the VoiceCallManager interface is withdrawn and re-added, Calls keeps the
old origin's CallAdded handler and eventually crashes.
2026-09-20 16:07:46 +02:00
2026-07-22 22:53:28 +02:00
## License
GPL-3.0-only — see [LICENSE ](LICENSE ).
## Copyright
Copyright (C) 2026 Catcrafts®
catcrafts.net