imsd 0.2.7 — initial public snapshot

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>
This commit is contained in:
Jorijn van der Graaf 2026-07-22 22:53:28 +02:00
commit 6e77823933
31 changed files with 8761 additions and 0 deletions

42
packaging/APKBUILD Normal file
View file

@ -0,0 +1,42 @@
# SPDX-License-Identifier: GPL-3.0-only
# SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
# Maintainer: Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>
# APKBUILD skeleton — buildable once crafter-build is packaged for Alpine
# (or replaced here by a bootstrap invocation). Until then this documents
# the packaging shape.
pkgname=imsd
pkgver=0.2.7
pkgrel=0
pkgdesc="Userspace IMS/VoLTE daemon for mainline Linux phones"
url="https://forgejo.catcrafts.net/Catcrafts/imsd"
arch="aarch64 x86_64"
license="GPL-3.0-only"
# the media leg dlopen's the AMR-WB codecs; pw-record/pw-play drive PipeWire —
# none of which abuild's .so auto-scan can see
depends="modemmanager opencore-amr vo-amrwbenc pipewire-tools"
# versioned provides = imsd satisfies soc-qcom-modem's 81voltd dependency AND
# excludes the real package: 81voltd serves the modem's own ims-PDN requests,
# which races imsd for the PDN and flaps it with a new prefix every ~2.5 min
# (fp6 journal/ims.md s57) — the two IMS stacks cannot share one PDN
provides="81voltd=$pkgver-r$pkgrel"
makedepends="clang lld libc++-dev glib-dev crafter-build"
source=""
options="!check" # crafter-build test needs qemu for cross; run natively in CI
build() {
crafter-build
crafter-build -- --product=media
crafter-build -- --product=dialerd
}
package() {
install -Dm755 bin/imsd-*/imsd "$pkgdir"/usr/bin/imsd
install -Dm755 bin/imsd-media-*/imsd-media "$pkgdir"/usr/libexec/imsd-media
install -Dm755 bin/imsd-dialerd-*/imsd-dialerd "$pkgdir"/usr/bin/imsd-dialerd
install -Dm755 packaging/ims-pdn-up.sh "$pkgdir"/usr/libexec/ims-pdn-up.sh
install -Dm644 packaging/imsd.service "$pkgdir"/usr/lib/systemd/system/imsd.service
install -Dm644 packaging/imsd-dialerd.desktop \
"$pkgdir"/etc/xdg/autostart/imsd-dialerd.desktop
install -Dm644 packaging/net.catcrafts.IMS1.conf \
"$pkgdir"/usr/share/dbus-1/system.d/net.catcrafts.IMS1.conf
}

36
packaging/APKBUILD.binary Normal file
View file

@ -0,0 +1,36 @@
# SPDX-License-Identifier: GPL-3.0-only
# SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
# Maintainer: Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>
# Interim packaging: wraps a crafter-build binary cross-compiled on a dev box
# (see README "Cross-compiling") into a proper apk, so the daemon is
# apk-managed on the phone while crafter-build itself isn't packaged for
# Alpine yet. The source tarball is produced by packaging/make-bin-tarball.sh.
pkgname=imsd
pkgver=0.2.7
pkgrel=0
pkgdesc="Userspace IMS/VoLTE daemon for mainline Linux phones"
url="https://forgejo.catcrafts.net/Catcrafts/imsd"
arch="aarch64"
license="GPL-3.0-only"
# the media leg dlopen's the AMR-WB codecs; pw-record/pw-play drive PipeWire
depends="modemmanager libc++ opencore-amr vo-amrwbenc pipewire-tools"
# versioned provides = imsd satisfies soc-qcom-modem's 81voltd dependency AND
# excludes the real package: 81voltd serves the modem's own ims-PDN requests,
# which races imsd for the PDN and flaps it with a new prefix every ~2.5 min
# (fp6 journal/ims.md s57) — the two IMS stacks cannot share one PDN
provides="81voltd=$pkgver-r$pkgrel"
options="!check"
source="imsd-$pkgver.tar.gz"
package() {
cd "$srcdir/imsd-$pkgver"
install -Dm755 imsd "$pkgdir"/usr/bin/imsd
install -Dm755 imsd-media "$pkgdir"/usr/libexec/imsd-media
install -Dm755 imsd-dialerd "$pkgdir"/usr/bin/imsd-dialerd
install -Dm755 ims-pdn-up.sh "$pkgdir"/usr/libexec/ims-pdn-up.sh
install -Dm644 imsd.service "$pkgdir"/usr/lib/systemd/system/imsd.service
install -Dm644 imsd-dialerd.desktop \
"$pkgdir"/etc/xdg/autostart/imsd-dialerd.desktop
install -Dm644 net.catcrafts.IMS1.conf \
"$pkgdir"/usr/share/dbus-1/system.d/net.catcrafts.IMS1.conf
}

