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:
parent
b60a3633c1
commit
21cb0f870d
4 changed files with 198 additions and 25 deletions
41
AGENTS.md
41
AGENTS.md
|
|
@ -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-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-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-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-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/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`. |
|
| `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
|
any failed resume), the `OnLink` read that writes the old registration off
|
||||||
when its address has left the netdev (`FreshRecover` after a failed move,
|
when its address has left the netdev (`FreshRecover` after a failed move,
|
||||||
`AddressGone`), and `PacketServiceUp`'s read of
|
`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
|
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
|
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
|
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`
|
- `Aka` — the AKAv1-MD5 digest, identity derivation, the `qmicli`/`mmcli`
|
||||||
text parsers, the AUTHENTICATE APDU and its response parser.
|
text parsers, the AUTHENTICATE APDU and its response parser.
|
||||||
- `Ipsec` — the exact `ip xfrm` argv lists for the SA pairs, the
|
- `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
|
- `Messages` — every outgoing SIP message byte-for-byte, cross-generated from
|
||||||
the Python reference stack imsd replaced.
|
the Python reference stack imsd replaced.
|
||||||
- `Sip` — the text layer's edge cases (empty header values, line anchoring,
|
- `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.
|
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`
|
`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
|
(§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
|
- `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
|
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
|
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
|
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
|
ungated write member, and do not add a root translation daemon that
|
||||||
forwards calls without gating its own callers through `imsd-bus`.
|
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;
|
- 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
|
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
|
policy that names an account breaks the moment the account is called
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,18 @@ std::string RedactKeys(const std::vector<std::string>& argv) {
|
||||||
return s;
|
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:") -------
|
// ---- small text extractors (401 header params, qmicli "completed:") -------
|
||||||
|
|
||||||
std::optional<std::string> QuotedAfter(std::string_view text, std::string_view key) {
|
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.portUc = PortUc; sp.portUs = PortUs; sp.portPs = portPs; sp.portPc = portPc;
|
||||||
sp.alg = alg;
|
sp.alg = alg;
|
||||||
sp.ealg = ealg;
|
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
|
saReplaced_ = true; // from here on the old registration's SAs are gone
|
||||||
for (auto& cmd : imsd::ipsec::BuildSetupCommands(sp)) {
|
for (auto& cmd : imsd::ipsec::BuildSetupCommands(sp)) {
|
||||||
int rc = RunCmd(cmd);
|
int rc = RunCmd(cmd);
|
||||||
|
|
@ -1501,7 +1520,7 @@ private:
|
||||||
if (autoTcp) {
|
if (autoTcp) {
|
||||||
// Two unanswered TCP connects: treat the P-CSCF as UDP-only
|
// Two unanswered TCP connects: treat the P-CSCF as UDP-only
|
||||||
// and register again over UDP — a new challenge and a new SA
|
// 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");
|
Log("protected TCP connect unanswered; registering fresh over UDP");
|
||||||
SetTransport(Transport::Udp);
|
SetTransport(Transport::Udp);
|
||||||
FreshRegisterAttempt();
|
FreshRegisterAttempt();
|
||||||
|
|
@ -1873,7 +1892,7 @@ private:
|
||||||
}
|
}
|
||||||
|
|
||||||
// The rest of what a restart does, in place: a new Call-ID, a new
|
// 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 post-registration steps. A refused or throttled attempt waits on
|
||||||
// the throttle schedule (a wait no Dial cuts short — on KPN a refused
|
// the throttle schedule (a wait no Dial cuts short — on KPN a refused
|
||||||
// attempt costs the old registration) and a silent one on the
|
// attempt costs the old registration) and a silent one on the
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,11 @@ 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
|
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
|
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
|
SA pair in the kernel (`ChooseResumeSecurityVerify` decides; since 0.3.12
|
||||||
the 401's own list is stored there).
|
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).
|
||||||
|
|
||||||
Pure std C++ (the shell runs the commands and captures the text). Command
|
Pure std C++ (the shell runs the commands and captures the text). Command
|
||||||
shapes and the parser are pinned by tests/Ipsec.
|
shapes and the parser are pinned by tests/Ipsec.
|
||||||
|
|
@ -114,9 +118,11 @@ export namespace imsd::ipsec {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The full `ip xfrm` argv sequence to install a fresh SA pair: flush
|
// The full `ip xfrm` argv sequence to install a fresh SA pair: the four
|
||||||
// state+policy, add the four ESP states, add the four transport policies.
|
// ESP states, then the four transport policies — no flush: the caller
|
||||||
// Order matches imscall.setup_sa exactly. Callers validate p.alg with
|
// takes imsd's previous entries out first (OwnXfrmDeleteCommands), so
|
||||||
|
// another IPsec user's stay. Order otherwise matches imscall.setup_sa.
|
||||||
|
// Callers validate p.alg with
|
||||||
// KernelAuth() first; an unknown alg goes to the kernel by its own name
|
// KernelAuth() first; an unknown alg goes to the kernel by its own name
|
||||||
// and is rejected there, never mapped to a default.
|
// and is rejected there, never mapped to a default.
|
||||||
std::vector<std::vector<std::string>> BuildSetupCommands(const SaParams& p) {
|
std::vector<std::vector<std::string>> BuildSetupCommands(const SaParams& p) {
|
||||||
|
|
@ -127,8 +133,6 @@ export namespace imsd::ipsec {
|
||||||
std::vector<std::string> enc = detail::EncArgs(p);
|
std::vector<std::string> enc = detail::EncArgs(p);
|
||||||
int plen = imsd::util::Is6(local) ? 128 : 32;
|
int plen = imsd::util::Is6(local) ? 128 : 32;
|
||||||
std::vector<std::vector<std::string>> cmds;
|
std::vector<std::vector<std::string>> cmds;
|
||||||
cmds.push_back({"ip", "xfrm", "state", "flush"});
|
|
||||||
cmds.push_back({"ip", "xfrm", "policy", "flush"});
|
|
||||||
cmds.push_back(detail::StateAdd(local, pcscf, p.spiPs, 1, auth, ikx, enc));
|
cmds.push_back(detail::StateAdd(local, pcscf, p.spiPs, 1, auth, ikx, enc));
|
||||||
cmds.push_back(detail::StateAdd(pcscf, local, p.spiUc, 1, auth, ikx, enc));
|
cmds.push_back(detail::StateAdd(pcscf, local, p.spiUc, 1, auth, ikx, enc));
|
||||||
cmds.push_back(detail::StateAdd(local, pcscf, p.spiPc, 2, auth, ikx, enc));
|
cmds.push_back(detail::StateAdd(local, pcscf, p.spiPc, 2, auth, ikx, enc));
|
||||||
|
|
@ -140,10 +144,6 @@ export namespace imsd::ipsec {
|
||||||
return cmds;
|
return cmds;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<std::vector<std::string>> BuildTeardownCommands() {
|
|
||||||
return {{"ip", "xfrm", "state", "flush"}, {"ip", "xfrm", "policy", "flush"}};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Security-Server (RFC 3329 §2.2) -----------------------------------
|
// ---- Security-Server (RFC 3329 §2.2) -----------------------------------
|
||||||
//
|
//
|
||||||
// The P-CSCF answers the challenge with the mechanisms it supports, each
|
// The P-CSCF answers the challenge with the mechanisms it supports, each
|
||||||
|
|
@ -367,6 +367,21 @@ export namespace imsd::ipsec {
|
||||||
if (ec != std::errc{} || p != tok.data() + tok.size()) return std::nullopt;
|
if (ec != std::errc{} || p != tok.data() + tok.size()) return std::nullopt;
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A policy block of imsd's own: transport mode, reqid 1 or 2, and the
|
||||||
|
// fixed protected port on the UE side — sport PortUc/PortUs on `dir
|
||||||
|
// out`, dport on `dir in` (BuildSetupCommands' PolicyAdd shape). Reqid
|
||||||
|
// and mode alone are anyone's (L2TP/IPsec, a host-to-host SA); the
|
||||||
|
// ports are the fingerprint. The one predicate every reader of the
|
||||||
|
// kernel's policy text uses.
|
||||||
|
inline bool OwnPolicy(std::string_view b) {
|
||||||
|
auto reqid = ParseDec(TokenAfter(b, "reqid "));
|
||||||
|
if (!reqid || (*reqid != 1 && *reqid != 2) || b.find("mode transport") == std::string_view::npos) return false;
|
||||||
|
const int port = *reqid == 1 ? imsd::util::PortUc : imsd::util::PortUs;
|
||||||
|
if (b.find("dir out") != std::string_view::npos) return ParseDec(TokenAfter(b, "sport ")) == port;
|
||||||
|
if (b.find("dir in") != std::string_view::npos) return ParseDec(TokenAfter(b, "dport ")) == port;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reconstruct SA facts from `ip xfrm state` + `ip xfrm policy` text.
|
// Reconstruct SA facts from `ip xfrm state` + `ip xfrm policy` text.
|
||||||
|
|
@ -398,7 +413,14 @@ export namespace imsd::ipsec {
|
||||||
if (src == p && dst == l && *reqid == 2) sa.spiUs = *spi;
|
if (src == p && dst == l && *reqid == 2) sa.spiUs = *spi;
|
||||||
}
|
}
|
||||||
for (std::string_view b : detail::SrcBlocks(policyText)) {
|
for (std::string_view b : detail::SrcBlocks(policyText)) {
|
||||||
if (b.find("dir out") == std::string_view::npos) continue;
|
if (b.find("dir out") == std::string_view::npos || !detail::OwnPolicy(b)) continue;
|
||||||
|
// the ports come from our own pair — a policy of imsd's own
|
||||||
|
// (detail::OwnPolicy) between our two addresses — never from a
|
||||||
|
// foreign policy that happens to carry ports
|
||||||
|
std::string_view srcTok = detail::TokenAfter(b, "src ");
|
||||||
|
std::string_view dstTok = detail::TokenAfter(b, "dst ");
|
||||||
|
if (imsd::util::CanonicalAddr(srcTok.substr(0, srcTok.find('/'))) != l) continue;
|
||||||
|
if (imsd::util::CanonicalAddr(dstTok.substr(0, dstTok.find('/'))) != p) continue;
|
||||||
auto dport = detail::ParseDec(detail::TokenAfter(b, "dport "));
|
auto dport = detail::ParseDec(detail::TokenAfter(b, "dport "));
|
||||||
auto reqid = detail::ParseDec(detail::TokenAfter(b, "reqid "));
|
auto reqid = detail::ParseDec(detail::TokenAfter(b, "reqid "));
|
||||||
if (!dport || !reqid) continue;
|
if (!dport || !reqid) continue;
|
||||||
|
|
@ -452,4 +474,57 @@ export namespace imsd::ipsec {
|
||||||
}
|
}
|
||||||
return locals;
|
return locals;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// fixed protected port on the UE side: the ports BuildSetupCommands
|
||||||
|
// installs), by the selector `ip xfrm policy` prints (src, dst, proto,
|
||||||
|
// sport, dport, dev) and its direction; and every transport-mode ESP state
|
||||||
|
// with reqid 1 or 2 running from such a policy's source to its destination,
|
||||||
|
// or anchored on `local`, the UE's ims-PDN address, by src, dst and SPI —
|
||||||
|
// policy or not: a setup whose policy add failed leaves such states, and
|
||||||
|
// an ESP one another transport-mode user put on that address with reqid 1
|
||||||
|
// or 2 goes with them (§12; an AH or IPComp state is not selected).
|
||||||
|
// 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
|
||||||
|
// 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
|
||||||
|
// transport-mode ESP state on `local` goes whoever made it (tests/Ipsec).
|
||||||
|
std::vector<std::vector<std::string>> OwnXfrmDeleteCommands(std::string_view stateText, std::string_view policyText, std::string_view local) {
|
||||||
|
auto addr = [](std::string_view tok) { return imsd::util::CanonicalAddr(tok.substr(0, tok.find('/'))); };
|
||||||
|
const std::string l = imsd::util::CanonicalAddr(local);
|
||||||
|
std::vector<std::pair<std::string, std::string>> pairs; // (src, dst) of imsd's policies
|
||||||
|
std::vector<std::vector<std::string>> policyDels;
|
||||||
|
for (std::string_view b : detail::SrcBlocks(policyText)) {
|
||||||
|
if (!detail::OwnPolicy(b)) continue;
|
||||||
|
const std::string_view src = detail::TokenAfter(b, "src ");
|
||||||
|
const std::string_view dst = detail::TokenAfter(b, "dst ");
|
||||||
|
pairs.emplace_back(addr(src), addr(dst));
|
||||||
|
// the selector line as `ip xfrm policy` prints it: src dst [proto] [sport] [dport] [dev] — every token
|
||||||
|
// present goes into the delete (the tmpl line's `proto esp` is not the selector's), none is emitted empty
|
||||||
|
const std::string_view sel = b.substr(0, b.find('\n'));
|
||||||
|
std::vector<std::string> del{"ip", "xfrm", "policy", "delete", "src", std::string(src), "dst", std::string(dst)};
|
||||||
|
for (const char* key : {"proto ", "sport ", "dport ", "dev "})
|
||||||
|
if (std::string_view v = detail::TokenAfter(sel, key); !v.empty()) { del.emplace_back(key, std::strlen(key) - 1); del.emplace_back(v); }
|
||||||
|
del.emplace_back("dir"); del.emplace_back(detail::TokenAfter(b, "dir "));
|
||||||
|
policyDels.push_back(std::move(del));
|
||||||
|
}
|
||||||
|
std::vector<std::vector<std::string>> cmds;
|
||||||
|
for (std::string_view b : detail::SrcBlocks(stateText)) {
|
||||||
|
if (b.find("proto esp ") == std::string_view::npos) continue; // the delete names proto esp: an AH or IPComp state is not this rule's
|
||||||
|
if (b.find("reqid 1 mode transport") == std::string_view::npos && b.find("reqid 2 mode transport") == std::string_view::npos) continue;
|
||||||
|
const std::string_view src = detail::TokenAfter(b, "src ");
|
||||||
|
const std::string_view dst = detail::TokenAfter(b, "dst ");
|
||||||
|
bool mine = !l.empty() && (addr(src) == l || addr(dst) == l);
|
||||||
|
for (const auto& p : pairs) if (p.first == addr(src) && p.second == addr(dst)) { mine = true; break; }
|
||||||
|
if (!mine) continue;
|
||||||
|
cmds.push_back({"ip", "xfrm", "state", "delete", "src", std::string(src), "dst", std::string(dst), "proto", "esp", "spi", std::string(detail::TokenAfter(b, "spi "))});
|
||||||
|
}
|
||||||
|
cmds.insert(cmds.end(), policyDels.begin(), policyDels.end());
|
||||||
|
return cmds;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,54 @@ int main() {
|
||||||
Check(SaLocals(State, "2001:db8::999").empty(), "SaLocals: no SA to that P-CSCF -> none");
|
Check(SaLocals(State, "2001:db8::999").empty(), "SaLocals: no SA to that P-CSCF -> none");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- OwnXfrmDeleteCommands: imsd's own entries out of the kernel by exact key; everything else stays
|
||||||
|
{
|
||||||
|
const std::string ue = "2001:db8:1111:2222:3333:4444:5555:db43";
|
||||||
|
// a policy-based VPN's: a tunnel policy (v6, reqid 1), an IPv4 one to 0.0.0.0/0, a tunnel SA
|
||||||
|
const std::string vpn = "src 2001:db8::1/128 dst 2001:db8::2/128 \n\tdir out priority 0 \n\ttmpl src 2001:db8::1 dst 2001:db8::2\n\t\tproto esp reqid 1 mode tunnel\n";
|
||||||
|
const std::string vpn4 = "src 192.0.2.10/32 dst 0.0.0.0/0 \n\tdir out priority 383615 ptype main \n\ttmpl src 192.0.2.10 dst 192.0.2.1\n\t\tproto esp reqid 1 mode tunnel\n";
|
||||||
|
const std::string tunState = "src 2001:db8::1 dst 2001:db8::2\n\tproto esp spi 0x00000777 reqid 7 mode tunnel\n\treplay-window 32 \n";
|
||||||
|
// another transport-mode user's reqid 1 + 2 pair on other ports, with its SA between other addresses
|
||||||
|
const std::string otherOut2 = "src " + ue + "/128 dst 2001:db8::106/128 sport 1 dport 2 \n\tdir out priority 0 \n\ttmpl src " + ue + " dst 2001:db8::106\n\t\tproto esp reqid 2 mode transport\n";
|
||||||
|
const std::string otherOut1 = "src " + ue + "/128 dst 2001:db8::106/128 sport 3 dport 4 \n\tdir out priority 0 \n\ttmpl src " + ue + " dst 2001:db8::106\n\t\tproto esp reqid 1 mode transport\n";
|
||||||
|
const std::string otherState = "src 2001:db8::7 dst 2001:db8::106\n\tproto esp spi 0x00000106 reqid 2 mode transport\n\treplay-window 0 \n";
|
||||||
|
auto dels = OwnXfrmDeleteCommands(State, Policy, ue);
|
||||||
|
Check(dels.size() == 8, "OwnXfrmDeleteCommands: our four states and four policies -> 8 deletes");
|
||||||
|
Check(dels.size() == 8 && Join(dels[0]) == "ip xfrm state delete src 2001:db8::105 dst " + ue + " proto esp spi 0x0a84ab2f", "OwnXfrmDeleteCommands: a state by src, dst and SPI, byte-exact");
|
||||||
|
Check(dels.size() == 8 && Join(dels[4]) == "ip xfrm policy delete src 2001:db8::105/128 dst " + ue + "/128 sport 33421 dport 45062 dir in", "OwnXfrmDeleteCommands: a policy by its selector and direction, byte-exact");
|
||||||
|
Check(dels.size() == 8 && Join(dels[1]) == "ip xfrm state delete src " + ue + " dst 2001:db8::105 proto esp spi 0x0526f2c1" && Join(dels[2]) == "ip xfrm state delete src 2001:db8::105 dst " + ue + " proto esp spi 0x0071acd4" && Join(dels[3]) == "ip xfrm state delete src " + ue + " dst 2001:db8::105 proto esp spi 0x0526f2c0",
|
||||||
|
"OwnXfrmDeleteCommands: the other three states byte-exact, in the kernel's order");
|
||||||
|
Check(dels.size() == 8 && Join(dels[5]) == "ip xfrm policy delete src " + ue + "/128 dst 2001:db8::105/128 sport 45062 dport 33421 dir out" && Join(dels[6]) == "ip xfrm policy delete src 2001:db8::105/128 dst " + ue + "/128 sport 6000 dport 45061 dir in" && Join(dels[7]) == "ip xfrm policy delete src " + ue + "/128 dst 2001:db8::105/128 sport 45061 dport 6000 dir out",
|
||||||
|
"OwnXfrmDeleteCommands: the other three policies byte-exact, in the kernel's order");
|
||||||
|
auto besideVpn = OwnXfrmDeleteCommands(std::string(State) + tunState, std::string(Policy) + vpn + vpn4, ue);
|
||||||
|
Check(besideVpn.size() == 8 && std::ranges::none_of(besideVpn, [](const auto& c) { auto j = Join(c); return j.contains("2001:db8::2") || j.contains("192.0.2.10") || j.contains("0x00000777"); }),
|
||||||
|
"OwnXfrmDeleteCommands: a VPN's tunnel SA and two tunnel policies beside ours -> ours only, theirs untouched");
|
||||||
|
Check(OwnXfrmDeleteCommands(otherState, otherOut2 + otherOut1, ue).empty(), "OwnXfrmDeleteCommands: another transport-mode reqid 1 + 2 pair on other ports, with its SA between other addresses -> nothing deleted");
|
||||||
|
Check(OwnXfrmDeleteCommands(std::string(State) + otherState, std::string(Policy) + otherOut2 + otherOut1, ue).size() == 8, "OwnXfrmDeleteCommands: the same beside our pair -> ours only");
|
||||||
|
Check(OwnXfrmDeleteCommands("", "", ue).empty(), "OwnXfrmDeleteCommands: nothing -> nothing");
|
||||||
|
Check(OwnXfrmDeleteCommands(State, "", ue).size() == 4, "OwnXfrmDeleteCommands: policy-less states of imsd's on the UE's address are deleted (a setup whose policy add failed leaves them)");
|
||||||
|
Check(OwnXfrmDeleteCommands(otherState, "", ue).empty(), "OwnXfrmDeleteCommands: a policy-less state between other addresses is left");
|
||||||
|
// the fingerprint selects, not a pair: a lone transport-mode reqid-1 policy carrying the protected port between other addresses goes, with the reqid-1 state between them (the §12 residual)
|
||||||
|
const std::string loneOther = "src 2001:db8::7/128 dst 2001:db8::106/128 sport 45061 dport 4 \n\tdir out priority 0 \n\ttmpl src 2001:db8::7 dst 2001:db8::106\n\t\tproto esp reqid 1 mode transport\n";
|
||||||
|
const std::string loneOtherState = "src 2001:db8::7 dst 2001:db8::106\n\tproto esp spi 0x00000107 reqid 1 mode transport\n\treplay-window 0 \n";
|
||||||
|
Check(OwnXfrmDeleteCommands(loneOtherState, loneOther, ue).size() == 2, "OwnXfrmDeleteCommands: a lone policy carrying the fingerprint between other addresses, and the reqid-1 state in its direction, are deleted (the fingerprint selects, not a pair)");
|
||||||
|
// a reqid-1/2 transport-mode state on the UE's address goes whoever made it: the other user's SA between the UE address and its peer, its policies on other ports staying (the §12 residual)
|
||||||
|
const std::string otherOnUe = "src " + ue + " dst 2001:db8::106\n\tproto esp spi 0x00000108 reqid 1 mode transport\n\treplay-window 0 \n";
|
||||||
|
auto onUe = OwnXfrmDeleteCommands(otherOnUe, otherOut2 + otherOut1, ue);
|
||||||
|
Check(onUe.size() == 1 && Join(onUe[0]) == "ip xfrm state delete src " + ue + " dst 2001:db8::106 proto esp spi 0x00000108", "OwnXfrmDeleteCommands: another transport-mode user's reqid-1 state on the UE's address is deleted whoever made it; its policies on other ports stay (the §12 residual)");
|
||||||
|
Check(OwnXfrmDeleteCommands("src " + ue + " dst 2001:db8::106\n\tproto ah spi 0x00000109 reqid 1 mode transport\n\treplay-window 0 \n", otherOut2 + otherOut1, ue).empty(), "OwnXfrmDeleteCommands: an AH reqid-1 transport state on the UE's address is not selected (the delete names proto esp)");
|
||||||
|
// the selector as `ip xfrm policy` prints it: a fingerprinted policy with a protocol (and a dev) keeps them in its delete, a missing port is no empty token
|
||||||
|
const std::string protoOther = "src 2001:db8::7/128 dst 2001:db8::106/128 proto udp sport 45061 dport 4 \n\tdir out priority 0 \n\ttmpl src 2001:db8::7 dst 2001:db8::106\n\t\tproto esp reqid 1 mode transport\n";
|
||||||
|
auto protoDels = OwnXfrmDeleteCommands("", protoOther, ue);
|
||||||
|
Check(protoDels.size() == 1 && Join(protoDels[0]) == "ip xfrm policy delete src 2001:db8::7/128 dst 2001:db8::106/128 proto udp sport 45061 dport 4 dir out", "OwnXfrmDeleteCommands: a fingerprinted policy with a protocol in its selector is deleted by that selector (proto kept; the tmpl's 'proto esp' is not the selector's)");
|
||||||
|
const std::string portlessOther = "src 2001:db8::7/128 dst 2001:db8::106/128 sport 45061 dev qmapmux0.0 \n\tdir out priority 0 \n\ttmpl src 2001:db8::7 dst 2001:db8::106\n\t\tproto esp reqid 1 mode transport\n";
|
||||||
|
auto portlessDels = OwnXfrmDeleteCommands("", portlessOther, ue);
|
||||||
|
Check(portlessDels.size() == 1 && Join(portlessDels[0]) == "ip xfrm policy delete src 2001:db8::7/128 dst 2001:db8::106/128 sport 45061 dev qmapmux0.0 dir out", "OwnXfrmDeleteCommands: a selector without a dport and with a dev -> no empty token, the dev kept");
|
||||||
|
// the SA reader's ports come from our pair, never from a foreign transport policy that happens to carry ports
|
||||||
|
auto saBeside = ParseExistingSa(State, std::string(Policy) + otherOut2, "2001:db8::105", ue);
|
||||||
|
Check(saBeside && saBeside->portPc == 33421 && saBeside->portPs == 6000, "ParseExistingSa: a foreign transport policy's ports beside ours are not taken");
|
||||||
|
}
|
||||||
|
|
||||||
// ---- fresh setup command sequence
|
// ---- fresh setup command sequence
|
||||||
{
|
{
|
||||||
SaParams p;
|
SaParams p;
|
||||||
|
|
@ -111,23 +159,21 @@ int main() {
|
||||||
p.spiUc = 100; p.spiUs = 200; p.spiPc = 300; p.spiPs = 400;
|
p.spiUc = 100; p.spiUs = 200; p.spiPc = 300; p.spiPs = 400;
|
||||||
p.portUc = 45061; p.portUs = 45062; p.portPs = 6000; p.portPc = 33421;
|
p.portUc = 45061; p.portUs = 45062; p.portPs = 6000; p.portPc = 33421;
|
||||||
auto cmds = BuildSetupCommands(p);
|
auto cmds = BuildSetupCommands(p);
|
||||||
Check(cmds.size() == 10, "flush x2 + 4 states + 4 policies");
|
Check(cmds.size() == 8, "4 states + 4 policies, no flush (OwnXfrmDeleteCommands takes imsd's previous entries out)");
|
||||||
Check(Join(cmds[0]) == "ip xfrm state flush", "state flush first");
|
|
||||||
Check(Join(cmds[1]) == "ip xfrm policy flush", "policy flush second");
|
|
||||||
// reqid1 outbound state: src LOCAL dst P spi=spiPs, IK auth, CK enc
|
// reqid1 outbound state: src LOCAL dst P spi=spiPs, IK auth, CK enc
|
||||||
Check(Join(cmds[2]) ==
|
Check(Join(cmds[0]) ==
|
||||||
"ip xfrm state add src 2001:db8::db43 dst 2001:db8::105 "
|
"ip xfrm state add src 2001:db8::db43 dst 2001:db8::105 "
|
||||||
"proto esp spi 400 mode transport reqid 1 "
|
"proto esp spi 400 mode transport reqid 1 "
|
||||||
"auth-trunc hmac(sha1) 0xabababababababababababababababab 96 "
|
"auth-trunc hmac(sha1) 0xabababababababababababababababab 96 "
|
||||||
"enc cbc(aes) 0xcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd",
|
"enc cbc(aes) 0xcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd",
|
||||||
"state add reqid1 outbound byte-exact");
|
"state add reqid1 outbound byte-exact");
|
||||||
// first out policy: sport PORT_UC dport PORT_PS reqid 1
|
// first out policy: sport PORT_UC dport PORT_PS reqid 1
|
||||||
Check(Join(cmds[6]) == "ip xfrm policy add src 2001:db8::db43/128 dst 2001:db8::105/128 " "sport 45061 dport 6000 dir out tmpl src 2001:db8::db43 " "dst 2001:db8::105 proto esp reqid 1 mode transport", "policy add out reqid1 byte-exact");
|
Check(Join(cmds[4]) == "ip xfrm policy add src 2001:db8::db43/128 dst 2001:db8::105/128 " "sport 45061 dport 6000 dir out tmpl src 2001:db8::db43 " "dst 2001:db8::105 proto esp reqid 1 mode transport", "policy add out reqid1 byte-exact");
|
||||||
// v4 uses /32
|
// v4 uses /32
|
||||||
SaParams p4 = p;
|
SaParams p4 = p;
|
||||||
p4.local = "10.0.0.2"; p4.pcscf = "10.0.0.1";
|
p4.local = "10.0.0.2"; p4.pcscf = "10.0.0.1";
|
||||||
auto c4 = BuildSetupCommands(p4);
|
auto c4 = BuildSetupCommands(p4);
|
||||||
Check(c4[6][5] == "10.0.0.2/32", "v4 prefix length /32");
|
Check(c4[4][5] == "10.0.0.2/32", "v4 prefix length /32");
|
||||||
}
|
}
|
||||||
// null cipher -> cipher_null + empty key arg
|
// null cipher -> cipher_null + empty key arg
|
||||||
{
|
{
|
||||||
|
|
@ -137,7 +183,7 @@ int main() {
|
||||||
p.ck = {};
|
p.ck = {};
|
||||||
p.ealg = "null";
|
p.ealg = "null";
|
||||||
auto cmds = BuildSetupCommands(p);
|
auto cmds = BuildSetupCommands(p);
|
||||||
auto& st = cmds[2];
|
auto& st = cmds[0];
|
||||||
Check(st[st.size() - 3] == "enc" && st[st.size() - 2] == "cipher_null" && st[st.size() - 1].empty(), "null cipher: cipher_null + empty key");
|
Check(st[st.size() - 3] == "enc" && st[st.size() - 2] == "cipher_null" && st[st.size() - 1].empty(), "null cipher: cipher_null + empty key");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -149,7 +195,7 @@ int main() {
|
||||||
p.ck = {};
|
p.ck = {};
|
||||||
p.alg = "hmac-md5-96"; p.ealg = "null";
|
p.alg = "hmac-md5-96"; p.ealg = "null";
|
||||||
auto cmds = BuildSetupCommands(p);
|
auto cmds = BuildSetupCommands(p);
|
||||||
Check(cmds[2][17] == "hmac(md5)" && cmds[2][18] == "0x01010101010101010101010101010101" && cmds[2][19] == "96", "md5: auth-trunc hmac(md5) IK 96");
|
Check(cmds[0][17] == "hmac(md5)" && cmds[0][18] == "0x01010101010101010101010101010101" && cmds[0][19] == "96", "md5: auth-trunc hmac(md5) IK 96");
|
||||||
Check(KernelAuth("hmac-sha-1-96") == "hmac(sha1)" && KernelAuth("hmac-md5-96") == "hmac(md5)" && !KernelAuth("hmac-sha-256-128"), "KernelAuth names");
|
Check(KernelAuth("hmac-sha-1-96") == "hmac(sha1)" && KernelAuth("hmac-md5-96") == "hmac(md5)" && !KernelAuth("hmac-sha-256-128"), "KernelAuth names");
|
||||||
Check(KnownEalg("null") && KnownEalg("aes-cbc") && KnownEalg("des-ede3-cbc") && !KnownEalg("aes-gcm") && !KnownEalg("") && !KnownEalg("3des"), "KnownEalg set (canonical names only)");
|
Check(KnownEalg("null") && KnownEalg("aes-cbc") && KnownEalg("des-ede3-cbc") && !KnownEalg("aes-gcm") && !KnownEalg("") && !KnownEalg("3des"), "KnownEalg set (canonical names only)");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue