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

@ -114,6 +114,18 @@ std::string RedactKeys(const std::vector<std::string>& argv) {
return s;
}
// imsd's own xfrm entries out of the kernel, each by its key (the
// OwnXfrmDeleteCommands list — never `ip xfrm … flush`: another IPsec user's
// entries, a policy-based VPN's, stay, §12); a delete that fails (an entry
// gone between the read and the delete) is logged and the rest go on. At
// file scope, as LoadHolds and PersistHolds are.
void RunOwnXfrmDeletes(const std::vector<std::vector<std::string>>& cmds) {
for (auto& cmd : cmds) {
int rc = RunCmd(cmd);
if (rc != 0) Log(std::format("{} -> rc {}", RedactKeys(cmd), rc));
}
}
// ---- small text extractors (401 header params, qmicli "completed:") -------
std::optional<std::string> QuotedAfter(std::string_view text, std::string_view key) {
@ -1471,6 +1483,13 @@ private:
sp.portUc = PortUc; sp.portUs = PortUs; sp.portPs = portPs; sp.portPc = portPc;
sp.alg = alg;
sp.ealg = ealg;
// imsd's previous pair out first, each entry by its key — not `ip xfrm …
// flush`: another IPsec user's entries (a policy-based VPN's) stay (§12)
{
auto state = RunCapture({"ip", "xfrm", "state"});
auto policy = RunCapture({"ip", "xfrm", "policy"});
RunOwnXfrmDeletes(imsd::ipsec::OwnXfrmDeleteCommands(state.out, policy.out, local_));
}
saReplaced_ = true; // from here on the old registration's SAs are gone
for (auto& cmd : imsd::ipsec::BuildSetupCommands(sp)) {
int rc = RunCmd(cmd);
@ -1501,7 +1520,7 @@ private:
if (autoTcp) {
// Two unanswered TCP connects: treat the P-CSCF as UDP-only
// and register again over UDP — a new challenge and a new SA
// pair (the SA setup flushes this one), same ports.
// pair (the SA setup takes this one out), same ports.
Log("protected TCP connect unanswered; registering fresh over UDP");
SetTransport(Transport::Udp);
FreshRegisterAttempt();
@ -1873,7 +1892,7 @@ private:
}
// The rest of what a restart does, in place: a new Call-ID, a new
// challenge, a new SA pair (FreshRegister flushes the old ones), then
// challenge, a new SA pair (FreshRegister takes the old ones out), then
// the post-registration steps. A refused or throttled attempt waits on
// the throttle schedule (a wait no Dial cuts short — on KPN a refused
// attempt costs the old registration) and a silent one on the