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
|
|
@ -54,6 +54,7 @@ constexpr const char* ObjPath = "/net/catcrafts/IMS1";
|
|||
constexpr const char* Iface = "net.catcrafts.IMS1";
|
||||
constexpr int PortUc = imsd::util::PortUc;
|
||||
constexpr int PortUs = imsd::util::PortUs;
|
||||
constexpr int PortUcNext = imsd::util::PortUcNext;
|
||||
constexpr int InitPort = 5060;
|
||||
|
||||
std::string EnvOr(const char* k, std::string d) {
|
||||
|
|
@ -296,10 +297,12 @@ constexpr std::string_view TransportName(Transport t) { return t == Transport::U
|
|||
// live). Handled in-process: a
|
||||
// systemd restart every RestartSec would never clear it.
|
||||
struct ThrottledError : std::runtime_error { using std::runtime_error::runtime_error; };
|
||||
// A REGISTER the network refused: the protected one, or a refresh, answered
|
||||
// with a final other than 200 (a refresh's first 401 is its challenge); the
|
||||
// unprotected initial one answered with a final from 300 up (its 401 is the
|
||||
// challenge). The status and its Retry-After feed imsd::engine::RegRecovery.
|
||||
// A REGISTER the network refused: the protected one, or a reregistration,
|
||||
// answered with a final other than 200 (a reregistration's 401 asks for a
|
||||
// re-authentication imsd does not do over the old SAs: a refusal carrying
|
||||
// 401); the unprotected initial one answered with a final from 300 up (its
|
||||
// 401 is the challenge). The status and its Retry-After feed
|
||||
// imsd::engine::RegRecovery.
|
||||
struct RefusedError : std::runtime_error {
|
||||
int status;
|
||||
long retryAfter;
|
||||
|
|
@ -635,6 +638,12 @@ struct RegState {
|
|||
// Contact user-part of the stored binding (UUID since the s53 oracle
|
||||
// diff; empty = legacy IMSI binding from an older state file).
|
||||
std::string contactUser;
|
||||
// The answer to the last challenge — the 401's nonce, our cnonce and
|
||||
// the response — which every reregistration carries verbatim (TS
|
||||
// 24.229 5.1.1.4.2 a). Empty = a registration resumed from a pre-0.3.11
|
||||
// state file; its reregistrations carry an empty digest, the pre-0.3.11
|
||||
// shape, until the next fresh registration.
|
||||
std::string authNonce, authCnonce, authResponse;
|
||||
};
|
||||
|
||||
// Writable directory for the daemon's own files: the persisted registration
|
||||
|
|
@ -653,9 +662,10 @@ void PersistState(const RegState& r, const std::string& route, const std::string
|
|||
std::string j = std::format(
|
||||
"{{\"callid\": \"{}\", \"ftag\": \"{}\", \"cseq\": {}, \"spi_uc\": {}, "
|
||||
"\"spi_us\": {}, \"expiry\": {}, \"route\": \"{}\", \"ppi\": \"{}\", "
|
||||
"\"aor\": \"{}\", \"contact_user\": \"{}\", \"transport\": \"{}\"}}",
|
||||
"\"aor\": \"{}\", \"contact_user\": \"{}\", \"transport\": \"{}\", "
|
||||
"\"auth_nonce\": \"{}\", \"auth_cnonce\": \"{}\", \"auth_response\": \"{}\"}}",
|
||||
r.callid, r.ftag, r.cseq, r.spiUc, r.spiUs, r.expiry, route, ppi, aor,
|
||||
r.contactUser, transport);
|
||||
r.contactUser, transport, r.authNonce, r.authCnonce, r.authResponse);
|
||||
std::string path = StatePath();
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(std::filesystem::path(path).parent_path(), ec);
|
||||
|
|
@ -679,7 +689,7 @@ void DumpRaw(std::string_view name, std::string_view msg) {
|
|||
}
|
||||
|
||||
struct PersistedState {
|
||||
std::optional<std::string> callid, ftag, route, ppi, aor, contactUser, transport;
|
||||
std::optional<std::string> callid, ftag, route, ppi, aor, contactUser, transport, authNonce, authCnonce, authResponse;
|
||||
std::optional<long> cseq, spiUc, spiUs, expiry;
|
||||
};
|
||||
std::optional<PersistedState> LoadState() {
|
||||
|
|
@ -697,6 +707,9 @@ std::optional<PersistedState> LoadState() {
|
|||
ps.aor = QuotedAfter(s, "\"aor\": \"");
|
||||
ps.contactUser = QuotedAfter(s, "\"contact_user\": \"");
|
||||
ps.transport = QuotedAfter(s, "\"transport\": \""); // absent in pre-0.3.5 files: TCP
|
||||
ps.authNonce = QuotedAfter(s, "\"auth_nonce\": \""); // the three absent in pre-0.3.11 files
|
||||
ps.authCnonce = QuotedAfter(s, "\"auth_cnonce\": \"");
|
||||
ps.authResponse = QuotedAfter(s, "\"auth_response\": \"");
|
||||
ps.cseq = IntAfter(s, "\"cseq\": ");
|
||||
ps.spiUc = IntAfter(s, "\"spi_uc\": ");
|
||||
ps.spiUs = IntAfter(s, "\"spi_us\": ");
|
||||
|
|
@ -1295,7 +1308,9 @@ private:
|
|||
reg_.spiUc = static_cast<std::uint32_t>(ps->spiUc.value_or(sa->spiUc));
|
||||
reg_.spiUs = static_cast<std::uint32_t>(ps->spiUs.value_or(sa->spiUs));
|
||||
reg_.expiry = ps->expiry.value_or(0);
|
||||
auto [okMsg, used] = Reregister(reg_.callid, reg_.ftag, reg_.cseq + 1, reg_.spiUc, reg_.spiUs);
|
||||
reg_.authNonce = ps->authNonce.value_or(""); reg_.authCnonce = ps->authCnonce.value_or(""); reg_.authResponse = ps->authResponse.value_or("");
|
||||
if (reg_.authNonce.empty()) Log("state file holds no challenge answer (pre-0.3.11): the refresh carries an empty digest");
|
||||
auto [okMsg, used] = Reregister(reg_.callid, reg_.ftag, reg_.cseq + 1);
|
||||
reg_.cseq = used;
|
||||
if (auto e = imsd::sip::GrantedExpires(okMsg)) reg_.expiry = *e;
|
||||
UpdateRouteAndPpi(okMsg);
|
||||
|
|
@ -1461,6 +1476,7 @@ private:
|
|||
reg_.callid = callidReg; reg_.ftag = ftag; reg_.cseq = 2;
|
||||
reg_.contactUser = contactUser;
|
||||
reg_.spiUc = spiUc; reg_.spiUs = spiUs;
|
||||
reg_.authNonce = *nonceB64; reg_.authCnonce = cnonce; reg_.authResponse = response;
|
||||
reg_.expiry = imsd::sip::GrantedExpires(ok).value_or(0);
|
||||
DumpRaw("imsd-register-200.raw", ok);
|
||||
UpdateRouteAndPpi(ok);
|
||||
|
|
@ -1551,35 +1567,35 @@ private:
|
|||
throw std::runtime_error("no 401 to unprotected REGISTER");
|
||||
}
|
||||
|
||||
// re-REGISTER over the (resumed or live) SA. Returns (200 msg, used cseq).
|
||||
std::pair<std::string, long> Reregister(const std::string& callid, const std::string& ftag, long startCseq, std::uint32_t spiUc, std::uint32_t spiUs) {
|
||||
std::string auth = imsd::msg::AuthEmpty(ctx_);
|
||||
long cseq = startCseq;
|
||||
bool challenged = false;
|
||||
for (;;) {
|
||||
std::string reg = imsd::msg::BuildRegisterProtected(ctx_, callid, ftag, rng_.Token(16), cseq, spiUc, spiUs, auth);
|
||||
if (!sip_.Send(reg)) throw std::runtime_error("send re-REGISTER failed");
|
||||
std::string m = AwaitFinal(callid, cseq, "re-REGISTER");
|
||||
auto st = imsd::sip::Status(m);
|
||||
if (st == 200) return {m, cseq};
|
||||
if (st == 401 && !challenged) {
|
||||
challenged = true;
|
||||
auto nonce = QuotedAfter(m, "nonce=\"");
|
||||
auto raw = nonce ? imsd::util::FromBase64(*nonce) : std::nullopt;
|
||||
if (!raw || raw->size() < 32) throw std::runtime_error("bad 401 nonce on refresh");
|
||||
std::vector<std::uint8_t> rnd(raw->begin(), raw->begin() + 16);
|
||||
std::vector<std::uint8_t> autn(raw->begin() + 16, raw->begin() + 32);
|
||||
auto aka = Authenticate(rnd, autn);
|
||||
if (!aka) throw std::runtime_error("AKA failed on refresh");
|
||||
std::string cnonce = rng_.Token(16);
|
||||
std::string response = imsd::aka::DigestAkav1(*nonce, aka->res, cnonce, ctx_.id.regUri, ctx_.id.impi, ctx_.id.domain);
|
||||
auth = imsd::msg::AuthAka(ctx_, *nonce, cnonce, response);
|
||||
cseq++;
|
||||
continue;
|
||||
}
|
||||
DumpRaw("imsd-register-fail.raw", m);
|
||||
throw RefusedError(std::format("re-REGISTER refused: {}", imsd::sip::RefusalSummary(m)), *st, imsd::sip::RetryAfter(m).value_or(0));
|
||||
}
|
||||
// A reregistration over the (resumed or live) SA, TS 24.229 5.1.1.4.2:
|
||||
// the same Call-ID and Contact, CSeq+1, the Authorization of the last
|
||||
// successful authentication, a Security-Client with new SPIs and a new
|
||||
// client port for the SA pairs a re-authentication would set up (the
|
||||
// server port stays, TS 33.203 7.4.0), the Security-Verify (the 401's
|
||||
// list for a registration this process made fresh; for a resumed one
|
||||
// the entry rebuilt from the kernel's SA, on every reregistration until
|
||||
// the next fresh registration). Returns (200 msg, used cseq). A 401
|
||||
// asks for re-authentication over temporary SAs on the proposed ports,
|
||||
// which imsd does not do: it is thrown as a refusal whose recovery is a
|
||||
// fresh registration (RegRecovery's 401 rule). Every refresh of the
|
||||
// pre-0.3.11 shape on record from O2 UK — an initial registration's
|
||||
// empty digest, the in-use SPIs — was refused 503 (2026-09-24 under
|
||||
// 0.3.9, 2026-10-08 under 0.3.10) while the fresh registrations that
|
||||
// followed were accepted; KPN accepted both shapes without a challenge.
|
||||
std::pair<std::string, long> Reregister(const std::string& callid, const std::string& ftag, long cseq) {
|
||||
std::string auth = reg_.authNonce.empty() ? imsd::msg::AuthEmpty(ctx_)
|
||||
: imsd::msg::AuthAka(ctx_, reg_.authNonce, reg_.authCnonce, reg_.authResponse);
|
||||
std::uint32_t spiUc = 0, spiUs = 0;
|
||||
do spiUc = rng_.UInt(0x10000, 0xFFFFFFF); while (spiUc == reg_.spiUc || spiUc == reg_.spiUs);
|
||||
do spiUs = rng_.UInt(0x10000, 0xFFFFFFF); while (spiUs == reg_.spiUc || spiUs == reg_.spiUs || spiUs == spiUc);
|
||||
std::string reg = imsd::msg::BuildRegisterProtected(ctx_, callid, ftag, rng_.Token(16), cseq, spiUc, spiUs, auth, PortUcNext);
|
||||
if (!sip_.Send(reg)) throw std::runtime_error("send re-REGISTER failed");
|
||||
std::string m = AwaitFinal(callid, cseq, "re-REGISTER");
|
||||
auto st = imsd::sip::Status(m);
|
||||
if (st == 200) return {m, cseq};
|
||||
DumpRaw("imsd-register-fail.raw", m);
|
||||
if (st == 401) throw RefusedError(std::format("re-REGISTER challenged: {}; re-authentication asked, the next registration is a fresh one", imsd::sip::RefusalSummary(m)), 401, imsd::sip::RetryAfter(m).value_or(0));
|
||||
throw RefusedError(std::format("re-REGISTER refused: {}", imsd::sip::RefusalSummary(m)), *st, imsd::sip::RetryAfter(m).value_or(0));
|
||||
}
|
||||
|
||||
void UpdateRouteAndPpi(const std::string& okMsg) {
|
||||
|
|
@ -1662,7 +1678,7 @@ private:
|
|||
if (Mono() - lastRefresh_ < interval) return;
|
||||
try {
|
||||
RefreshPani();
|
||||
auto [msg, used] = Reregister(reg_.callid, reg_.ftag, reg_.cseq + 1, reg_.spiUc, reg_.spiUs);
|
||||
auto [msg, used] = Reregister(reg_.callid, reg_.ftag, reg_.cseq + 1);
|
||||
reg_.cseq = used;
|
||||
if (auto e = imsd::sip::GrantedExpires(msg)) reg_.expiry = *e;
|
||||
PersistState(reg_, route_, ppi_, ctx_.aor, ctx_.transport);
|
||||
|
|
@ -1670,11 +1686,12 @@ private:
|
|||
OnRegistered();
|
||||
} catch (const RefusedError& e) {
|
||||
// The network refused the refresh: the recovery policy counts it
|
||||
// — a refusal with a Retry-After of at least a second makes the
|
||||
// fresh registration the next step, behind the network's wait;
|
||||
// one without gets one more refresh first, a second refusal then
|
||||
// escalates — and the flow is dropped so the next step runs
|
||||
// after the wait.
|
||||
// — a refusal with a Retry-After of at least a second, or a 401
|
||||
// (the refresh throws a challenge as a refusal), makes the fresh
|
||||
// registration the next step, behind any wait the network set; a
|
||||
// refusal with neither gets one more refresh first, a second
|
||||
// refusal then escalates — and the flow is dropped so the next
|
||||
// step runs after the wait.
|
||||
Log(std::format("keepalive re-REGISTER failed: {}", e.what()));
|
||||
CloseFlowAfterWait();
|
||||
RecoveryFailed(imsd::engine::RegRecovery::Failure::Refused, e.retryAfter, "keepalive refresh refused", e.status);
|
||||
|
|
@ -1770,7 +1787,7 @@ private:
|
|||
}
|
||||
try {
|
||||
RefreshPani();
|
||||
auto [msg, used] = Reregister(reg_.callid, reg_.ftag, reg_.cseq + 1, reg_.spiUc, reg_.spiUs);
|
||||
auto [msg, used] = Reregister(reg_.callid, reg_.ftag, reg_.cseq + 1);
|
||||
reg_.cseq = used;
|
||||
if (auto e = imsd::sip::GrantedExpires(msg)) reg_.expiry = *e;
|
||||
UpdateRouteAndPpi(msg);
|
||||
|
|
|
|||
Loading…
Reference in a new issue