ipsec, engine, imsd: the state file records the P-CSCF and a resume follows the kernel's SA peer

A resume compared PCSCF= against the kernel's SAs and, when they did not
match, registered fresh without a word — a second initial REGISTER from an
address whose registration is live; on KPN a fresh pair to the same P-CSCF
meets the 21-minute refusal that costs the live registration (§13), what
another P-CSCF does with it is unmeasured. The registration was made to
whatever P-CSCF the SAs name (the SA setup takes imsd's previous pair
out first), so that is where a resume belongs: SaPeer reads
it from the dir-out transport policies — the one address a reqid-1 and a
reqid-2 policy both name, each with imsd's fixed protected port as its
selector's sport (reqid and mode alone are anyone's): the state text cannot tell the UE from the
P-CSCF, a lone policy is not ours and a VPN's beside the pair changes
nothing — TryResume resumes there, routes it, and says so when PCSCF=
differs; the next fresh registration moves to PCSCF= and moves back when
it fails before the SA setup and the old registration still stands (its
address still on the link), as it already did between addresses, and
its fallback Route is the P-CSCF it registers at, set before the 200's
Service-Route is looked for — an empty Service-Route value counts as
none — so a move never keeps the previous node's route. A file left by an attempt that got through the SA setup and did not live
to record — to this or another P-CSCF — is recognised by its SA pair: the
file's spi_uc/spi_us (a 0.3.x file carries them too) against the kernel's
(FileNamesAnotherPair, Imsd:Ipsec, pinned in tests/Ipsec), instead of
resumed with the wrong dialog; the state file gains pcscf, named in that
line. Both fresh paths — the bring-up's and recovery's — now set
their target themselves: with no PCSCF=, a bring-up that did not resume
fails with its own message, and since no fresh one follows: a resumable
pair whose attempt failed stays for the restart to resume, a pair nothing
can resume — one the engine's rule wrote off during the attempt among them,
a refresh refused with a 403 that carries a Retry-After or a binding a NOTIFY
terminated under the re-REGISTER wait, decided once after both catch arms by
its verdict (Reconnectable(), and with no fresh target Fresh(): a 401, a
refusal with a Retry-After, the second refusal — with no fresh target
every refusal reaches the reducer and its count travels in the holds, so
the restart's refusal is the second, while with a target only a
Retry-After does, as before — a refusal without one is not counted and the
fresh registration follows at once; without the count a refused no-target resume would re-REGISTER on the
same Call-ID at every RestartSec, the F16 loop) — is removed so the next
bring-up finds nothing — and a recovery step that needs one restarts the daemon; the "PCSCF not set" check at the top of BringUp is gone, so a
registration can be resumed with no setting at all. Three journal lines of
the fresh-target path change with it: `resume connect failed; falling back
to fresh` and `state file transport differs from SIP_TRANSPORT; fresh` end
in `the next registration is a fresh one`, and a resume the engine writes
off (a 403 with a Retry-After, a binding a NOTIFY terminated under the wait)
logs `resume failed: … — written off by the network: the next registration
is a fresh one` (§7.8). `no reg state file; resume falls back to fresh`
keeps its text with a fresh target and ends in `nothing can resume it: the
pair is removed` without one; the new `the state file names another SA pair
(0x…/0x…[ to X]) than the kernel's (0x…/0x… to Y): another attempt's; …`
line takes the same two tails.

