ims-pdn-up: discover the P-CSCF list once the PDN is up and hand it to imsd

With the netdev configured, the script runs imsd-pcscf against the
netdev's rmnet mux id (and the ims profile index when it knows one) and
hands the IPv6 addresses it prints to imsd as PCSCF_DISCOVERED= in
/run/imsd.env and gives the first — the daemon's fresh target — a host
route (the SA peer of a registration imsd resumes is routed by imsd itself,
as the configured address's arm already left it); IPv4 addresses and names
are logged and dropped (the PDN is IPv6-only), at most eight are handed
over, and nothing usable is logged with its cause and is not the
script's failure: imsd resumes a registration of its own when it can, else
says so and exits, and the restart runs discovery again. PCSCF= still wins and skips discovery; a list in it is refused.
The script does not read the variable from its environment: the unit
hands it the previous run's file, and a stale discovered value must not
pass for the admin's setting. The reader's path is the IMSD_PCSCF seam
the mock bench uses.

Tested: on the mock bench against this commit's script (the twelve 0.3.10
scenarios; this step's discovery arms S13–S17 and S19–S22, with S18 the
pre-existing non-IPv6 refusal; S55, the reader's rc-0 answer without an
address), and in builds of this series on the dev phone (KPN), whose PCO —
two IPv6 addresses, nothing else — reaches the plain found arm; the
configured skip, the bracketed refusal and, through the IMSD_PCSCF seam, the
reader's failure and its exit 2 ran on the phone too; the v4/FQDN line, the
eight-address cap, the IPv4-only and no-mux-id skips, the comma-list refusal
and the rc-0 arm on the mock only. On a tip build of this series (daemon
2ec1a02f62831edb; bench 40, 2026-10-13) a fresh registration reached a target
this script had not routed — the reader stubbed through the IMSD_PCSCF seam to
find none, the address in /etc/imsd.env as PCSCF_DISCOVERED= — over imsd's own
route, and a resume stood with the peer's route removed first.
This commit is contained in:
Jorijn van der Graaf 2026-10-11 16:16:46 +02:00
commit 1d28063700
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
6 changed files with 186 additions and 86 deletions

View file

