sip: UDP for the protected leg, TCP first with a fallback
imsd's protected leg (the second REGISTER and everything after it) was
TCP only. A P-CSCF that never answers the TCP connect on its protected
server port (O2 UK: the SYNs leave ESP-protected, nothing comes back)
left the unit looping with no way forward, although the SAs, the
listener sockets and the firewall rule already covered UDP.
The client flow now carries a transport. UDP binds the same protected
client port and connect()s the datagram socket to the P-CSCF's protected
server port, so the kernel delivers that peer's datagrams to it ahead of
the unconnected listener on the same port. One datagram is one message
(RFC 3261 18.3: a Content-Length that fits truncates, one that does not
fit discards the datagram, none means the rest of the datagram); a
receive error is logged and marks the flow dead; a message over the
single-packet ESP budget at the ims PDN's MTU is logged once per flow,
since the kernel fragments it and a P-CSCF may drop the fragments.
Every protected request's Via follows the transport; the challenge
stays UDP.
SIP_TRANSPORT selects the policy: auto (default) registers over TCP and,
after two unanswered connects, registers again over UDP (new challenge,
new SA pair) - but only on a phone whose state file does not record a
successful TCP registration, so an outage on a TCP carrier never becomes
a second initial REGISTER; a phone whose last registration was UDP goes
straight to UDP. tcp and udp force one. A connect refused locally
(EADDRNOTAVAIL: the 4-tuple still in TIME_WAIT from the previous flow)
keeps retrying and is not counted as silence; SO_ERROR results are
logged by name. The registration's transport is persisted so a warm
resume reconnects the same way, and the resume gives up after two
silences.
Two things the change exposed and fixes: an engine-fatal event exited 0
("exiting for systemd restart" with Restart=on-failure never firing),
and a fresh registration refused with the P-CSCF's fresh-SA throttle
(Security-Server spi-s=0) must not be retried by a 120-s restart loop,
since every attempt re-arms the ~20-min window - it is now waited out
in-process, growing on repeats, with D-Bus commands still served. A
resume the network refuses falls through to a fresh registration. A
retransmitted 200 OK to our INVITE is ACKed again and no longer starts
a second media leg.
Verified on KPN: resume over TCP after a binary swap, MT and MO calls
with media both ways, the throttle deferral and its self-recovery, the
UDP client path up to KPN dropping the datagram. The UDP success path
is a field test; SIP retransmission timers over UDP are not in this
change.
This commit is contained in:
parent
587b06b583
commit
5c89dcb033
8 changed files with 292 additions and 29 deletions
|
|
@ -208,6 +208,13 @@ int main() {
|
|||
Check(active && active->state == "active" && active->reason == "accepted", "200 emits active/accepted");
|
||||
Check(m.State() == CallState::Active, "state active");
|
||||
|
||||
// a retransmitted 200 (RFC 3261 13.3.1.4: the UAS repeats it until our
|
||||
// ACK arrives) is ACKed again and nothing else — no second media leg
|
||||
auto a200b = m.OnInviteResponse(R200(m.CallId()));
|
||||
Check(Sent(a200b, "ACK ") != nullptr, "retransmitted 200 -> ACK again");
|
||||
Check(!Has(a200b, Action::Type::StartMedia) && !Has(a200b, Action::Type::State), "retransmitted 200: no second media start, no state action");
|
||||
Check(m.State() == CallState::Active, "state still active after the retransmit");
|
||||
|
||||
// media leg exits code 3 (downlink dried up) -> BYE + terminated
|
||||
auto amx = m.OnMediaExit(3);
|
||||
Check(Sent(amx, "BYE ") != nullptr, "media far-end hangup sends BYE");
|
||||
|
|
|
|||
|
|
@ -192,6 +192,21 @@ int main() {
|
|||
Reg2, "protected REGISTER");
|
||||
}
|
||||
|
||||
// ---- protected-leg transport follows Context.transport (SIP_TRANSPORT=udp,
|
||||
// or the auto fallback): every protected request's Via says UDP, the
|
||||
// challenge's Via stays UDP either way, and TCP is the untouched default.
|
||||
{
|
||||
Context cu = c;
|
||||
cu.transport = "UDP";
|
||||
std::string auth = AuthAka(cu, "NONCEXYZ", "CNONCE0123456789", "RESP0000");
|
||||
std::string r2u = BuildRegisterProtected(cu, "REGCALLID@2001:db8::db43", "FTAG5678", "REGBRANCH123456", 2, 7449812u, 176466735u, auth);
|
||||
Check(r2u.contains("\r\nVia: SIP/2.0/UDP [2001:db8::db43]:45062;branch=z9hG4bKREGBRANCH123456;rport\r\n"), "protected REGISTER Via follows transport=UDP");
|
||||
Check(!r2u.contains("SIP/2.0/TCP"), "no TCP token left in a UDP protected REGISTER");
|
||||
Check(BuildSubscribeReg(cu, "SUBCALLID@2001:db8::db43", "STAG1234", "SUBBRANCH0000000000").contains("\r\nVia: SIP/2.0/UDP [2001:db8::db43]:45062;"), "SUBSCRIBE Via follows transport=UDP");
|
||||
Check(BuildRegisterInitial(cu, "REGCALLID@2001:db8::db43", "FTAG5678", "REGBRANCH123456", 7449812u, 176466735u).contains("\r\nVia: SIP/2.0/UDP [2001:db8::db43]:5060;"), "initial REGISTER Via is UDP regardless");
|
||||
Check(BuildRegisterProtected(c, "REGCALLID@2001:db8::db43", "FTAG5678", "REGBRANCH123456", 2, 7449812u, 176466735u, auth).contains("\r\nVia: SIP/2.0/TCP [2001:db8::db43]:45062;"), "default transport stays TCP");
|
||||
}
|
||||
|
||||
// ---- RuriFor variants
|
||||
Eq(RuriFor("1233", c.id.domain), "sip:1233;phone-context=ims.mnc001.mcc001.3gppnetwork.org@ims.mnc001.mcc001.3gppnetwork.org;user=phone", "ruri short code");
|
||||
Eq(RuriFor("+31612345678", c.id.domain), "sip:+31612345678@ims.mnc001.mcc001.3gppnetwork.org;user=phone", "ruri E.164");
|
||||
|
|
|
|||
|
|
@ -174,6 +174,28 @@ int main() {
|
|||
Check(!ViaResponsePort("UPDATE sip:me SIP/2.0\r\n\r\n").has_value(), "no Via -> nullopt");
|
||||
}
|
||||
|
||||
// ---- DatagramMessage (one UDP datagram = one message, RFC 3261 18.3)
|
||||
{
|
||||
std::string why;
|
||||
Check(!DatagramMessage("\r\n", &why).has_value() && why.empty(), "CRLF-only datagram is a pong, not a message, no problem");
|
||||
Check(!DatagramMessage("\r\n\r\n\r\n").has_value(), "CRLFCRLF-only datagram is a pong");
|
||||
std::string hdr = "SIP/2.0 200 OK\r\nCall-ID: a\r\n";
|
||||
auto m0 = DatagramMessage(hdr + "Content-Length: 0\r\n\r\n");
|
||||
Check(m0 && *m0 == hdr + "Content-Length: 0\r\n\r\n", "Content-Length 0, no body: whole datagram");
|
||||
auto m1 = DatagramMessage(hdr + "Content-Length: 4\r\n\r\nv=0\nJUNK");
|
||||
Check(m1 && *m1 == hdr + "Content-Length: 4\r\n\r\nv=0\n", "Content-Length that fits truncates the body");
|
||||
why.clear();
|
||||
Check(!DatagramMessage(hdr + "Content-Length: 40\r\n\r\nv=0\n", &why).has_value() && why.contains("truncated"), "Content-Length larger than the datagram: discarded, problem set");
|
||||
auto m2 = DatagramMessage("\r\n" + hdr + "\r\nv=0\n");
|
||||
Check(m2 && *m2 == hdr + "\r\nv=0\n", "no Content-Length: body is the rest of the datagram, leading CRLF stripped");
|
||||
why.clear();
|
||||
Check(!DatagramMessage("SIP/2.0 200 OK\r\nCall-ID: a\r\n", &why).has_value() && why.contains("terminator"), "no header terminator: discarded, problem set");
|
||||
auto m3 = DatagramMessage(hdr + "X-Note: Content-Length: 99\r\n\r\nv=0\n");
|
||||
Check(m3 && *m3 == hdr + "X-Note: Content-Length: 99\r\n\r\nv=0\n", "a header name inside a value does not match");
|
||||
auto m4 = DatagramMessage(hdr + "Content-Length: 99999999999999999999\r\n\r\nv=0\n", &why);
|
||||
Check(!m4.has_value(), "absurd Content-Length: discarded, no overflow");
|
||||
}
|
||||
|
||||
if (Failures == 0) std::println("Sip: all tests passed");
|
||||
return Failures;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue