imsd/packaging/ims-pdn-up.sh
Jorijn van der Graaf e5911c09c2 ims-pdn-up: log mmcli errors, gate on registration, configurable ip-type
Three hardenings, each of which cost a field-debugging round-trip:

- Every mmcli failure now lands in the journal verbatim (create-bearer
  and connect stderr were swallowed, making 'connect attempt N failed'
  undecodable — no-service and interface-in-use looked identical).
- Connect attempts are gated on network registration (up to
  IMS_REG_TIMEOUT, default 300 s): the 10x10 s window is shorter than
  some carriers' post-boot attach (~2 min measured), so every attempt
  could burn out before the network was even attached. The settled
  modem/packet-service state is logged either way.
- Bearer ip-type is configurable via IMS_IP_TYPE in /etc/imsd.env
  (default ipv6); the bearer find/reuse now matches ip-type too, so a
  config change cannot silently reuse a stale bearer of the old type.

TimeoutStartSec grows 600->900 to cover the registration gate.

Tested on the FP6 dev phone (KPN): clean-state bring-up in 4 s;
gate + timeout path with modem disabled; per-attempt error lines on a
real contested-PDN failure (second ims PDN while one is connected);
idempotent reuse + REGISTERED (fresh) via systemd.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-01 18:10:04 +02:00

154 lines
6.2 KiB
Shell

#!/bin/sh
# SPDX-License-Identifier: GPL-3.0-only
# SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
# ims-pdn-up.sh — boot bring-up for the `ims` PDN (journal/ims.md s45).
#
# Finds-or-creates the ims 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.
#
# Configuration, via the unit's EnvironmentFile /etc/imsd.env (also read
# directly so manual runs behave the same):
# IMS_IP_TYPE bearer ip-type (default ipv6)
# IMS_REG_TIMEOUT max seconds to wait for network registration before
# connect attempts start counting anyway (default 300)
log() { echo "ims-pdn-up: $*"; }
kv() { mmcli "$@" -K 2>/dev/null; }
# mmcli's multi-line error text as one journal-friendly line
squash() { printf '%s' "$1" | tr '\n' ' ' | sed 's/ */ /g'; }
modem_state() { kv -m "$MODEM" | sed -n 's/^modem\.generic\.state *: *//p'; }
packet_state() { kv -m "$MODEM" | sed -n 's/^modem\.3gpp\.packet-service-state *: *//p'; }
bearer_paths() {
kv -m "$MODEM" | sed -n 's/^modem\.generic\.bearers\.value\[[0-9]*\] *: *//p'
}
envval() { # $1 = key — for manual runs; under systemd the vars are inherited
sed -n "s/^$1=//p" /etc/imsd.env 2>/dev/null | tail -n1 | tr -d '"'
}
IP_TYPE=${IMS_IP_TYPE:-$(envval IMS_IP_TYPE)}
IP_TYPE=${IP_TYPE:-ipv6}
REG_TIMEOUT=${IMS_REG_TIMEOUT:-$(envval IMS_REG_TIMEOUT)}
REG_TIMEOUT=${REG_TIMEOUT:-300}
# ---- 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
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\.properties\.ip-type *: *$IP_TYPE\$" || continue
echo "$INFO" | grep -q "^bearer\.status\.connected *: *$1\$" || continue
echo "$B"
return 0
done
return 1
}
BEARER=$(find_ims_bearer yes)
# ---- gate the connect attempts on network registration: the 10x10 s window
# below is shorter than some carriers' post-boot attach (measured ~2 min), so
# without this every attempt can fail on no-service and the retries end
# before the network is even attached
if [ -z "$BEARER" ]; then
waited=0 last=
while :; do
STATE=$(modem_state)
case "$STATE" in registered|connecting|connected) break ;; esac
[ "$STATE" != "$last" ] && log "waiting for registration (state: ${STATE:-unknown})"
last=$STATE
if [ "$waited" -ge "$REG_TIMEOUT" ]; then
log "not registered after $REG_TIMEOUT s — attempting anyway"
break
fi
sleep 5
waited=$((waited + 5))
done
log "modem state: $(modem_state), packet service: $(packet_state)"
fi
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
OUT=$(mmcli -m "$MODEM" --create-bearer="apn=ims,ip-type=$IP_TYPE" 2>&1)
B=$(printf '%s' "$OUT" | sed -n 's,.*\(/org/freedesktop/ModemManager1/Bearer/[0-9]*\).*,\1,p')
if [ -n "$B" ]; then
log "created bearer $B (ip-type=$IP_TYPE)"
else
log "create-bearer attempt $n failed: $(squash "$OUT"); retrying in 10 s"
sleep 10
continue
fi
fi
if OUT=$(mmcli -b "$B" --connect 2>&1); then
BEARER=$B
else
log "connect attempt $n failed (state: $(modem_state)): $(squash "$OUT"); 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