Tested: crafter-build test, 7 passed (tests/Engine: the refusal count's travel
through the holds); the resume on the SA peer with
the fresh target absent in builds of this series on the dev
phone (KPN), and the no-target bring-up there with a resume that failed on
a transport mismatch (the pair kept, the restart resumed) and with the
state file removed (the pair removed, the next bring-up found nothing); the
two dead-resume lines' no-target tail — a missing file, a file with another
pair's SPIs — and the pair's removal on the tip's host build (bench 23f: a
fake modem on PATH, a real xfrm in a namespace; a file with the pair's SPIs
but another P-CSCF's address passes the check and the resume runs — `RESUME
via existing SA`, the connect then failing in the namespace); on the dev
phone (bench 41, the tip's build) a resume on a file without its pcscf key,
the live pair's SPIs.
This commit is contained in:
Jorijn van der Graaf 2026-10-11 15:58:49 +02:00
commit 40a960f749
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
7 changed files with 448 additions and 84 deletions

View file

@ -19,11 +19,14 @@ SPIs in use, which a reregistration's new SPIs must differ from (and the
state file falls back to) — and a one-mechanism Security-Server value that
stands in for the Security-Verify when the state file holds no list for the
SA pair in the kernel (`ChooseResumeSecurityVerify` decides; since 0.3.12
the 401's own list is stored there). Since 1.0.0 also
`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).
the 401's own list is stored there). Since 1.0.0 also `SaPeer`: the P-CSCF
the kernel's pair is to, read from the `dir out` policies that carry imsd's
fingerprint; `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); and `FileNamesAnotherPair`, whether a state
file's spi_uc/spi_us name the pair the kernel holds (unknown on either side
decides nothing).
Pure std C++ (the shell runs the commands and captures the text). Command
shapes and the parser are pinned by tests/Ipsec.
@ -461,6 +464,19 @@ export namespace imsd::ipsec {
return r;
}
// Whether a state file was written for the SA pair the kernel holds: the
// file's spi_uc/spi_us (the UE's inbound SPIs — drawn per fresh attempt,
// kept by a re-REGISTER, written back by a resume) against the pair's. A
// file another attempt left behind — one that got through the SA setup
// and did not live to record, to this or another P-CSCF — names another
// pair, and its dialog is not this pair's. Unknown on either side (a
// file from before the keys, a 0 from a missing inbound state) decides
// nothing.
bool FileNamesAnotherPair(std::optional<long> fileUc, std::optional<long> fileUs, std::uint32_t saUc, std::uint32_t saUs) {
if (!fileUc || !fileUs || *fileUc <= 0 || *fileUs <= 0 || saUc == 0 || saUs == 0) return false;
return static_cast<std::uint32_t>(*fileUc) != saUc || static_cast<std::uint32_t>(*fileUs) != saUs;
}
// Every local address holding an SA to the P-CSCF, once each, in kernel
// order: where a registration made on another address of the link (the
// kernel's temporary one, before 0.3.10 — ims ledger F18) still lives.
@ -475,6 +491,37 @@ export namespace imsd::ipsec {
return locals;
}
// The P-CSCF the kernel's SAs are to: the one address that a `dir out`
// policy of imsd's own with reqid 1 and one with reqid 2 both name
// (detail::OwnPolicy: transport mode, the fixed protected port as the
// selector's sport — reqid and mode alone are anyone's) — exactly the pair
// BuildSetupCommands installs. The state text cannot
// tell the two ends apart (the UE is a `dst` there too). A foreign policy
// beside the pair (a VPN's: tunnel mode, another destination, whatever
// its reqid) changes nothing; a lone policy is not a registration, and
// two addresses each carrying both reqids are not one pair to tell
// apart: nullopt then, and when there are none.
std::optional<std::string> SaPeer(std::string_view policyText) {
std::vector<std::string> r1;
std::vector<std::string> r2;
for (std::string_view b : detail::SrcBlocks(policyText)) {
if (b.find("dir out") == std::string_view::npos || !detail::OwnPolicy(b)) continue;
auto reqid = detail::ParseDec(detail::TokenAfter(b, "reqid "));
std::string_view dstTok = detail::TokenAfter(b, "dst ");
const std::string dst = imsd::util::CanonicalAddr(dstTok.substr(0, dstTok.find('/')));
if (dst.empty()) continue;
std::vector<std::string>& set = *reqid == 1 ? r1 : r2;
if (std::ranges::find(set, dst) == set.end()) set.push_back(dst);
}
std::optional<std::string> peer;
for (const std::string& d : r1) {
if (std::ranges::find(r2, d) == r2.end()) continue;
if (peer) return std::nullopt;
peer = d;
}
return peer;
}
// The `ip xfrm … delete` argv lists that take the entries two rules select
// out of the kernel as the two texts show it: every policy
// of imsd's own (detail::OwnPolicy — transport mode, reqid 1 or 2, the
@ -489,7 +536,8 @@ export namespace imsd::ipsec {
// Everything else — a policy-based
// VPN's tunnel entries, another transport-mode user's on other ports and
// addresses — stays: the fresh registration's SA setup runs these before
// BuildSetupCommands' adds, and flushes nothing (§12). Residual: a
// BuildSetupCommands' adds, the dead pair's teardown (RemoveDeadPair,
// main.cpp) runs them alone, and neither flushes (§12). Residual: a
// transport-mode policy with reqid 1 and the UE-side port 45061, or reqid
// 2 and 45062, alone or paired, carries the fingerprint and goes with the
// reqid-1/2 transport-mode ESP states in its direction, and a reqid-1/2