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:
commit
6e77823933
31 changed files with 8761 additions and 0 deletions
42
packaging/APKBUILD
Normal file
42
packaging/APKBUILD
Normal 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
36
packaging/APKBUILD.binary
Normal 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
106
packaging/ims-pdn-up.sh
Normal 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
|
||||
11
packaging/imsd-dialerd.desktop
Normal file
11
packaging/imsd-dialerd.desktop
Normal 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
34
packaging/imsd.service
Normal 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
28
packaging/make-bin-tarball.sh
Executable 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
45
packaging/make-sysroot.sh
Executable 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"
|
||||
22
packaging/net.catcrafts.IMS1.conf
Normal file
22
packaging/net.catcrafts.IMS1.conf
Normal 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue