ims-pdn-up: never let the ims PDN be the IPv6 default route

The PGW's router advertisement on the ims netdev offers a default
router, and on IPv4-only wifi that is the phone's only IPv6 default
route. Every AAAA-first client (apk, browsers) then sends its SYN into
the IMS walled garden, which drops it: apk sat in SYN-SENT for nine
minutes on the dev phone, and a field user's remedy was to stop imsd
before package work.

Set accept_ra_defrtr=0 on the muxed netdev before link-up (the router
solicitation goes out once the link-local address passes DAD), flush a
default route an earlier RA installed (an imsd restart), and give the
P-CSCF a host route over the netdev instead. imsd talks to the P-CSCF
only and already installs a host route per media peer; the RA's prefix
and MTU still apply. The host route makes the P-CSCF reachable before
the RA has been processed, so the PDN MTU ModemManager reports is set
on the link as well, the way NetworkManager does for its bearers. A
failed host route, or a P-CSCF that is not an IPv6 address (the PDN is
brought up IPv6-only), fails the unit with a clear message.

Dev phone (KPN, 0.3.8-r0): restart path drops the RA default and
registers over the host route, apk update in 6 s where it hung; MO and
MT calls with audio both ways; cold boot never installs the default
route, the link carries the 1280 MTU before the host route exists,
fresh registration in 1 s.
This commit is contained in:
Jorijn van der Graaf 2026-09-24 04:12:39 +02:00
commit 8b376bbf95
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
2 changed files with 62 additions and 3 deletions

View file

@ -149,7 +149,16 @@ On a phone the daemon runs as root on the system bus (policy in
account `user` access; adjust it alongside `AUDIO_USER` if your session account `user` access; adjust it alongside `AUDIO_USER` if your session
account is named differently), started by `packaging/imsd.service` account is named differently), started by `packaging/imsd.service`
after `packaging/ims-pdn-up.sh` has brought up the ims PDN through after `packaging/ims-pdn-up.sh` has brought up the ims PDN through
ModemManager. It spawns `/usr/libexec/imsd-media` per call. Registration ModemManager. The bring-up never lets the ims PDN become the phone's IPv6
default route: the PDN's router advertisement is not accepted as a default
router (`accept_ra_defrtr=0` on the netdev; its prefix and MTU still apply)
and the P-CSCF gets a host route instead, so on IPv4-only wifi the phone's
other IPv6 traffic (apk, browsers) is not sent into the IMS walled garden,
where it would hang. Media peers get a host route per call; anything else
on the IMS network (its DNS servers, a traceroute target) needs its own
(`ip -6 route add <addr> dev qmapmux0.0`). The PDN MTU ModemManager reports
is set on the link. It spawns
`/usr/libexec/imsd-media` per call. Registration
resumes an in-kernel IPsec SA when one is present (avoiding the network's resumes an in-kernel IPsec SA when one is present (avoiding the network's
fresh-SA throttle) and keeps itself alive with a re-REGISTER refresh at half fresh-SA throttle) and keeps itself alive with a re-REGISTER refresh at half
the granted lifetime. `packaging/APKBUILD` builds the apk from source via the granted lifetime. `packaging/APKBUILD` builds the apk from source via

View file

