- C++ 93.2%
- Shell 6.8%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
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. |
||
| .forgejo/workflows | ||
| implementations | ||
| interfaces | ||
| packaging | ||
| tests | ||
| .gitignore | ||
| AGENTS.md | ||
| LICENSE | ||
| project.cpp | ||
| README.md | ||
imsd
Userspace IMS/VoLTE daemon for mainline Linux phones.
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 IPsec (ESP), call signalling, and the AMR-WB RTP media plane wired into 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.
Status
Call-capable, both directions: outgoing calls place, answer, carry full-duplex
AMR-WB audio and tear down. Incoming calls ring, answer and carry media the
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,
USSD, P-CSCF discovery (the address is configured by hand).
Verified by community members to be fully working:
| Device | OS | Carrier |
|---|---|---|
| The Fairphone (Gen. 6) | postmarketOS, Linux 7.1.2 | KPN NL |
| The Fairphone (Gen. 6) | postmarketOS, Linux 7.1.2 | Telekom Deutschland GER |
| The Fairphone (Gen. 6) | postmarketOS, Linux 7.1.2 | Phonero |
| The Fairphone (Gen. 6) | postmarketOS, Linux 7.1.2 | Telia Norge |
What it needs
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
WWAN/ModemManager integration, kernel ESP, PipeWire, systemd, polkit and
your carrier's P-CSCF address.
Install and configure
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
Configuration is environment variables in /etc/imsd.env; the one required
setting is the P-CSCF:
# your carrier's P-CSCF address, capture via: `adb shell dumpsys telephony.registry` on Android
PCSCF=2001:db8::105
The apk's imsd.service is enabled by preset and starts at boot once that
file exists (the condition is checked when the unit starts, so after writing
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:
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
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.
| 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 |
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.
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']").
Do not run it alongside a real ofonod.
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.
Full config:
| Variable | Default | Meaning |
|---|---|---|
PCSCF |
(none — required) | P-CSCF address (IPv6); without it registration fails with a clear error |
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 |
LOCAL |
(auto) | UE address. 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) |
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), where that schedule stands and whether the old registration is gone (old_gone), 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) |
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) |
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.
License
GPL-3.0-only — see LICENSE.
Copyright
Copyright (C) 2026 Catcrafts® catcrafts.net