106
packaging/ims-pdn-up.sh Normal file
View file

@ -0,0 +1,106 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-3.0-only
# SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
# ims-pdn-up.sh — boot bring-up for the IPv6 `ims` PDN (journal/ims.md s45).
#
# Finds-or-creates the ims ipv6 bearer via ModemManager, connects it, and
# configures the muxed netdev with the MM-assigned address (the s34 recipe,
# automated). Idempotent — safe to run when the PDN is already up. Runs as
# ExecStartPre of imsd.service, so imsd only starts once the PDN exists; a
# nonzero exit fails the unit and systemd retries per Restart/RestartSec.
log() { echo "ims-pdn-up: $*"; }
kv() { mmcli "$@" -K 2>/dev/null; }
bearer_paths() {
kv -m "$MODEM" | sed -n 's/^modem\.generic\.bearers\.value\[[0-9]*\] *: *//p'
}
# ---- wait for a modem (MM + modem firmware take a while after boot)
n=0
while :; do
MODEM=$(mmcli -L 2>/dev/null | sed -n 's,.*/Modem/\([0-9]*\).*,\1,p' | head -n1)
[ -n "$MODEM" ] && break
n=$((n + 1))
[ "$n" -ge 60 ] && { log "no modem after 120 s"; exit 1; }
sleep 2
done
log "modem $MODEM"
# ---- find a connected ims bearer; else find-or-create one and connect it
# (LTE attach can lag boot, so connect attempts retry)
find_ims_bearer() { # $1 = required bearer.status.connected value
for B in $(bearer_paths); do
INFO=$(kv -b "$B") || continue
echo "$INFO" | grep -q '^bearer\.properties\.apn *: *ims$' || continue
echo "$INFO" | grep -q "^bearer\.status\.connected *: *$1\$" || continue
echo "$B"
return 0
done
return 1
}
BEARER=$(find_ims_bearer yes)
n=0
while [ -z "$BEARER" ]; do
n=$((n + 1))
[ "$n" -gt 10 ] && { log "bearer connect failed after 10 attempts"; exit 1; }
B=$(find_ims_bearer no) # reuse a stale disconnected ims bearer
if [ -z "$B" ]; then
B=$(mmcli -m "$MODEM" --create-bearer='apn=ims,ip-type=ipv6' 2>/dev/null |
sed -n 's,.*\(/org/freedesktop/ModemManager1/Bearer/[0-9]*\).*,\1,p')
[ -n "$B" ] && log "created bearer $B"
fi
if [ -n "$B" ] && mmcli -b "$B" --connect >/dev/null 2>&1; then
BEARER=$B
else
log "connect attempt $n failed; retrying in 10 s"
sleep 10
fi
done
log "connected: $BEARER"
# ---- configure the muxed netdev with the MM-assigned address
INFO=$(kv -b "$BEARER")
IFACE=$(echo "$INFO" | sed -n 's/^bearer\.status\.interface *: *//p')
ADDR=$(echo "$INFO" | sed -n 's/^bearer\.ipv6-config\.address *: *//p')
PREFIX=$(echo "$INFO" | sed -n 's/^bearer\.ipv6-config\.prefix *: *//p')
if [ -z "$IFACE" ] || [ -z "$ADDR" ]; then
log "bearer up but no interface/address in mmcli output"
exit 1
fi
ip link set "$IFACE" up || exit 1
ip -6 addr replace "$ADDR/${PREFIX:-64}" dev "$IFACE" || exit 1
# wait out IPv6 DAD: binding a tentative address gives EADDRNOTAVAIL
# (first boot attempt cost a 120 s systemd retry exactly this way)
n=0
while ip -6 addr show dev "$IFACE" | grep -q tentative; do
n=$((n + 1))
[ "$n" -ge 10 ] && { log "address still tentative after 10 s"; break; }
sleep 1
done
log "$IFACE up, $ADDR/${PREFIX:-64}"
# ---- hand the connected ims netdev to imsd (imsd.service reads
# /run/imsd.env) so the daemon carries no baked-in interface name
echo "DEV=$IFACE" > /run/imsd.env
# ---- open the IMS protected ports in the firewall (rung 5b root cause,
# journal/ims.md s56): pmOS's default nftables INPUT chain is policy-drop and
# explicitly drops all inbound on qmapmux*, which silently killed every
# network-initiated request (reg-event NOTIFY, MT INVITE) after ESP decap —
# the P-CSCF's TCP SYNs to the protected server port never reached the
# listener, so terminating delivery failed and MT calls fell back to CS.
# 45061/45062 = imsd's protected client/server ports (kPortUc/kPortUs).
# Best-effort: never fail the unit over a missing/foreign firewall.
if nft list table inet filter >/dev/null 2>&1; then
if ! nft list chain inet filter input | grep -q imsd-protected-ports; then
nft insert rule inet filter input iifname "qmapmux*" tcp dport 45061-45062 accept comment '"imsd-protected-ports"' &&
nft insert rule inet filter input iifname "qmapmux*" udp dport 45061-45062 accept comment '"imsd-protected-ports"' &&
log "nftables: opened protected ports 45061-45062 on qmapmux*" ||
log "nftables: rule insert failed (continuing)"
fi
fi
exit 0