@ -5,7 +5,8 @@
# #
# Finds-or-creates the ims bearer via ModemManager, connects it, and # Finds-or-creates the ims bearer via ModemManager, connects it, and
# configures the muxed netdev with the MM-assigned address (the s34 recipe, # 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 # 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
# ExecStartPre of imsd.service, so imsd only starts once the PDN exists; a # ExecStartPre of imsd.service, so imsd only starts once the PDN exists; a
# nonzero exit fails the unit and systemd retries per Restart/RestartSec. # nonzero exit fails the unit and systemd retries per Restart/RestartSec.
# #
@ -21,6 +22,8 @@
# (journal/ims 2026-09-16/17 bench: Odido, O2 UK, Swisscom). # (journal/ims 2026-09-16/17 bench: Odido, O2 UK, Swisscom).
# IMS_REG_TIMEOUT max seconds to wait for network registration before # IMS_REG_TIMEOUT max seconds to wait for network registration before
# connect attempts start counting anyway (default 300) # 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
log() { echo "ims-pdn-up: $*"; } log() { echo "ims-pdn-up: $*"; }
@ -217,6 +220,29 @@ if [ -z "$IFACE" ] || [ -z "$ADDR" ]; then
log "bearer up but no interface/address in mmcli output" log "bearer up but no interface/address in mmcli output"
exit 1 exit 1
fi fi
# ---- the ims PDN must never be the phone's IPv6 default route (F15). The
# PGW's router advertisement offers a default router; on IPv4-only wifi
# that is the phone's only v6 default, so every AAAA-first client (apk,
# browsers) sends its SYN into the IMS walled garden, which drops it
# silently (dev phone 2026-09-20: a 9-min apk hang). imsd only talks to the
# P-CSCF (host route below) and to media peers (imsd installs those host
# routes per call); the RA's prefix and MTU are still applied (its hop
# limit is not — the kernel default 64 stays). Per-netdev, via /proc, and
# before link-up: the router solicitation goes out once the link-local
# address passes DAD, ~1–2 s after link-up, and the RA answers at once.
if ! echo 0 > "/proc/sys/net/ipv6/conf/$IFACE/accept_ra_defrtr" 2>/dev/null; then
log "could not set accept_ra_defrtr=0 on $IFACE (continuing)"
fi
# the PDN's MTU as the modem reports it (1280 here), on the link itself:
# the P-CSCF host route makes the P-CSCF reachable before the RA's MTU
# option has been processed, and a send in that window would otherwise be
# sized by the link's default MTU. NetworkManager does the same for its
# bearers. An RA MTU no larger than the link's is still applied.
MTU=$(echo "$INFO" | sed -n 's/^bearer\.ipv6-config\.mtu *: *//p')
case "$MTU" in ''|--|*[!0-9]*) MTU= ;; esac
if [ -n "$MTU" ] && [ "$MTU" -ge 1280 ]; then
ip link set "$IFACE" mtu "$MTU" || log "could not set mtu $MTU on $IFACE (continuing)"
fi
ip link set "$IFACE" up || exit 1 ip link set "$IFACE" up || exit 1
ip -6 addr replace "$ADDR/${PREFIX:-64}" dev "$IFACE" || exit 1 ip -6 addr replace "$ADDR/${PREFIX:-64}" dev "$IFACE" || exit 1
@ -228,7 +254,31 @@ while ip -6 addr show dev "$IFACE" | grep -q tentative; do
[ "$n" -ge 10 ] && { log "address still tentative after 10 s"; break; } [ "$n" -ge 10 ] && { log "address still tentative after 10 s"; break; }
sleep 1 sleep 1
done done
log "$IFACE up, $ADDR/${PREFIX:-64}" log "$IFACE up, $ADDR/${PREFIX:-64}, ipv6 mtu $(cat "/proc/sys/net/ipv6/conf/$IFACE/mtu" 2>/dev/null)"
# a default route an earlier RA already installed (the link was up before
# this run: an imsd restart) — accept_ra_defrtr=0 leaves an existing one in
# place until its lifetime runs out. Any ::/0 via this netdev goes, by the
# policy above.
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.
PCSCF=${PCSCF:-$(envval PCSCF)}
case "$PCSCF" in
'') log "PCSCF not configured: no P-CSCF host route (imsd needs PCSCF= in /etc/imsd.env)" ;;
*:*) 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 "PCSCF '$PCSCF' is not an IPv6 address; the ims PDN is brought up IPv6-only"; exit 1 ;;
esac
# ---- hand the connected ims netdev to imsd (imsd.service reads # ---- hand the connected ims netdev to imsd (imsd.service reads
# /run/imsd.env) so the daemon carries no baked-in interface name # /run/imsd.env) so the daemon carries no baked-in interface name