diff --git a/AGENTS.md b/AGENTS.md index 0eb0c29..6c5daf5 100644 --- a/AGENTS.md +++ b/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-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=` (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 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 diff --git a/implementations/main.cpp b/implementations/main.cpp index 809870b..25eadb4 100644 --- a/implementations/main.cpp +++ b/implementations/main.cpp @@ -114,6 +114,18 @@ std::string RedactKeys(const std::vector& 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>& 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 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 diff --git a/interfaces/Imsd-Ipsec.cppm b/interfaces/Imsd-Ipsec.cppm index 25eb0eb..54e52e2 100644 --- a/interfaces/Imsd-Ipsec.cppm +++ b/interfaces/Imsd-Ipsec.cppm @@ -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 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). +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 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 - // state+policy, add the four ESP states, add the four transport policies. - // Order matches imscall.setup_sa exactly. Callers validate p.alg with + // The full `ip xfrm` argv sequence to install a fresh SA pair: the four + // ESP states, then the four transport policies — no flush: the caller + // 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 // and is rejected there, never mapped to a default. std::vector> BuildSetupCommands(const SaParams& p) { @@ -127,8 +133,6 @@ export namespace imsd::ipsec { std::vector enc = detail::EncArgs(p); int plen = imsd::util::Is6(local) ? 128 : 32; std::vector> 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(pcscf, local, p.spiUc, 1, 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; } - std::vector> BuildTeardownCommands() { - return {{"ip", "xfrm", "state", "flush"}, {"ip", "xfrm", "policy", "flush"}}; - } - // ---- Security-Server (RFC 3329 §2.2) ----------------------------------- // // 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; 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. @@ -398,7 +413,14 @@ export namespace imsd::ipsec { if (src == p && dst == l && *reqid == 2) sa.spiUs = *spi; } 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 reqid = detail::ParseDec(detail::TokenAfter(b, "reqid ")); if (!dport || !reqid) continue; @@ -452,4 +474,57 @@ export namespace imsd::ipsec { } 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> 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> pairs; // (src, dst) of imsd's policies + std::vector> 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 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> 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; + } } diff --git a/tests/Ipsec/main.cpp b/tests/Ipsec/main.cpp index d7383d6..0f9a299 100644 --- a/tests/Ipsec/main.cpp +++ b/tests/Ipsec/main.cpp @@ -101,6 +101,54 @@ int main() { 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 { SaParams p; @@ -111,23 +159,21 @@ int main() { p.spiUc = 100; p.spiUs = 200; p.spiPc = 300; p.spiPs = 400; p.portUc = 45061; p.portUs = 45062; p.portPs = 6000; p.portPc = 33421; auto cmds = BuildSetupCommands(p); - Check(cmds.size() == 10, "flush x2 + 4 states + 4 policies"); - Check(Join(cmds[0]) == "ip xfrm state flush", "state flush first"); - Check(Join(cmds[1]) == "ip xfrm policy flush", "policy flush second"); + Check(cmds.size() == 8, "4 states + 4 policies, no flush (OwnXfrmDeleteCommands takes imsd's previous entries out)"); // 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 " "proto esp spi 400 mode transport reqid 1 " "auth-trunc hmac(sha1) 0xabababababababababababababababab 96 " "enc cbc(aes) 0xcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", "state add reqid1 outbound byte-exact"); // 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 SaParams p4 = p; p4.local = "10.0.0.2"; p4.pcscf = "10.0.0.1"; 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 { @@ -137,7 +183,7 @@ int main() { p.ck = {}; p.ealg = "null"; 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"); } @@ -149,7 +195,7 @@ int main() { p.ck = {}; p.alg = "hmac-md5-96"; p.ealg = "null"; 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(KnownEalg("null") && KnownEalg("aes-cbc") && KnownEalg("des-ede3-cbc") && !KnownEalg("aes-gcm") && !KnownEalg("") && !KnownEalg("3des"), "KnownEalg set (canonical names only)"); }