engine: keep the reg-event subscription alive and act on a terminated binding
The reg-event subscription is the network's own account of our binding (TS 24.229 5.1.1.3), and the daemon opened a new dialog for it at every refresh and reconnect and otherwise never looked back: a refused SUBSCRIBE was forgotten, an unanswered one had no timeout, an ended subscription got nothing until the next refresh's new dialog, and a NOTIFY was logged whatever it said. O2 UK, 2026-09-24: a fresh registration's SUBSCRIBE got 500, a NOTIFY arrived all the same and showed our contact active, the flow died eight seconds after the 200 and no connect over the SA succeeded after that; the 500 was logged and nothing was done about it. Keep the subscription alive: retry a refused or unanswered SUBSCRIBE on a backoff, renew one the S-CSCF ends with deactivated, timeout, giveup or probation (RFC 6665 4.1.3; after any other reason, or none, the next refresh subscribes anew), and subscribe anew ahead of the lifetime the S-CSCF granted. A refresh or a reconnect opens no dialog while imsd holds one for the registration or has a retry scheduled. Read the reginfo for our own contact and act on its state: a NOTIFY that terminates our binding schedules a fresh registration — at once for deactivated or expired, after the network's retry-after (up to an hour) for probation, on imsd's own refusal schedule (21 min, growing to 60) for rejected or unregistered (RFC 3680). A hold already running stands, and a REGISTER wait a call kept open that still gets its 200 after such a NOTIFY leaves that registration standing. The parser is in Imsd:Sip and pinned by tests/Sip with the O2 document shapes; the policy is RegRecovery's (OnBindingTerminated, added here with its tests in tests/Engine). Tested on the dev phone (KPN) in builds of this series; not every path has run there.
This commit is contained in:
parent
142adf057c
commit
db848c6490
6 changed files with 412 additions and 38 deletions
|
|
@ -93,11 +93,13 @@ export namespace imsd::engine {
|
|||
// unbarred registers within the hour); once a fresh registration is
|
||||
// due it also says the old registration is gone (KPN drops it the
|
||||
// moment an initial REGISTER arrives, throttled or not), as does a
|
||||
// fresh attempt that got past the SA setup, and a resume that failed at
|
||||
// bring-up (OldGone) — no reconnect over the old SA from then on. Every wait
|
||||
// fresh attempt that got past the SA setup, a binding the network
|
||||
// terminated, and a resume that failed at bring-up (OldGone) — no
|
||||
// reconnect over the old SA from then on. Every wait
|
||||
// is either the self-imposed backoff, which a user's Dial cuts short,
|
||||
// or the network's own word (a Retry-After, the schedule after a
|
||||
// refused or throttled fresh attempt), which nothing cuts short — a
|
||||
// refused or throttled fresh attempt, a terminated binding's probation
|
||||
// or its refusal schedule), which nothing cuts short — a
|
||||
// restart of the process neither: the shell keeps the holds (Holds)
|
||||
// and hands them to the next one (Restore). The shell supplies the
|
||||
// clock and the network; this holds the deadlines and names the next
|
||||
|
|
@ -137,10 +139,11 @@ export namespace imsd::engine {
|
|||
int throttleMinutes = ThrottleMinutesMin;
|
||||
bool fresh = false; // the next registration attempt is a fresh one
|
||||
bool oldGone = false; // the old registration is gone: nothing to reconnect over
|
||||
bool terminated = false; // the network terminated our binding, and the policy has counted it
|
||||
// the deadlines, on the shell's clock
|
||||
double backoffUntil = 0; // self-imposed: no step before this; a Dial clears it
|
||||
double holdUntil = 0; // the network's word for any attempt (a Retry-After on a refresh): nothing clears it
|
||||
double freshHoldUntil = 0; // no fresh attempt before this (the schedule after a refused or throttled fresh attempt, a fresh Retry-After)
|
||||
double freshHoldUntil = 0; // no fresh attempt before this (the schedule after a refused or throttled fresh attempt, a fresh Retry-After, probation, rejected/unregistered)
|
||||
|
||||
bool Fresh() const { return fresh; }
|
||||
bool Reconnectable() const { return !oldGone; }
|
||||
|
|
@ -187,13 +190,18 @@ export namespace imsd::engine {
|
|||
// 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) {
|
||||
if (retryAfter > 0) holdUntil = std::max(holdUntil, now + static_cast<double>(std::min(retryAfter, RetryAfterMax)));
|
||||
// A cycle over a registration already known gone — the network
|
||||
// terminated the binding while the cycle waited for its final —
|
||||
// fails by definition: not a silent cycle, not a refusal, and no
|
||||
// backoff before the fresh registration that verdict scheduled.
|
||||
if (oldGone) return;
|
||||
if (f == Failure::Refused) refusals++;
|
||||
else silentCycles++;
|
||||
if (refusals >= RefusalsBeforeFresh || silentCycles >= SilentCyclesBeforeFresh) fresh = true;
|
||||
if (status == 403 && fresh) oldGone = true;
|
||||
backoffUntil = now + reconnectDelay;
|
||||
reconnectDelay = std::min(reconnectDelay * 2, ReconnectDelayMax);
|
||||
if (retryAfter > 0) holdUntil = std::max(holdUntil, now + static_cast<double>(std::min(retryAfter, RetryAfterMax)));
|
||||
}
|
||||
|
||||
// A failed fresh registration; `saReplaced`: it got past the SA
|
||||
|
|
@ -219,6 +227,32 @@ export namespace imsd::engine {
|
|||
freshHoldUntil = now + wait;
|
||||
}
|
||||
|
||||
// The network terminated our binding (a reg-event NOTIFY, RFC
|
||||
// 3680): the old registration is gone. deactivated and expired
|
||||
// mean a fresh registration now; probation after the network's
|
||||
// retry-after (up to RetryAfterMax); rejected and unregistered on the
|
||||
// refusal schedule —
|
||||
// the network's word is neither hammered nor taken as final. One
|
||||
// termination counts once, however often it is reported (every
|
||||
// reg-event dialog of the account carries it, a NOTIFY may be
|
||||
// retransmitted), and never shortens a hold already running.
|
||||
void OnBindingTerminated(double now, std::string_view event, long retryAfter = 0) {
|
||||
bool reported = terminated;
|
||||
terminated = true;
|
||||
oldGone = true;
|
||||
fresh = true;
|
||||
if (event == "probation") {
|
||||
freshHoldUntil = std::max(freshHoldUntil, now + static_cast<double>(std::clamp(retryAfter, 1L, RetryAfterMax)));
|
||||
} else if (reported) {
|
||||
return;
|
||||
} else if (event == "rejected" || event == "unregistered") {
|
||||
freshHoldUntil = std::max(freshHoldUntil, now + throttleMinutes * 60.0);
|
||||
throttleMinutes = std::min(throttleMinutes * 3 / 2, ThrottleMinutesMax);
|
||||
} else {
|
||||
backoffUntil = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// The stored registration is not there to reconnect over (a resume
|
||||
// that failed at bring-up found its SAs unusable, or there are none).
|
||||
void OldGone() { oldGone = true; fresh = true; }
|
||||
|
|
|
|||
|
|
@ -154,6 +154,60 @@ export namespace imsd::sip {
|
|||
return s;
|
||||
}
|
||||
|
||||
// Our binding in a reg-event NOTIFY body (RFC 3680 reginfo): the
|
||||
// <contact> whose <uri> carries `user` as its SIP user part, across every
|
||||
// <registration> of the implicit set (the sip: and the tel: identity
|
||||
// list the same contact). Active anywhere = Active; otherwise the first
|
||||
// terminated occurrence's event and retry-after attribute; NotListed
|
||||
// when no contact of ours is in the document — a partial update about
|
||||
// other bindings, or a previous registration's binding (the contact our
|
||||
// own fresh registration deactivated, O2 UK 2026-09-24).
|
||||
struct RegInfoContact {
|
||||
enum class State { NotListed, Active, Terminated };
|
||||
State state = State::NotListed;
|
||||
std::string event; // terminated: deactivated | probation | rejected | unregistered | expired | …
|
||||
long retryAfter = 0; // terminated: the retry-after attribute (probation), seconds
|
||||
};
|
||||
RegInfoContact RegInfoContactState(std::string_view reginfo, std::string_view user) {
|
||||
constexpr std::size_t npos = std::string_view::npos;
|
||||
RegInfoContact out;
|
||||
std::string needle = std::format(":{}@", user); // sip:<user>@… / sips:<user>@…
|
||||
std::size_t pos = 0;
|
||||
while ((pos = reginfo.find("<contact", pos)) != npos) {
|
||||
std::size_t tagEnd = reginfo.find('>', pos);
|
||||
if (tagEnd == npos) break;
|
||||
std::string_view tag = reginfo.substr(pos, tagEnd - pos);
|
||||
std::size_t end = reginfo.find("</contact>", tagEnd);
|
||||
std::string_view body = reginfo.substr(tagEnd + 1, end == npos ? npos : end - tagEnd - 1);
|
||||
pos = end == npos ? reginfo.size() : end + 10;
|
||||
std::size_t u = body.find("<uri>");
|
||||
if (u == npos) continue;
|
||||
std::size_t ue = body.find("</uri>", u);
|
||||
std::string_view uri = Trim(body.substr(u + 5, ue == npos ? npos : ue - u - 5));
|
||||
if (!uri.contains(needle)) continue;
|
||||
auto attr = [&](std::string_view name) -> std::string_view {
|
||||
std::string key = std::format(" {}=\"", name);
|
||||
std::size_t a = tag.find(key);
|
||||
if (a == npos) return {};
|
||||
std::size_t b = tag.find('"', a + key.size());
|
||||
return tag.substr(a + key.size(), b == npos ? npos : b - a - key.size());
|
||||
};
|
||||
std::string_view state = attr("state");
|
||||
if (state == "active") {
|
||||
out.state = RegInfoContact::State::Active;
|
||||
out.event.clear();
|
||||
out.retryAfter = 0;
|
||||
return out;
|
||||
}
|
||||
if (state == "terminated" && out.state == RegInfoContact::State::NotListed) {
|
||||
out.state = RegInfoContact::State::Terminated;
|
||||
out.event = std::string(attr("event"));
|
||||
out.retryAfter = LeadingDigits(attr("retry-after")).value_or(0);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Where a UDP response to this request must go (RFC 3261 18.2.2): the
|
||||
// top Via's sent-by port — unless the Via carries rport, in which case
|
||||
// the caller should reply to the request's source port instead
|
||||
|
|
|
|||
Loading…
Reference in a new issue