engine, messages, util, imsd: the reregistration carries the last challenge answer and new SA parameters
The keepalive refresh and the resume's re-REGISTER carried an initial registration's first-REGISTER Authorization — an empty nonce and response — in the second REGISTER's frame, with a Security-Client naming the SPIs and ports in use. TS 24.229 5.1.1.4.2 has a reregistration carry the Authorization of the last successful authentication — the last received nonce and the last calculated response — and a Security-Client with the new parameter values for the two SA pairs a re-authentication would set up; TS 33.203 7.4.0 keeps the server ports and changes the client ports. O2 UK refused the refresh of the old shape with 503 on every occasion on record (alyx, 2026-09-24 to 10-08, a stationary phone included) and accepted the fresh registrations that followed; KPN accepted the old shape without a challenge. The fresh registration now records the 401's nonce, our cnonce and the response in the registration state (auth_nonce, auth_cnonce, auth_response in the state file); a reregistration sends them verbatim, mints new SPIs and proposes PortUcNext (45063) as the client port with the server port unchanged. A registration resumed from a state file written before this change has no answer: its reregistrations carry the empty digest, as before, until its next fresh registration, and the resume says so. A 401 to a reregistration asks for re-authentication over temporary SAs on the proposed ports (TS 24.229 5.1.1.5.1), which imsd does not implement: the refresh throws it as a refusal and RegRecovery makes the next step the fresh registration, replacing the in-place answer over the old SAs. Tested with the Messages and Engine suites and on the dev phone (KPN) with a build of this commit's tree: a resume from the previous state file, a fresh registration, a resume that sent the stored answer, three refreshes at a 120-s interval, an outgoing call to a bench line and an incoming call's ring.
This commit is contained in:
parent
a00b14a5a9
commit
799fd1a802
8 changed files with 185 additions and 72 deletions
|
|
@ -60,7 +60,9 @@ export namespace imsd::engine {
|
|||
// bring-up, as a pure reducer: the shell supplies the clock and the
|
||||
// network, this decides the next step and how long to wait for it. Two
|
||||
// kinds of failure reach it. A REFUSAL: the network answered a refresh
|
||||
// (or a fresh REGISTER) with a final other than 200/401. SILENCE: the
|
||||
// with a final other than 200 — a 401, re-authentication, included — or
|
||||
// a fresh registration's protected REGISTER with a final other than 200
|
||||
// (the unprotected one's 401 is its challenge). SILENCE: the
|
||||
// flow died and reconnecting through the SA, or the refresh over it,
|
||||
// got no answer. Neither is retried for long on the same registration.
|
||||
// TS 24.229 5.1.1.4.1 has the UE perform an initial registration after
|
||||
|
|
@ -79,12 +81,15 @@ export namespace imsd::engine {
|
|||
// a fresh registration after two (one 503 may be a momentary hiccup a
|
||||
// retried refresh clears); one with such a Retry-After escalates at
|
||||
// once — it is the network's considered answer (O2 2026-10-08: the
|
||||
// refresh after the first wait was refused again, 414 s then 772 s).
|
||||
// Silence escalates after two reconnect cycles
|
||||
// refresh after the first wait was refused again, 414 s then 772 s) —
|
||||
// and so does a 401, the network asking to re-authenticate, which imsd
|
||||
// answers with a fresh registration (TS 24.229 5.1.1.5.1's temporary
|
||||
// SAs on the proposed client port are not implemented). Silence
|
||||
// escalates after two reconnect cycles
|
||||
// that end without an answer — a TCP connect series whose last attempt
|
||||
// was not a local refusal, a bind() the kernel refused, or a refresh
|
||||
// that fails short of a refusal: unanswered, an unusable challenge, a
|
||||
// failed send (the resume at start gives up after two unanswered
|
||||
// that fails short of a refusal: unanswered, a failed send (the resume
|
||||
// at start gives up after two unanswered
|
||||
// connects).
|
||||
// A fresh registration is the expensive step: an AKA run, a
|
||||
// new SA pair, and on KPN a refusal (spi-s=0) while the registration on
|
||||
|
|
@ -191,7 +196,7 @@ export namespace imsd::engine {
|
|||
// cycle; `status` is the refusal's. The backoff paces the next step;
|
||||
// a Retry-After of at least a second holds every attempt for its own
|
||||
// length (up to RetryAfterMax), whichever is longer, and makes the
|
||||
// next step the fresh registration. A 403 once a fresh
|
||||
// next step the fresh registration, as does a 401. A 403 once a fresh
|
||||
// registration is due says the old registration is gone (KPN drops
|
||||
// it the moment an initial REGISTER arrives, throttled or not).
|
||||
void OnReconnectFailure(double now, Failure f, long retryAfter = 0, int status = 0) {
|
||||
|
|
@ -204,7 +209,7 @@ export namespace imsd::engine {
|
|||
if (f == Failure::Refused) refusals++;
|
||||
else silentCycles++;
|
||||
if (refusals >= RefusalsBeforeFresh || silentCycles >= SilentCyclesBeforeFresh) fresh = true;
|
||||
if (f == Failure::Refused && retryAfter > 0) fresh = true;
|
||||
if (f == Failure::Refused && (retryAfter > 0 || status == 401)) fresh = true;
|
||||
if (status == 403 && fresh) oldGone = true;
|
||||
backoffUntil = now + reconnectDelay;
|
||||
reconnectDelay = std::min(reconnectDelay * 2, ReconnectDelayMax);
|
||||
|
|
|
|||
|
|
@ -14,8 +14,10 @@ negotiated ealg keyed with CK. Both come from the Security-Server mechanism
|
|||
the P-CSCF selected in its 401 (parsed here), and this module turns the
|
||||
negotiated SPIs/ports/keys into the exact argv lists the daemon shell hands to
|
||||
`ip`; for a warm resume it reconstructs the SPIs, ports, and Security-Server
|
||||
value back out of `ip xfrm state`/`policy` text so a refresh re-REGISTER can
|
||||
advertise the SPIs actually in use.
|
||||
value back out of `ip xfrm state`/`policy` text — the Security-Verify and
|
||||
the P-CSCF ports for the resume's flow, and the SPIs in use, which a
|
||||
reregistration's new SPIs must differ from (and the state file falls back
|
||||
to).
|
||||
|
||||
Pure std C++ (the shell runs the commands and captures the text). Command
|
||||
shapes and the parser are pinned by tests/Ipsec.
|
||||
|
|
@ -309,7 +311,7 @@ export namespace imsd::ipsec {
|
|||
std::string securityServer; // reconstructed Security-Server header value
|
||||
int portPs = 0;
|
||||
int portPc = 0;
|
||||
std::uint32_t spiUc = 0; // our inbound client SPI (advertise on refresh)
|
||||
std::uint32_t spiUc = 0; // our inbound client SPI (a reregistration offers a different one)
|
||||
std::uint32_t spiUs = 0; // our inbound server SPI
|
||||
std::uint32_t spiPs = 0;
|
||||
std::uint32_t spiPc = 0;
|
||||
|
|
|
|||
|
|
@ -88,9 +88,15 @@ export namespace imsd::msg {
|
|||
return std::format("Via: SIP/2.0/{} {};branch=z9hG4bK{};rport", transport, imsd::util::HostPort(c.local, port), branch);
|
||||
}
|
||||
|
||||
// Security-Client value carrying our in-use client SPIs + protected ports.
|
||||
// Security-Client value: the SPIs and protected ports offered for the SA
|
||||
// pairs this REGISTER may set up — the in-use ones on an initial
|
||||
// registration, new SPIs and a new client port on a reregistration
|
||||
// (TS 24.229 5.1.1.4.2 d; TS 33.203 7.4.0: the server port stays).
|
||||
inline std::string SecurityClient(const Context& c, std::uint32_t spiUc, std::uint32_t spiUs, int portUc, int portUs) {
|
||||
return std::format("Security-Client: ipsec-3gpp; alg=hmac-sha-1-96; ealg={}; " "spi-c={}; spi-s={}; port-c={}; port-s={}", c.ealgOffer, spiUc, spiUs, portUc, portUs);
|
||||
}
|
||||
inline std::string SecurityClient(const Context& c, std::uint32_t spiUc, std::uint32_t spiUs) {
|
||||
return std::format("Security-Client: ipsec-3gpp; alg=hmac-sha-1-96; ealg={}; " "spi-c={}; spi-s={}; port-c={}; port-s={}", c.ealgOffer, spiUc, spiUs, c.portUc, c.portUs);
|
||||
return SecurityClient(c, spiUc, spiUs, c.portUc, c.portUs);
|
||||
}
|
||||
|
||||
// Authorization line with an empty (unchallenged) digest — the first
|
||||
|
|
@ -217,9 +223,17 @@ export namespace imsd::msg {
|
|||
// The stock oracle sends it in BOTH REGISTERs (2026-07-21 capture,
|
||||
// reg2 build at 19:30:44); an earlier reading that reg2 omits it was
|
||||
// wrong and cost us all terminating delivery. `authLine` is a full
|
||||
// Authorization line (AuthEmpty for the refresh's first try, AuthAka
|
||||
// once challenged).
|
||||
std::string BuildRegisterProtected(const Context& c, std::string_view callid, std::string_view ftag, std::string_view branch, int cseq, std::uint32_t spiUc, std::uint32_t spiUs, std::string_view authLine) {
|
||||
// Authorization line: AuthAka with the challenge's answer for the
|
||||
// initial registration's second REGISTER and, verbatim, for every
|
||||
// reregistration after it (TS 24.229 5.1.1.4.2 a: the last received
|
||||
// nonce, the last calculated response); AuthEmpty for a registration
|
||||
// resumed from a pre-0.3.11 state file, on every reregistration until
|
||||
// its next fresh registration. A
|
||||
// reregistration also passes NEW SPIs and the client port it proposes
|
||||
// for a re-authentication's SA pairs (`portUc`, TS 33.203 7.4.0: the
|
||||
// server port stays); 0 offers the in-use client port, the initial
|
||||
// registration's case.
|
||||
std::string BuildRegisterProtected(const Context& c, std::string_view callid, std::string_view ftag, std::string_view branch, int cseq, std::uint32_t spiUc, std::uint32_t spiUs, std::string_view authLine, int portUc = 0) {
|
||||
std::vector<std::string> l = {
|
||||
std::format("REGISTER {} SIP/2.0", c.id.regUri),
|
||||
ViaLine(c, c.transport, c.portUs, branch),
|
||||
|
|
@ -233,7 +247,7 @@ export namespace imsd::msg {
|
|||
std::string(authLine),
|
||||
"Require: sec-agree",
|
||||
"Proxy-Require: sec-agree",
|
||||
SecurityClient(c, spiUc, spiUs),
|
||||
SecurityClient(c, spiUc, spiUs, portUc ? portUc : c.portUc, c.portUs),
|
||||
std::format("Security-Verify: {}", c.securityServer),
|
||||
};
|
||||
l.push_back("Supported: sec-agree, path");
|
||||
|
|
|
|||
|
|
@ -24,6 +24,12 @@ export namespace imsd::util {
|
|||
// an installed package's nftables accept rules.
|
||||
constexpr int PortUc = 45061;
|
||||
constexpr int PortUs = 45062;
|
||||
// The client port a reregistration proposes for the SA pairs a
|
||||
// re-authentication would set up: TS 33.203 7.4.0 keeps the server
|
||||
// ports (port_us, port_ps) and changes the client ports (port_uc,
|
||||
// port_pc). imsd answers such a challenge with a fresh registration, so
|
||||
// nothing is bound here and the firewall rules need not open it.
|
||||
constexpr int PortUcNext = 45063;
|
||||
|
||||
// An address is IPv6 iff it contains a colon (matches the Python is6()).
|
||||
inline bool Is6(std::string_view addr) { return addr.contains(':'); }
|
||||
|
|
|
|||
Loading…
Reference in a new issue