@ -5,8 +5,11 @@
#
# Finds-or-creates the ims bearer via ModemManager, connects it, and
# configures the muxed netdev with the MM-assigned address (the s34 recipe,
# automated) and a host route to the P-CSCF — never a default route (F15
# below). Idempotent — safe to run when the PDN is already up. Runs as
# automated), reads the P-CSCF list the network put in the PDN's PCO from the
# modem (imsd-pcscf) unless PCSCF= names one, and gives the configured or
# the first discovered address (the daemon's fresh target) a host route —
# never a default route (F15 below). 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.
#
@ -22,8 +25,13 @@
# (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)
# PCSCF the P-CSCF address imsd registers with (its own
# required setting); gets the host route over the ims PDN
# PCSCF one P-CSCF address, by hand: it wins over discovery
# (which is then skipped) and gets the host route; unset,
# imsd-pcscf reads the PDN's PCO list from the modem and
# this script hands it to imsd as PCSCF_DISCOVERED= in
# /run/imsd.env, the first of them routed
# IMSD_PCSCF the reader's path (default /usr/libexec/imsd-pcscf) — a
# bench seam, from the environment only (not from the file)
log() { echo "ims-pdn-up: $*"; }
@ -291,31 +299,91 @@ log "$IFACE up, $ADDR/${PREFIX:-64}, ipv6 mtu $(cat "/proc/sys/net/ipv6/conf/$IF
if [ -n "$(ip -6 route show default dev "$IFACE" 2>/dev/null)" ]; then
ip -6 route flush default dev "$IFACE" && log "dropped the RA default route on $IFACE"
fi
# the P-CSCF is reachable through the ims PDN only: a host route stands in
# for the default. `dev` without a gateway — a raw-IP netdev without
# header_ops, so the kernel resolves no neighbours — the shape imsd uses
# for media peers. The PDN is brought up IPv6-only (the address above), so
# the P-CSCF must be an IPv6 address; anything else on the IMS network
# (its DNS servers, a traceroute target) needs its own host route. Without
# this route imsd cannot reach the P-CSCF, so a failure is the unit's
# failure.
# ---- the P-CSCF. PCSCF= when the admin set one (one IPv6 address; it wins
# and discovery is skipped), else the list the network put in the PDN's PCO,
# read from the modem by imsd-pcscf: a QMI WDS client bound to this netdev's
# mux data port — the binding ModemManager gives its own clients, and only
# such a client is shown the call's settings. The list goes to imsd as
# PCSCF_DISCOVERED= (its own name: this script inherits the previous run's
# /run/imsd.env through the unit, so a stale discovered value must never
# pass for the admin's PCSCF=, and it is never read from the environment
# here). The PDN is brought up IPv6-only, so only IPv6 addresses are usable;
# IPv4 ones and names are logged and dropped. Nothing usable is not this
# script's failure: imsd resumes a registration of its own when it can; with
# nothing to resume it says so and exits, and the unit's restart runs
# discovery again.
# The P-CSCF is reachable through the ims PDN only: a host route stands in
# for the default — `dev` without a gateway (a raw-IP netdev without
# header_ops, so the kernel resolves no neighbours), the shape imsd uses for
# media peers; of a discovered list the first address — the daemon's fresh
# target — gets one here; imsd routes the P-CSCF it uses itself as well — the
# fresh target before a fresh registration, the resumed SA peer once its SAs
# are confirmed — with the same idempotent replace as a call's media peer, so
# a target this script did not route (a list set in /etc/imsd.env, a route
# lost with the PDN) is reachable.
# Anything else on the IMS network (its DNS servers, a traceroute
# target) needs its own host route. Without the route imsd cannot reach the
# P-CSCF, so a route failure is the unit's failure.
route_pcscf() { # $1 = address
OUT=$(ip -6 route replace "$1/128" dev "$IFACE" 2>&1) ||
{ log "host route to P-CSCF $1 failed: $(squash "$OUT")"; exit 1; }
log "host route to P-CSCF $1 dev $IFACE"
}
PCSCF=${PCSCF:-$(envval PCSCF)}
DISCOVERED=
case "$PCSCF" in
'') log "PCSCF not configured: no P-CSCF host route (imsd needs PCSCF= in /etc/imsd.env)" ;;
'')
if [ "$IP_TYPE" = ipv4 ]; then
log "P-CSCF discovery: not for IMS_IP_TYPE=$IP_TYPE — the bring-up configures only the PDN's IPv6 address"
else
MUX=$(ip -d link show "$IFACE" 2>/dev/null | sed -n 's/.*rmnet mux_id \([0-9]*\).*/\1/p' | head -n1)
if [ -z "$MUX" ]; then
log "P-CSCF discovery: no rmnet mux id on $IFACE (ip -d link); set PCSCF="
else
OUT=$("${IMSD_PCSCF:-/usr/libexec/imsd-pcscf}" --mux "$MUX" ${PROFILE_ID:+--profile "$PROFILE_ID"} --timeout 10 2>&1); rc=$?
V6=; SHOWN=; OTHER=; FLAGS=; ERR=; n=0
while IFS= read -r line; do
case "$line" in
'') ;;
v4\ *|fqdn\ *) OTHER="${OTHER:+$OTHER }$line" ;;
flag\ *) FLAGS="${FLAGS:+$FLAGS, }${line#flag }" ;;
*[!0-9a-f:]*) ERR="${ERR:+$ERR }$line" ;;
*:*) [ "$n" -ge 8 ] && continue; n=$((n + 1))
V6="${V6:+$V6 }$line"; DISCOVERED="${DISCOVERED:+$DISCOVERED,}$line"; SHOWN="${SHOWN:+$SHOWN, }$line" ;;
*) ERR="${ERR:+$ERR }$line" ;;
esac
done <<EOF
$OUT
EOF
if [ -n "$V6" ]; then
log "P-CSCF discovered: $SHOWN${FLAGS:+ ($FLAGS)}"
[ -n "$OTHER" ] && log "P-CSCF discovery also gave $OTHER (not usable on the IPv6-only ims PDN)"
route_pcscf "${V6%% *}" # the first: the daemon's fresh target; imsd routes the P-CSCF it uses itself as well (the fresh target, the resumed peer)
else
case $rc in
0) log "P-CSCF discovery: nothing usable (imsd-pcscf printed no address${ERR:+: $ERR})" ;;
2) log "P-CSCF discovery: nothing usable (the PCO carried no IPv6 P-CSCF${OTHER:+; it gave $OTHER}${FLAGS:+; $FLAGS})" ;;
*) log "P-CSCF discovery: nothing usable (imsd-pcscf rc $rc${ERR:+: $ERR})" ;;
esac
fi
fi
fi ;;
*,*) log "PCSCF '$PCSCF' is a list; one address here (a list is PCSCF_DISCOVERED, the bring-up's)"; exit 1 ;;
*\[*) log "PCSCF '$PCSCF': write the bare address, as ip prints it (no brackets)"; exit 1 ;;
*:*) OUT=$(ip -6 route replace "$PCSCF/128" dev "$IFACE" 2>&1) ||
{ log "host route to P-CSCF $PCSCF failed: $(squash "$OUT")"; exit 1; }
log "host route to P-CSCF $PCSCF dev $IFACE" ;;
*:*) log "P-CSCF $PCSCF configured: discovery skipped"; route_pcscf "$PCSCF" ;;
*) log "PCSCF '$PCSCF' is not an IPv6 address; the ims PDN is brought up IPv6-only"; exit 1 ;;
esac
# ---- hand the connected ims netdev and the PDN's own address to imsd
# (imsd.service reads /run/imsd.env): the daemon carries no baked-in
# interface name and registers on the address the network assigned — not
# on the kernel's temporary SLAAC address from the RA, which the netdev
# lists first and which is deprecated after a day and removed after two
# (ims ledger F18; the SAs and the Contact would go with it)
printf 'DEV=%s\nLOCAL=%s\n' "$IFACE" "$ADDR" > /run/imsd.env
# ---- hand the connected ims netdev, the PDN's own address and the
# discovered P-CSCF list to imsd (imsd.service reads /run/imsd.env): the
# daemon carries no baked-in interface name and registers on the address
# the network assigned — not on the kernel's temporary SLAAC address from
# the RA, which the netdev lists first and which is deprecated after a day
# and removed after two (ims ledger F18; the SAs and the Contact would go
# with it). PCSCF_DISCOVERED= is written on every run, empty when nothing
# was discovered or PCSCF= is set.
printf 'DEV=%s\nLOCAL=%s\nPCSCF_DISCOVERED=%s\n' "$IFACE" "$ADDR" "$DISCOVERED" > /run/imsd.env
log "wrote /run/imsd.env: DEV=$IFACE LOCAL=$ADDR PCSCF_DISCOVERED=$DISCOVERED"
# ---- 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