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
|
|
@ -14,8 +14,9 @@
|
|||
// 480, BYE both ways. Then RegRecovery, the registration recovery policy:
|
||||
// the refusal and silence thresholds, local refusals, the reconnect and
|
||||
// fresh backoffs, the throttle schedule, Retry-After, which waits a Dial
|
||||
// may cut short, when the old registration counts as gone, and the holds
|
||||
// a restart takes over (Holds/Restore). Pure reducers, no I/O.
|
||||
// may cut short, when the old registration counts as gone, the holds a
|
||||
// restart takes over (Holds/Restore), and a binding the network terminated
|
||||
// (OnBindingTerminated). Pure reducers, no I/O.
|
||||
import std;
|
||||
import Imsd;
|
||||
|
||||
|
|
@ -977,6 +978,65 @@ int main() {
|
|||
t.OnFreshFailure(1000, F::Refused, 0, /*saReplaced=*/true);
|
||||
Check(!t.Reconnectable() && freshAt(t, 1000, 1000 + 21 * 60), "refused past the SA setup: gone, and on the schedule");
|
||||
}
|
||||
// the network terminated our binding
|
||||
{
|
||||
RegRecovery n;
|
||||
n.OnBindingTerminated(1000, "expired");
|
||||
Check(n.Fresh() && !n.Reconnectable() && freshAt(n, 1000, 0), "expired: fresh now, nothing to reconnect to");
|
||||
RegRecovery o;
|
||||
o.OnBindingTerminated(1000, "probation", 300);
|
||||
Check(freshAt(o, 1000, 1300), "probation: after the network's retry-after");
|
||||
o.OnDial();
|
||||
Check(freshAt(o, 1000, 1300), "…which a Dial does not cut");
|
||||
RegRecovery p;
|
||||
p.OnBindingTerminated(1000, "probation");
|
||||
Check(freshAt(p, 1000, 1001), "probation without a retry-after: at least 1 s");
|
||||
RegRecovery q;
|
||||
q.OnBindingTerminated(1000, "rejected");
|
||||
Check(freshAt(q, 1000, 1000 + 21 * 60) && !q.Reconnectable(), "rejected: on the refusal schedule, a hold, the old SA not retried");
|
||||
q.OnBindingTerminated(1001, "rejected");
|
||||
q.OnBindingTerminated(2000, "unregistered");
|
||||
Check(q.freshHoldUntil == 1000 + 21 * 60 && q.throttleMinutes == 31, "the same termination reported again (another dialog, a retransmission): counted once");
|
||||
RegRecovery u;
|
||||
u.OnBindingTerminated(1000, "deactivated");
|
||||
u.OnFreshFailure(1000, F::Silent);
|
||||
u.OnBindingTerminated(1005, "deactivated");
|
||||
Check(freshAt(u, 1005, 1030), "…nor does a repeat cut the backoff of the attempt made since");
|
||||
RegRecovery v;
|
||||
v.OnFreshFailure(1000, F::Refused, 3000);
|
||||
v.OnBindingTerminated(1010, "rejected");
|
||||
Check(!v.Reconnectable() && freshAt(v, 1010, 4000) && v.throttleMinutes == 46, "a rejected binding never shortens the network's Retry-After");
|
||||
RegRecovery w;
|
||||
w.OnBindingTerminated(1000, "probation", 3000);
|
||||
w.OnBindingTerminated(1010, "probation", 60);
|
||||
Check(freshAt(w, 1010, 4000), "…nor a second probation report the first one's retry-after");
|
||||
RegRecovery x;
|
||||
x.OnBindingTerminated(1000, "probation", 86400);
|
||||
Check(freshAt(x, 1000, 1000 + 3600), "probation's retry-after is cut to the hour like a Retry-After");
|
||||
// terminated while a reconnect cycle waited for its re-REGISTER final: the cycle's failure is no news
|
||||
RegRecovery y;
|
||||
y.OnReconnectFailure(1000, F::Silent);
|
||||
y.OnBindingTerminated(1010, "expired");
|
||||
y.OnReconnectFailure(1011, F::Silent);
|
||||
Check(y.silentCycles == 1 && freshAt(y, 1011, 0), "the flow closed under the cycle: not a silent cycle, the fresh registration now");
|
||||
y.OnReconnectFailure(1012, F::Refused, 90, 503);
|
||||
Check(y.refusals == 0 && freshAt(y, 1012, 1102), "…a refusal with a Retry-After on it: not counted, the network's hold kept");
|
||||
}
|
||||
// a terminated binding's verdict outlives the process with its hold
|
||||
{
|
||||
RegRecovery tg;
|
||||
tg.OnBindingTerminated(1000, "rejected");
|
||||
auto htg = tg.Holds(1100);
|
||||
Check(htg.Any() && htg.oldGone && htg.freshHold == 1000 + 21 * 60 - 1100, "a terminated binding's holds carry that the old registration is gone");
|
||||
RegRecovery th;
|
||||
th.Restore(50, htg);
|
||||
Check(!th.Reconnectable() && freshAt(th, 50, 50 + htg.freshHold), "…and the restart does not reconnect over its SAs: the fresh attempt waits what was left");
|
||||
RegRecovery ti;
|
||||
ti.OnBindingTerminated(1000, "probation", 1800);
|
||||
RegRecovery tj;
|
||||
tj.Restore(0, ti.Holds(1300));
|
||||
Check(!tj.Reconnectable() && freshAt(tj, 0, 1500), "probation's retry-after survives a restart the same way");
|
||||
}
|
||||
// the network's holds outlive the process
|
||||
{
|
||||
RegRecovery a;
|
||||
|
|
|
|||
|
|
@ -241,6 +241,53 @@ int main() {
|
|||
Check(!ResponseOf("", "12345@host", "2 REGISTER"), "nothing is not it");
|
||||
}
|
||||
|
||||
// ---- RegInfoContactState: our binding in a reg-event NOTIFY (RFC 3680)
|
||||
{
|
||||
using S = RegInfoContact::State;
|
||||
auto contact = [](std::string_view id, std::string_view state, std::string_view extra, std::string_view user) {
|
||||
return std::format(
|
||||
"\t\t<contact id=\"{}\" state=\"{}\"{}>\n"
|
||||
"\t\t\t<uri>sip:{}@[2a03:db8::1]:45062</uri>\n"
|
||||
"\t\t\t<unknown-param name=\"+sip.instance\">"<urn:gsma:imei:35901961-000000-0>" </unknown-param>\n"
|
||||
"\t\t</contact>\n", id, state, extra, user);
|
||||
};
|
||||
auto reginfo = [](std::string_view contacts) {
|
||||
return std::format(
|
||||
"<?xml version=\"1.0\"?>\n"
|
||||
"<reginfo xmlns=\"urn:ietf:params:xml:ns:reginfo\" version=\"1\" state=\"full\">\n"
|
||||
"\t<registration aor=\"sip:+447700900000@example.net\" id=\"2\" state=\"active\">\n{0}\t</registration>\n"
|
||||
"\t<registration aor=\"tel:+447700900000\" id=\"3\" state=\"active\">\n{0}\t</registration>\n"
|
||||
"</reginfo>\n", contacts);
|
||||
};
|
||||
const std::string me = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa";
|
||||
const std::string old = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb";
|
||||
// the first NOTIFY of a registration: our contact active in both registrations
|
||||
auto r0 = RegInfoContactState(reginfo(contact("3", "active", " event=\"created\" expires=\"10800\"", me)), me);
|
||||
Check(r0.state == S::Active, "our contact active -> Active");
|
||||
// O2 2026-09-24 10:43: the previous registration's contact deactivated by ours, ours created
|
||||
std::string both = contact("3", "terminated", " event=\"deactivated\"", old) + contact("4", "active", " event=\"created\" expires=\"10800\"", me);
|
||||
auto r1 = RegInfoContactState(reginfo(both), me);
|
||||
Check(r1.state == S::Active, "old contact terminated + ours active -> Active (the old one is not ours)");
|
||||
auto r1o = RegInfoContactState(reginfo(both), old);
|
||||
Check(r1o.state == S::Terminated && r1o.event == "deactivated" && r1o.retryAfter == 0, "seen from the old contact's side: Terminated/deactivated");
|
||||
// a network-initiated deregistration of our own binding
|
||||
auto r2 = RegInfoContactState(reginfo(contact("4", "terminated", " event=\"probation\" retry-after=\"120\"", me)), me);
|
||||
Check(r2.state == S::Terminated && r2.event == "probation" && r2.retryAfter == 120, "probation with retry-after");
|
||||
auto r3 = RegInfoContactState(reginfo(contact("4", "terminated", " event=\"rejected\"", me)), me);
|
||||
Check(r3.state == S::Terminated && r3.event == "rejected", "rejected");
|
||||
// active in one registration of the set beats terminated in another
|
||||
std::string mixed =
|
||||
"<reginfo version=\"2\" state=\"partial\">\n<registration aor=\"tel:+1\" id=\"3\" state=\"active\">\n" + contact("4", "terminated", " event=\"expired\"", me) +
|
||||
"</registration>\n<registration aor=\"sip:+1@x\" id=\"2\" state=\"active\">\n" + contact("4", "active", "", me) + "</registration>\n</reginfo>";
|
||||
Check(RegInfoContactState(mixed, me).state == S::Active, "active anywhere in the set -> Active");
|
||||
// not ours at all
|
||||
auto r4 = RegInfoContactState(reginfo(contact("9", "terminated", " event=\"unregistered\"", old)), me);
|
||||
Check(r4.state == S::NotListed && r4.event.empty(), "a document without our contact -> NotListed");
|
||||
Check(RegInfoContactState("", me).state == S::NotListed, "empty body -> NotListed");
|
||||
Check(RegInfoContactState("<reginfo><registration><contact id=\"1\" state=\"active\"><uri>sip:aaaaaaaa@x</uri></contact>", me).state == S::NotListed, "a user part that is only a prefix of ours does not match");
|
||||
Check(RegInfoContactState("<contact state=\"terminated\" event=\"expired\"><uri>sip:" + me + "@h</uri>", me).state == S::Terminated, "an unterminated element still parses");
|
||||
}
|
||||
|
||||
if (Failures == 0) std::println("Sip: all tests passed");
|
||||
return Failures;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue