ipsec, imsd: the SA setup deletes by exact key instead of flushing the host's xfrm

A fresh registration's SA setup began with `ip xfrm state flush` and `ip
xfrm policy flush`: every SA userspace had installed and every SPD policy
went, a policy-based VPN's (strongSwan's, libreswan's) SAs and policies
included. The setup now takes out what two rules select and leaves the
rest; and the SA reader, which took the P-CSCF ports for a resume from any
reqid-1/2 `dir out` policy — correct only while the flush left imsd's pair
alone in the kernel — takes them from the policies between our two
addresses only. Whether imsd runs beside a live strongSwan or libreswan
connection is unmeasured: the dev phone shows a tunnel policy and a tunnel
state left in place (Tested below), no more.

OwnXfrmDeleteCommands (Imsd:Ipsec) builds the deletes from the kernel text
the shell reads: every transport-mode policy with reqid 1 or 2 whose
selector carries imsd's protected port on the UE side, by the selector ip
xfrm policy prints (src, dst, proto, sport, dport, dev) and its direction,
and every transport-mode reqid-1/2 ESP state running from such a policy's
source to its destination or anchored on the UE's ims-PDN address, by src,
dst, proto esp and SPI — policy or not: a setup whose policy add failed
leaves such states (the setup logs a failed add and goes on), and an ESP
state another transport-mode user put on that address with reqid 1 or 2 goes
with them (an AH or IPComp state is not selected: the rule asks for ESP).
Every reader of the policy text — the deletes and the SA
reader's port pass here — goes through one predicate (detail::OwnPolicy),
written for whatever reads that text later.
BuildSetupCommands loses its two flush lines and the shell runs the deletes
right before it (RunOwnXfrmDeletes); BuildTeardownCommands, the same two
flushes with no caller, goes. A policy-based VPN's tunnel entries and
another transport-mode user's pair on other ports and addresses stay.
Residual: a transport-mode policy with reqid 1 and the UE-side port 45061,
or reqid 2 and 45062 — alone or paired, between any addresses — carries
imsd's fingerprint and goes, with the reqid-1/2 transport-mode ESP states
in its direction, and a reqid-1/2 transport-mode ESP state on the UE's
ims-PDN address goes whoever made it (tests/Ipsec pins both, and that an AH state
stays).

Tested: crafter-build test, 7 passed (the eight deletes byte-exact on the
fixture in the registered FP6's shape (synthetic addresses and keys); a VPN's tunnel SA and policies beside the pair
untouched; another transport-mode user's pair on other ports and addresses
untouched, its reqid-1 ESP state on the UE's address deleted whoever made
it, its AH state left; a policy-less state on the UE's address deleted, one
between other addresses left; the reader's ports from our pair beside
a foreign transport policy with ports); on the dev phone (KPN), in builds of
this series, a fresh registration with a VPN's tunnel policy and state in
the kernel: both still there afterwards, imsd's previous pair gone.
This commit is contained in:
Jorijn van der Graaf 2026-10-13 22:28:02 +02:00
commit 21cb0f870d
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
4 changed files with 198 additions and 25 deletions

View file

@ -39,7 +39,7 @@ PipeWire. The verified device/carrier combinations are in the README.
| `interfaces/Imsd-Sip.cppm` | `imsd::sip` — SIP as a text layer plus TCP framing: header extraction, status codes, the granted lifetime, `ResponseOf` transaction matching. Not a full RFC 3261 parser, deliberately. |
| `interfaces/Imsd-Sdp.cppm` | `imsd::sdp` — SDP offer/answer: the AMR-WB + AMR + telephone-event offer (optional IR.92 preconditions), the parser the media plane needs, `BuildAnswer` for the terminating side, the `CODECS` override. |
| `interfaces/Imsd-Aka.cppm` | `imsd::aka` — USIM AKA glue: the AKAv1-MD5 digest (RFC 3310), IMPI/IMPU/realm from IMSI + MCC/MNC, parsers for `qmicli --uim-get-card-status` and `mmcli` output, the AUTHENTICATE APDU and its response. |
| `interfaces/Imsd-Ipsec.cppm` | `imsd::ipsec` — the two ESP SA pairs (TS 33.203) as `ip xfrm` argv lists, the Security-Server mechanism selection, and the reader that reconstructs SPIs/ports from `ip xfrm state`/`policy` text for a warm resume. |
| `interfaces/Imsd-Ipsec.cppm` | `imsd::ipsec` — the two ESP SA pairs (TS 33.203) as `ip xfrm` argv lists, the Security-Server mechanism selection, and the reader that reconstructs SPIs/ports from `ip xfrm state`/`policy` text for a warm resume, and (1.0.0) `OwnXfrmDeleteCommands`, the deletes by exact key of what two rules select — the policies carrying imsd's fingerprint and the reqid-1/2 transport-mode ESP states in their direction or on the UE's ims-PDN address, whoever installed them (§12). |
| `interfaces/Imsd-Messages.cppm` | `imsd::msg` — every SIP request and response imsd sends, built from an explicit `Context` and per-request variables so the builders are pure functions; byte-pinned by `tests/Messages`. Also `IsEmergency`. |
| `interfaces/Imsd-Engine.cppm` | `imsd::engine` — the decisions as pure reducers: `CallMachine` (one call's dialog, returning `Action`s for the shell) and `RegRecovery` (the registration recovery policy, §7.4). |
| `interfaces/ImsdBus.cppm` | `imsd::bus` — the polkit gate in front of call control, shared by imsd and imsd-ofonod. GLib, so outside `imsd-core`. |
@ -95,7 +95,11 @@ refusal's Retry-After fed as a hold, the old registration written off after
any failed resume), the `OnLink` read that writes the old registration off
when its address has left the netdev (`FreshRecover` after a failed move,
`AddressGone`), and `PacketServiceUp`'s read of
ModemManager before a silent cycle is counted; the shell's reaction to a NOTIFY
ModemManager before a silent cycle is counted;
and the xfrm deletes the SA setup runs — `OwnXfrmDeleteCommands` builds them
from the kernel text, with a test; the shell reads that text and runs them
(§12);
the shell's reaction to a NOTIFY
terminating our binding (the flow closed at once, or kept under a call and
closed when the call ends, a reconnect cycle's or the bring-up resume's wait
ended by it — the reducer's verdict is pinned, the reaction is not); and the
@ -223,7 +227,13 @@ What the suites pin:
- `Aka` — the AKAv1-MD5 digest, identity derivation, the `qmicli`/`mmcli`
text parsers, the AUTHENTICATE APDU and its response parser.
- `Ipsec` — the exact `ip xfrm` argv lists for the SA pairs, the
Security-Server mechanism selection, the SA reader.
Security-Server mechanism selection, the SA reader, and
`OwnXfrmDeleteCommands` (our pair's eight deletes byte-exact on
the fixture in the registered FP6's shape (synthetic addresses and keys), a VPN's tunnel SA and policies beside it
untouched, another transport-mode user's pair on other ports and addresses
untouched — its reqid-1 ESP state on the UE's address deleted, its AH state
left —, a policy-less state on the UE's address deleted, one between
other addresses left).
- `Messages` — every outgoing SIP message byte-for-byte, cross-generated from
the Python reference stack imsd replaced.
- `Sip` — the text layer's edge cases (empty header values, line anchoring,
@ -328,7 +338,14 @@ Rules:
socket (a silent call) — a pin naming an account with one ran, 2026-10-04.
`DetectLocal`'s three-tier address ranking for a run without `LOCAL`
(§10.1) ran through a fake `ip` (2026-10-05: the phone's listing of the
netdev and two cut-down ones).
netdev and two cut-down ones). The 1.0.0 arms no bench has reached: the SA
setup's deletes with another transport-mode user's pair in the kernel
(`tests/Ipsec` only — with a VPN's tunnel policy and tunnel state there,
left in place through a fresh registration, they ran on the dev phone,
2026-10-12); `RunOwnXfrmDeletes`' line for a delete that failed (`… -> rc
N`: an entry gone between the read and the delete); the holds file's `pdn`
compare in canonical form (no capture stages a holds file whose `pdn` is
spelled unlike `LOCAL`).
- `imsd-media --selftest` round-trips the RFC 4867 payload packing (both
formats, both AMR codecs) and the G.711 tables. `MIC_SRC=<file>` (raw s16
PCM at the codec rate, paced in real time) and `PCM_DUMP=1` are test seams
@ -1084,6 +1101,22 @@ only the imsd side of its bus choice.
frozen ABI; the writes are polkit-gated (§7.1, §8.1). Do not add an
ungated write member, and do not add a root translation daemon that
forwards calls without gating its own callers through `imsd-bus`.
- imsd deletes by exact key what two rules select, whoever installed it. A
fresh registration's SA setup deletes — `ip xfrm state delete src dst proto
esp spi`, `ip xfrm policy delete <selector> dir` — what two rules select
(`OwnXfrmDeleteCommands`): a transport-mode policy with reqid 1 or 2 whose
selector carries the protected port 45061/45062 on the UE side (imsd's
fingerprint), and a transport-mode reqid-1/2 ESP state running from such a
policy's source to its destination or anchored on the UE's ims-PDN address
(a state whose policy add failed goes too; an AH or IPComp state is not
selected). Never `ip xfrm state|policy flush`: a policy-based VPN's
(strongSwan's, libreswan's) SAs and policies, or another transport-mode
user's pair on other ports and addresses, stay. Before 1.0.0 the setup
flushed the host's whole xfrm state and policy. Residual: a transport-mode
policy with reqid 1 and the UE-side port 45061, or reqid 2 and 45062 — alone
or paired, between any addresses — goes, with the reqid-1/2 transport-mode
ESP states in its direction, and a reqid-1/2 transport-mode ESP state on the
UE's ims-PDN address goes whoever made it (`tests/Ipsec` pins both).
- The bus policies deliberately let any local account *reach* both names;
authorization is polkit's, not the policy file's. Keep it that way: a
policy that names an account breaks the moment the account is called