View file

@ -0,0 +1,11 @@
# SPDX-License-Identifier: GPL-3.0-only
# SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
[Desktop Entry]
Type=Application
Name=IMS Dialer Daemon
Comment=Plasma Dialer backend for imsd (replaces plasma-dialer's modem-daemon)
Exec=/usr/bin/imsd-dialerd
Icon=call-start
NoDisplay=true
X-KDE-autostart-phase=1
X-KDE-StartupNotify=false

34
packaging/imsd.service Normal file
View file

@ -0,0 +1,34 @@
# SPDX-License-Identifier: GPL-3.0-only
# SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
[Unit]
Description=Userspace IMS/VoLTE daemon (SIP register + calls over the ims PDN)
# needs the ims PDN (multiplexed netdev + global v6) — ims-pdn-up.sh
# creates/connects the bearer through ModemManager and configures the netdev
After=ModemManager.service network.target
Wants=ModemManager.service
[Service]
Type=simple
# bring up the ims PDN first; waits for modem + LTE attach, idempotent.
# worst case ~4 min (modem wait + connect retries), hence TimeoutStartSec
ExecStartPre=/usr/libexec/ims-pdn-up.sh
TimeoutStartSec=600
# /run/imsd.env: written by ims-pdn-up.sh (DEV= the connected ims netdev).
# /etc/imsd.env: admin configuration — PCSCF= is REQUIRED (the carrier's
# P-CSCF address; see the README's Configuration section) and wins over the
# runtime-derived values.
EnvironmentFile=-/run/imsd.env
EnvironmentFile=-/etc/imsd.env
# registration state (imsreg.state) + media stats live here
StateDirectory=imsd
ExecStart=/usr/bin/imsd
# registration: resume an existing SA when possible (avoids fresh-SA
# throttles); a failed bring-up exits nonzero and systemd retries
Restart=on-failure
RestartSec=120
# SIGTERM: engine keeps the SA + registration state for the next resume
KillMode=mixed
TimeoutStopSec=10
[Install]
WantedBy=multi-user.target

28
packaging/make-bin-tarball.sh Executable file
View file

@ -0,0 +1,28 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-3.0-only
# SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
# make-bin-tarball.sh — bundle the cross-compiled imsd binary + runtime files
# into the source tarball APKBUILD.binary consumes. Run from the repo root
# after a cross build; output lands in the current directory.
set -eu
VER="${1:-$(sed -n 's/.*char\* Version = "\(.*\)".*/\1/p' implementations/main.cpp)}"
[ -n "$VER" ] || { echo "could not determine version — pass it as \$1" >&2; exit 1; }
BIN=$(ls -t bin/imsd-aarch64-*/imsd 2>/dev/null | head -n1)
MEDIA=$(ls -t bin/imsd-media-aarch64-*/imsd-media 2>/dev/null | head -n1)
DIALERD=$(ls -t bin/imsd-dialerd-aarch64-*/imsd-dialerd 2>/dev/null | head -n1)
[ -n "$BIN" ] || { echo "no aarch64 imsd build found — cross-compile first" >&2; exit 1; }
[ -n "$MEDIA" ] || { echo "no aarch64 imsd-media build — cross-compile with --product=media" >&2; exit 1; }
[ -n "$DIALERD" ] || { echo "no aarch64 imsd-dialerd build — cross-compile with --product=dialerd" >&2; exit 1; }
stage=$(mktemp -d)
trap 'rm -rf "$stage"' EXIT
mkdir "$stage/imsd-$VER"
cp "$BIN" "$stage/imsd-$VER/imsd"
cp "$MEDIA" "$stage/imsd-$VER/imsd-media"
cp "$DIALERD" "$stage/imsd-$VER/imsd-dialerd"
cp packaging/ims-pdn-up.sh packaging/imsd.service \
packaging/imsd-dialerd.desktop \
packaging/net.catcrafts.IMS1.conf "$stage/imsd-$VER/"
tar -C "$stage" -czf "imsd-$VER.tar.gz" "imsd-$VER"
echo "wrote imsd-$VER.tar.gz ($(du -h "imsd-$VER.tar.gz" | cut -f1))"

45
packaging/make-sysroot.sh Executable file
View file

@ -0,0 +1,45 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-3.0-only
# SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
# make-sysroot.sh — build an Alpine aarch64 sysroot for cross-compiling imsd.
#
# Downloads apk-tools-static + the Alpine signing keys from the CDN and
# populates a minimal rootfs (musl, libc++, glib and their headers) that
# clang can use via --sysroot. No root, no qemu, no device needed:
#
# packaging/make-sysroot.sh [dir] # default: ~/.cache/imsd/sysroot-aarch64-alpine
# crafter-build --target=aarch64-alpine-linux-musl --sysroot=<dir>
set -eu
SYSROOT="${1:-$HOME/.cache/imsd/sysroot-aarch64-alpine}"
MIRROR="${MIRROR:-https://dl-cdn.alpinelinux.org/alpine/edge}"
ARCH=aarch64
# gcc is in the list for its crtbeginS.o/libgcc: clang's driver for
# *-alpine-linux-musl links against the GCC runtime it detects under
# <sysroot>/usr/lib/gcc/<triple>/
PKGS="musl-dev libc++ libc++-dev glib-dev compiler-rt llvm-libunwind-dev gcc"
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT
fetch_pkg() { # $1 = repo (main/community), $2 = package name -> path on stdout
file=$(curl -fsSL "$MIRROR/$1/x86_64/" |
grep -o "href=\"$2-[0-9][^\"]*\.apk\"" | head -n1 | cut -d'"' -f2)
[ -n "$file" ] || { echo "cannot find $2 in $MIRROR/$1" >&2; exit 1; }
curl -fsSL -o "$work/$file" "$MIRROR/$1/x86_64/$file"
echo "$work/$file"
}
echo ">> fetching apk-tools-static + alpine-keys"
tar -xzf "$(fetch_pkg main apk-tools-static)" -C "$work" sbin/apk.static 2>/dev/null
tar -xzf "$(fetch_pkg main alpine-keys)" -C "$work" usr/share/apk/keys 2>/dev/null
echo ">> populating $SYSROOT ($ARCH: $PKGS)"
mkdir -p "$SYSROOT"
"$work/sbin/apk.static" \
--arch "$ARCH" --root "$SYSROOT" --initdb --no-scripts --no-cache \
--usermode --keys-dir "$work/usr/share/apk/keys/$ARCH" \
-X "$MIRROR/main" -X "$MIRROR/community" \
add $PKGS
echo ">> done: $SYSROOT"

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?> <!--*-nxml-*-->
<!-- SPDX-License-Identifier: GPL-3.0-only -->
<!-- SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® -->
<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<!-- imsd (userspace VoLTE daemon) system-bus policy.
Install: /usr/share/dbus-1/system.d/net.catcrafts.IMS1.conf -->
<busconfig>
<policy user="root">
<allow own="net.catcrafts.IMS1"/>
<allow send_destination="net.catcrafts.IMS1"/>
<allow receive_sender="net.catcrafts.IMS1"/>
</policy>
<!-- the phone session user drives calls via the imsdialerd shim -->
<policy user="user">
<allow send_destination="net.catcrafts.IMS1"/>
<allow receive_sender="net.catcrafts.IMS1"/>
</policy>
<policy context="default">
<deny send_destination="net.catcrafts.IMS1"/>
</policy>
</busconfig>