All checks were successful
package / package (push) Successful in 1m33s
On carriers whose attach PDN is granted IPv4-only (Odido, Swisscom, O2 UK so far) the modem refuses every AP-side IPv6 bearer request made by APN string with pdn-ipv6-call-disallowed before it reaches the air, so the ims PDN never came up and imsd looped. A request made by 3GPP profile index is treated as the IMS-class call the modem's own engine makes and is exempt: it goes on the air as IPv6 and the network grants it. That is the path 81voltd uses. Look the ims profile up by APN in the modem's profile list and create the bearer with profile-id=<index>,ip-type=<type>; fall back to the APN string when no such profile exists. A profile-indexed bearer reports no APN, so the reuse match accepts the profile id too. New knobs in /etc/imsd.env: IMS_APN (default ims) and IMS_PROFILE_ID (default: looked up; "none" forces the APN-string path). Verified on KPN with the wall induced by a forced IPv4 attach: the APN-string request is refused, the profile-indexed request connects, a second run reuses the bearer, imsd registers over it; attach and mobile data untouched.
188 lines
7.9 KiB
Shell
188 lines
7.9 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_APN ims APN name (default ims)
|
|
# IMS_IP_TYPE bearer ip-type (default ipv6)
|
|
# IMS_PROFILE_ID 3GPP profile index to connect through (default: looked up
|
|
# by APN in the modem's profile list; empty/none = connect by
|
|
# APN string). A profile-indexed call is exempt from the
|
|
# modem's attach-family forcing that refuses an APN-string
|
|
# IPv6 request when the attach PDN was granted IPv4-only
|
|
# (journal/ims 2026-09-16/17 bench: Odido, O2 UK, Swisscom).
|
|
# 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}
|
|
IMS_APN=${IMS_APN:-$(envval IMS_APN)}
|
|
IMS_APN=${IMS_APN:-ims}
|
|
PROFILE_ID=${IMS_PROFILE_ID:-$(envval IMS_PROFILE_ID)}
|
|
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"
|
|
|
|
# ---- the ims profile index (WDS profile list), unless configured
|
|
ims_profile_index() { # $1 = apn
|
|
qmicli -d qrtr://0 --wds-get-profile-list=3gpp 2>/dev/null | awk -v apn="$1" '
|
|
/^[ \t]*\[[0-9]+\] 3gpp/ { idx = $1; gsub(/[^0-9]/, "", idx) }
|
|
/APN:/ { a = $0; sub(/.*APN: '"'"'/, "", a); sub(/'"'"'.*/, "", a);
|
|
if (tolower(a) == tolower(apn) && idx != "") { print idx; exit } }'
|
|
}
|
|
if [ -z "$PROFILE_ID" ]; then
|
|
PROFILE_ID=$(ims_profile_index "$IMS_APN")
|
|
[ -n "$PROFILE_ID" ] && log "ims profile: index $PROFILE_ID (apn $IMS_APN)" || log "ims profile: none for apn $IMS_APN, connecting by APN string"
|
|
elif [ "$PROFILE_ID" = none ]; then
|
|
PROFILE_ID=
|
|
fi
|
|
|
|
# ---- 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
|
|
if [ -n "$PROFILE_ID" ]; then
|
|
echo "$INFO" | grep -q "^bearer\.properties\.profile-id *: *$PROFILE_ID\$" ||
|
|
echo "$INFO" | grep -q "^bearer\.properties\.apn *: *$IMS_APN\$" || continue
|
|
else
|
|
echo "$INFO" | grep -q "^bearer\.properties\.apn *: *$IMS_APN\$" || continue
|
|
fi
|
|
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
|
|
if [ -n "$PROFILE_ID" ]; then
|
|
SPEC="profile-id=$PROFILE_ID,ip-type=$IP_TYPE"
|
|
else
|
|
SPEC="apn=$IMS_APN,ip-type=$IP_TYPE"
|
|
fi
|
|
OUT=$(mmcli -m "$MODEM" --create-bearer="$SPEC" 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 ($SPEC)"
|
|
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
|