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
|
|
@ -161,6 +161,7 @@ PCSCF=2001:db8::105
|
||||||
| `RESUME` | `auto` | `0` forces a fresh registration (ignores a warm SA) |
|
| `RESUME` | `auto` | `0` forces a fresh registration (ignores a warm SA) |
|
||||||
| `REFRESH_INTERVAL` | *(auto)* | keepalive re-REGISTER period in s; default = half the granted expiry, clamped to [120, 1800] |
|
| `REFRESH_INTERVAL` | *(auto)* | keepalive re-REGISTER period in s; default = half the granted expiry, clamped to [120, 1800] |
|
||||||
| `EALG` | `aes-cbc` | offered ESP cipher (`aes-cbc`, `des-ede3-cbc`, `null`) |
|
| `EALG` | `aes-cbc` | offered ESP cipher (`aes-cbc`, `des-ede3-cbc`, `null`) |
|
||||||
|
| `SIP_TRANSPORT` | `auto` | transport of the protected leg (the second REGISTER and everything after it): `tcp`, `udp`, or `auto` = TCP, falling back to a fresh registration over UDP when the P-CSCF leaves two TCP connects unanswered — but only on a phone that has never registered over TCP: once the state file (`/var/lib/imsd/imsreg.state`) records a TCP registration, a silent connect is treated as an outage, not a policy (delete the file or set `udp` to override, e.g. after a SIM change). The challenge (first REGISTER) is always UDP. Over UDP a REGISTER or INVITE larger than ~1.2 KB leaves as IPv6 fragments at the ims PDN's 1280-byte MTU (the journal says so once per flow); there are no SIP retransmission timers over UDP yet, so a lost datagram costs a timeout. The SAs, the listener and the firewall rule cover both transports |
|
||||||
| `EMERGENCY_NUMBERS` | *(empty)* | comma-separated additions to the builtin 112/911 emergency numbers (e.g. a private test core's short code). SIM `EF_ECC` is not read yet |
|
| `EMERGENCY_NUMBERS` | *(empty)* | comma-separated additions to the builtin 112/911 emergency numbers (e.g. a private test core's short code). SIM `EF_ECC` is not read yet |
|
||||||
| `RTP_PORT` | `50004` | local RTP port advertised in SDP |
|
| `RTP_PORT` | `50004` | local RTP port advertised in SDP |
|
||||||
| `PRECOND` | `0` | `1` offers SDP QoS preconditions |
|
| `PRECOND` | `0` | `1` offers SDP QoS preconditions |
|
||||||
|
|
|
||||||
|
|
@ -179,10 +179,53 @@ socklen_t MakeAddr(std::string_view ip, int port, sockaddr_storage& ss) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- SIP over the protected TCP flow --------------------------------------
|
// ---- SIP over the protected TCP flow --------------------------------------
|
||||||
class SipTcp {
|
enum class Transport { Tcp, Udp };
|
||||||
|
constexpr std::string_view TransportName(Transport t) { return t == Transport::Udp ? "UDP" : "TCP"; }
|
||||||
|
|
||||||
|
// The protected client flow: our requests to the P-CSCF's protected server
|
||||||
|
// port and their responses. TCP is the shape every carrier that works so far
|
||||||
|
// serves; UDP is the alternative for a P-CSCF that never answers the TCP
|
||||||
|
// connect (O2 UK, 2026-09-17). Both run through the same SA pair — the xfrm
|
||||||
|
// policies select on ports only — and the same Via port.
|
||||||
|
// A P-CSCF refusing a fresh security association (KPN: Security-Server
|
||||||
|
// spi-s=0 to a fresh REGISTER made too soon after another; the window is
|
||||||
|
// ~20 min and every fresh attempt re-arms it). Handled in-process: a
|
||||||
|
// systemd restart every RestartSec would never clear it.
|
||||||
|
struct ThrottledError : std::runtime_error { using std::runtime_error::runtime_error; };
|
||||||
|
constexpr int ThrottleWaitMin = 21;
|
||||||
|
// Bytes of SIP that fit one ESP-protected IPv6 packet at the ims PDN's
|
||||||
|
// 1280-byte MTU (40 IPv6 + 8 ESP + 16 IV + 8 UDP + padding + 12 ICV).
|
||||||
|
constexpr std::size_t UdpSinglePacketBudget = 1190;
|
||||||
|
|
||||||
|
class SipFlow {
|
||||||
public:
|
public:
|
||||||
bool Connect(const std::string& local, int lport, const std::string& pcscf, int pport, int tries = 10) {
|
// TCP: connect up to `tries` times (12-s timeout each, 10 s apart). An
|
||||||
|
// attempt the network never answers counts towards `silentLimit` (0 =
|
||||||
|
// unlimited); an attempt refused locally — connect() itself failing:
|
||||||
|
// EADDRNOTAVAIL (the 4-tuple still in TIME_WAIT from the previous flow),
|
||||||
|
// ENETUNREACH (no route yet) — does not, so a restart within 60 s of the
|
||||||
|
// last one keeps retrying instead of concluding the P-CSCF is silent. UDP: bind the same client port and connect() the
|
||||||
|
// datagram socket to the same peer — the kernel then delivers that peer's
|
||||||
|
// datagrams here in preference to the unconnected ServerPorts socket on
|
||||||
|
// the same port. A UDP connect() cannot be refused by the peer, so it is
|
||||||
|
// one attempt.
|
||||||
|
bool Connect(const std::string& local, int lport, const std::string& pcscf, int pport, Transport t, int tries = 10, int silentLimit = 0) {
|
||||||
|
transport_ = t;
|
||||||
int fam = Is6(local) ? AF_INET6 : AF_INET;
|
int fam = Is6(local) ? AF_INET6 : AF_INET;
|
||||||
|
if (t == Transport::Udp) {
|
||||||
|
int fd = socket(fam, SOCK_DGRAM, 0);
|
||||||
|
if (fd < 0) return false;
|
||||||
|
int one = 1;
|
||||||
|
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one);
|
||||||
|
setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &one, sizeof one);
|
||||||
|
sockaddr_storage la; socklen_t ll = MakeAddr(local, lport, la);
|
||||||
|
if (bind(fd, reinterpret_cast<sockaddr*>(&la), ll) != 0) { close(fd); return false; }
|
||||||
|
sockaddr_storage pa; socklen_t pl = MakeAddr(pcscf, pport, pa);
|
||||||
|
if (connect(fd, reinterpret_cast<sockaddr*>(&pa), pl) != 0) { Log(std::format("UDP connect: {}", std::strerror(errno))); close(fd); return false; }
|
||||||
|
fd_ = fd; alive_ = true; fb_ = {}; sizeWarned_ = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
int silent = 0;
|
||||||
for (int attempt = 0; attempt < tries; attempt++) {
|
for (int attempt = 0; attempt < tries; attempt++) {
|
||||||
int fd = socket(fam, SOCK_STREAM, 0);
|
int fd = socket(fam, SOCK_STREAM, 0);
|
||||||
if (fd < 0) return false;
|
if (fd < 0) return false;
|
||||||
|
|
@ -200,22 +243,33 @@ public:
|
||||||
fcntl(fd, F_SETFL, O_NONBLOCK);
|
fcntl(fd, F_SETFL, O_NONBLOCK);
|
||||||
int rc = connect(fd, reinterpret_cast<sockaddr*>(&pa), pl);
|
int rc = connect(fd, reinterpret_cast<sockaddr*>(&pa), pl);
|
||||||
bool connected = (rc == 0);
|
bool connected = (rc == 0);
|
||||||
|
bool localRefusal = (rc < 0 && errno != EINPROGRESS);
|
||||||
|
int err = localRefusal ? errno : 0;
|
||||||
if (rc < 0 && errno == EINPROGRESS) {
|
if (rc < 0 && errno == EINPROGRESS) {
|
||||||
pollfd p{fd, POLLOUT, 0};
|
pollfd p{fd, POLLOUT, 0};
|
||||||
if (poll(&p, 1, 12000) > 0) {
|
if (poll(&p, 1, 12000) > 0) {
|
||||||
int err = 0; socklen_t el = sizeof err;
|
socklen_t el = sizeof err;
|
||||||
getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &el);
|
getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &el);
|
||||||
connected = (err == 0);
|
connected = (err == 0);
|
||||||
}
|
} else err = ETIMEDOUT;
|
||||||
}
|
}
|
||||||
if (connected) {
|
if (connected) {
|
||||||
fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) & ~O_NONBLOCK);
|
fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) & ~O_NONBLOCK);
|
||||||
fd_ = fd; alive_ = true; fb_ = {};
|
fd_ = fd; alive_ = true; fb_ = {}; sizeWarned_ = false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
close(fd);
|
close(fd);
|
||||||
|
if (!localRefusal) silent++;
|
||||||
if (attempt == tries - 1) return false;
|
if (attempt == tries - 1) return false;
|
||||||
Log(std::format("connect failed; retry in 10s (TIME_WAIT?) [{}/{}]", attempt + 1, tries));
|
if (silentLimit > 0 && silent >= silentLimit) {
|
||||||
|
Log(std::format("connect: no answer from the P-CSCF {} times", silent));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// An answer (RST → ECONNREFUSED, ICMP → EHOSTUNREACH/ENETUNREACH)
|
||||||
|
// counts towards the silent limit like a timeout: either way the
|
||||||
|
// P-CSCF does not serve TCP on that port. The log says which.
|
||||||
|
const char* why = err == ETIMEDOUT ? "no answer" : (err == EADDRNOTAVAIL ? "TIME_WAIT?" : std::strerror(err));
|
||||||
|
Log(std::format("connect failed; retry in 10s ({}) [{}/{}]", why, attempt + 1, tries));
|
||||||
sleep(10);
|
sleep(10);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -223,6 +277,10 @@ public:
|
||||||
// MSG_NOSIGNAL: a flow the network killed (CSFB excursion, P-CSCF idle
|
// MSG_NOSIGNAL: a flow the network killed (CSFB excursion, P-CSCF idle
|
||||||
// drop) must surface as a send error, not a SIGPIPE.
|
// drop) must surface as a send error, not a SIGPIPE.
|
||||||
bool Send(std::string_view msg) {
|
bool Send(std::string_view msg) {
|
||||||
|
if (transport_ == Transport::Udp && msg.size() > UdpSinglePacketBudget && !sizeWarned_) {
|
||||||
|
sizeWarned_ = true;
|
||||||
|
Log(std::format("UDP: {} bytes of SIP exceed the single-packet ESP budget (~{}) at MTU 1280; the kernel sends it as IPv6 fragments — a P-CSCF that cannot reassemble ESP fragments drops it silently", msg.size(), UdpSinglePacketBudget));
|
||||||
|
}
|
||||||
std::size_t off = 0;
|
std::size_t off = 0;
|
||||||
while (off < msg.size()) {
|
while (off < msg.size()) {
|
||||||
ssize_t w = send(fd_, msg.data() + off, msg.size() - off, MSG_NOSIGNAL);
|
ssize_t w = send(fd_, msg.data() + off, msg.size() - off, MSG_NOSIGNAL);
|
||||||
|
|
@ -232,6 +290,7 @@ public:
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
void SendKeepalive() {
|
void SendKeepalive() {
|
||||||
|
if (transport_ == Transport::Udp) return; // RFC 5626 4.4.1: CRLF keepalives are for connection-oriented transports
|
||||||
if (send(fd_, "\r\n\r\n", 4, MSG_NOSIGNAL) <= 0) alive_ = false;
|
if (send(fd_, "\r\n\r\n", 4, MSG_NOSIGNAL) <= 0) alive_ = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -248,6 +307,26 @@ public:
|
||||||
if (r <= 0) { if (r == 0) return std::nullopt; if (errno == EINTR) continue; return std::nullopt; }
|
if (r <= 0) { if (r == 0) return std::nullopt; if (errno == EINTR) continue; return std::nullopt; }
|
||||||
char buf[65535];
|
char buf[65535];
|
||||||
ssize_t n = recv(fd_, buf, sizeof buf, 0);
|
ssize_t n = recv(fd_, buf, sizeof buf, 0);
|
||||||
|
if (transport_ == Transport::Udp) {
|
||||||
|
// One datagram = one message (RFC 3261 18.3): never accumulated
|
||||||
|
// across datagrams. An empty datagram is not EOF. A recv error
|
||||||
|
// is the kernel relaying an ICMP for the inner (decrypted) UDP
|
||||||
|
// — port unreachable → ECONNREFUSED, prohibited/policy → EACCES
|
||||||
|
// — i.e. the peer answering "no"; an ICMP quoting the ESP
|
||||||
|
// packet itself never reaches us (esp6_err ignores all but
|
||||||
|
// PKT_TOOBIG), so UDP has no dead-flow signal beyond these and
|
||||||
|
// the refresh timeouts.
|
||||||
|
if (n < 0) {
|
||||||
|
if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) continue;
|
||||||
|
Log(std::format("UDP flow: recv error: {}", std::strerror(errno)));
|
||||||
|
alive_ = false; return std::nullopt;
|
||||||
|
}
|
||||||
|
if (n == 0) continue;
|
||||||
|
std::string why;
|
||||||
|
if (auto m = imsd::sip::DatagramMessage(std::string_view(buf, static_cast<std::size_t>(n)), &why)) return m;
|
||||||
|
if (!why.empty()) Log(std::format("UDP: {} — ignored", why));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
// EOF/RST is not a timeout: mark the flow dead so the engine's
|
// EOF/RST is not a timeout: mark the flow dead so the engine's
|
||||||
// reconnect path notices within one loop turn instead of at the
|
// reconnect path notices within one loop turn instead of at the
|
||||||
// next (possibly 30-min-away) keepalive refresh.
|
// next (possibly 30-min-away) keepalive refresh.
|
||||||
|
|
@ -257,10 +336,13 @@ public:
|
||||||
}
|
}
|
||||||
int Fd() const { return fd_; }
|
int Fd() const { return fd_; }
|
||||||
bool Alive() const { return fd_ >= 0 && alive_; }
|
bool Alive() const { return fd_ >= 0 && alive_; }
|
||||||
|
Transport CurrentTransport() const { return transport_; }
|
||||||
void Close() { if (fd_ >= 0) { close(fd_); fd_ = -1; } alive_ = false; }
|
void Close() { if (fd_ >= 0) { close(fd_); fd_ = -1; } alive_ = false; }
|
||||||
private:
|
private:
|
||||||
int fd_ = -1;
|
int fd_ = -1;
|
||||||
bool alive_ = false;
|
bool alive_ = false;
|
||||||
|
Transport transport_ = Transport::Tcp;
|
||||||
|
bool sizeWarned_ = false;
|
||||||
imsd::sip::FrameBuffer fb_;
|
imsd::sip::FrameBuffer fb_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -279,7 +361,7 @@ public:
|
||||||
if (ls >= 0) {
|
if (ls >= 0) {
|
||||||
int one = 1;
|
int one = 1;
|
||||||
setsockopt(ls, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one);
|
setsockopt(ls, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one);
|
||||||
// paired with SipTcp::Connect's REUSEPORT (reconnect binds
|
// paired with SipFlow::Connect's REUSEPORT (reconnect binds
|
||||||
// the client port while this listener holds it)
|
// the client port while this listener holds it)
|
||||||
setsockopt(ls, SOL_SOCKET, SO_REUSEPORT, &one, sizeof one);
|
setsockopt(ls, SOL_SOCKET, SO_REUSEPORT, &one, sizeof one);
|
||||||
sockaddr_storage a; socklen_t l = MakeAddr(local, port, a);
|
sockaddr_storage a; socklen_t l = MakeAddr(local, port, a);
|
||||||
|
|
@ -323,6 +405,14 @@ public:
|
||||||
bool blank = sv.find_first_not_of("\r\n \t") == std::string_view::npos;
|
bool blank = sv.find_first_not_of("\r\n \t") == std::string_view::npos;
|
||||||
if (blank) continue;
|
if (blank) continue;
|
||||||
std::string msg(sv);
|
std::string msg(sv);
|
||||||
|
if (imsd::sip::Status(msg).has_value()) {
|
||||||
|
// A response here means the P-CSCF answered to the Via port
|
||||||
|
// instead of the source port of our request (rport) — it
|
||||||
|
// never reaches the client flow. Say so.
|
||||||
|
int sp = src.ss_family == AF_INET6 ? ntohs(reinterpret_cast<sockaddr_in6*>(&src)->sin6_port) : ntohs(reinterpret_cast<sockaddr_in*>(&src)->sin_port);
|
||||||
|
Log(std::format("UDP response on server socket {} from port {} ignored: {}", us == udpUc_ ? PortUc : PortUs, sp, sv.substr(0, sv.find("\r\n"))));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (!imsd::sip::Status(msg).has_value()) { // request only
|
if (!imsd::sip::Status(msg).has_value()) { // request only
|
||||||
sockaddr_storage s = src; socklen_t sll = sl;
|
sockaddr_storage s = src; socklen_t sll = sl;
|
||||||
int rs = us;
|
int rs = us;
|
||||||
|
|
@ -411,13 +501,13 @@ std::string StateDir() {
|
||||||
// tiny JSON reader/writer for the persisted registration context.
|
// tiny JSON reader/writer for the persisted registration context.
|
||||||
std::string StatePath() { return EnvOr("STATE_FILE", std::format("{}/imsreg.state", StateDir())); }
|
std::string StatePath() { return EnvOr("STATE_FILE", std::format("{}/imsreg.state", StateDir())); }
|
||||||
|
|
||||||
void PersistState(const RegState& r, const std::string& route, const std::string& ppi, const std::string& aor) {
|
void PersistState(const RegState& r, const std::string& route, const std::string& ppi, const std::string& aor, const std::string& transport) {
|
||||||
std::string j = std::format(
|
std::string j = std::format(
|
||||||
"{{\"callid\": \"{}\", \"ftag\": \"{}\", \"cseq\": {}, \"spi_uc\": {}, "
|
"{{\"callid\": \"{}\", \"ftag\": \"{}\", \"cseq\": {}, \"spi_uc\": {}, "
|
||||||
"\"spi_us\": {}, \"expiry\": {}, \"route\": \"{}\", \"ppi\": \"{}\", "
|
"\"spi_us\": {}, \"expiry\": {}, \"route\": \"{}\", \"ppi\": \"{}\", "
|
||||||
"\"aor\": \"{}\", \"contact_user\": \"{}\"}}",
|
"\"aor\": \"{}\", \"contact_user\": \"{}\", \"transport\": \"{}\"}}",
|
||||||
r.callid, r.ftag, r.cseq, r.spiUc, r.spiUs, r.expiry, route, ppi, aor,
|
r.callid, r.ftag, r.cseq, r.spiUc, r.spiUs, r.expiry, route, ppi, aor,
|
||||||
r.contactUser);
|
r.contactUser, transport);
|
||||||
std::string path = StatePath();
|
std::string path = StatePath();
|
||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
std::filesystem::create_directories(std::filesystem::path(path).parent_path(), ec);
|
std::filesystem::create_directories(std::filesystem::path(path).parent_path(), ec);
|
||||||
|
|
@ -441,7 +531,7 @@ void DumpRaw(std::string_view name, std::string_view msg) {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PersistedState {
|
struct PersistedState {
|
||||||
std::optional<std::string> callid, ftag, route, ppi, aor, contactUser;
|
std::optional<std::string> callid, ftag, route, ppi, aor, contactUser, transport;
|
||||||
std::optional<long> cseq, spiUc, spiUs, expiry;
|
std::optional<long> cseq, spiUc, spiUs, expiry;
|
||||||
};
|
};
|
||||||
std::optional<PersistedState> LoadState() {
|
std::optional<PersistedState> LoadState() {
|
||||||
|
|
@ -458,6 +548,7 @@ std::optional<PersistedState> LoadState() {
|
||||||
ps.ppi = QuotedAfter(s, "\"ppi\": \"");
|
ps.ppi = QuotedAfter(s, "\"ppi\": \"");
|
||||||
ps.aor = QuotedAfter(s, "\"aor\": \"");
|
ps.aor = QuotedAfter(s, "\"aor\": \"");
|
||||||
ps.contactUser = QuotedAfter(s, "\"contact_user\": \"");
|
ps.contactUser = QuotedAfter(s, "\"contact_user\": \"");
|
||||||
|
ps.transport = QuotedAfter(s, "\"transport\": \""); // absent in pre-0.3.5 files: TCP
|
||||||
ps.cseq = IntAfter(s, "\"cseq\": ");
|
ps.cseq = IntAfter(s, "\"cseq\": ");
|
||||||
ps.spiUc = IntAfter(s, "\"spi_uc\": ");
|
ps.spiUc = IntAfter(s, "\"spi_uc\": ");
|
||||||
ps.spiUs = IntAfter(s, "\"spi_us\": ");
|
ps.spiUs = IntAfter(s, "\"spi_us\": ");
|
||||||
|
|
@ -492,6 +583,7 @@ GDBusConnection* DbusConn = nullptr;
|
||||||
std::map<std::string, CallInfo> Calls; // main thread only
|
std::map<std::string, CallInfo> Calls; // main thread only
|
||||||
StatusSnapshot StatusSnap; // main thread only
|
StatusSnapshot StatusSnap; // main thread only
|
||||||
GMainLoop* MainLoop = nullptr;
|
GMainLoop* MainLoop = nullptr;
|
||||||
|
bool FatalExit = false; // set by an engine-fatal event: main() exits non-zero so systemd restarts us
|
||||||
|
|
||||||
std::int64_t NowEpoch() {
|
std::int64_t NowEpoch() {
|
||||||
return std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch()).count();
|
return std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch()).count();
|
||||||
|
|
@ -547,6 +639,7 @@ gboolean OnIdleEvent(gpointer data) {
|
||||||
break;
|
break;
|
||||||
case Event::Type::Fatal:
|
case Event::Type::Fatal:
|
||||||
Log(std::format("engine fatal: {} — exiting for systemd restart", ev->text));
|
Log(std::format("engine fatal: {} — exiting for systemd restart", ev->text));
|
||||||
|
FatalExit = true; // non-zero exit, or Restart=on-failure never fires
|
||||||
g_main_loop_quit(MainLoop);
|
g_main_loop_quit(MainLoop);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -571,6 +664,16 @@ public:
|
||||||
rtpPort_ = std::atoi(EnvOr("RTP_PORT", "50004").c_str());
|
rtpPort_ = std::atoi(EnvOr("RTP_PORT", "50004").c_str());
|
||||||
precond_ = EnvOr("PRECOND", "0") == "1";
|
precond_ = EnvOr("PRECOND", "0") == "1";
|
||||||
ealgOffer_ = EnvOr("EALG", "aes-cbc");
|
ealgOffer_ = EnvOr("EALG", "aes-cbc");
|
||||||
|
// SIP_TRANSPORT: the protected leg's transport. auto (default) = TCP,
|
||||||
|
// and when the P-CSCF never answers the TCP connect, a fresh
|
||||||
|
// registration over UDP; tcp / udp force one. The challenge (first
|
||||||
|
// REGISTER) is UDP regardless.
|
||||||
|
sipPolicy_ = EnvOr("SIP_TRANSPORT", "auto");
|
||||||
|
if (sipPolicy_ != "auto" && sipPolicy_ != "tcp" && sipPolicy_ != "udp") {
|
||||||
|
Log(std::format("SIP_TRANSPORT={} unknown; using auto", sipPolicy_));
|
||||||
|
sipPolicy_ = "auto";
|
||||||
|
}
|
||||||
|
transport_ = sipPolicy_ == "udp" ? Transport::Udp : Transport::Tcp;
|
||||||
mediaBin_ = ResolveMediaBin();
|
mediaBin_ = ResolveMediaBin();
|
||||||
// CODECS: restrict + reorder the codecs we offer and accept (bench
|
// CODECS: restrict + reorder the codecs we offer and accept (bench
|
||||||
// knob — a gateway that transcodes every caller up to AMR-WB never
|
// knob — a gateway that transcodes every caller up to AMR-WB never
|
||||||
|
|
@ -620,8 +723,29 @@ public:
|
||||||
}
|
}
|
||||||
|
|
||||||
void Run() {
|
void Run() {
|
||||||
|
for (;;) {
|
||||||
try {
|
try {
|
||||||
BringUp();
|
BringUp();
|
||||||
|
break;
|
||||||
|
} catch (const ThrottledError& e) {
|
||||||
|
// The window grows on repeats (a carrier's window may exceed the
|
||||||
|
// first guess and every throttled attempt re-arms it); it resets
|
||||||
|
// after a successful bring-up.
|
||||||
|
int waitMin = throttleWaitMin_;
|
||||||
|
throttleCount_++;
|
||||||
|
Log(std::format("bring-up deferred: {} — attempt {}, next fresh registration in {} min", e.what(), throttleCount_, waitMin));
|
||||||
|
registered_ = false;
|
||||||
|
EmitStatus(true);
|
||||||
|
for (int i = 0; i < waitMin * 60 && !quit_.load(); i++) {
|
||||||
|
// A Dial/Accept/HangUp arriving now must not wait in the
|
||||||
|
// queue and run stale once registration succeeds; with no
|
||||||
|
// flow the call collapses to terminated at once (DevLoop's rule).
|
||||||
|
if (auto cmd = PopCmd()) { HandleCmd(*cmd); MaybeDropCall(); continue; }
|
||||||
|
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||||
|
}
|
||||||
|
if (quit_.load()) return;
|
||||||
|
throttleWaitMin_ = std::min(throttleWaitMin_ * 3 / 2, 60);
|
||||||
|
continue;
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
if (devMode_) {
|
if (devMode_) {
|
||||||
// dev/session mode (no modem): keep the ABI up, unregistered,
|
// dev/session mode (no modem): keep the ABI up, unregistered,
|
||||||
|
|
@ -638,6 +762,8 @@ public:
|
||||||
PostEvent(std::move(ev));
|
PostEvent(std::move(ev));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
throttleWaitMin_ = ThrottleWaitMin; throttleCount_ = 0;
|
||||||
registered_ = true;
|
registered_ = true;
|
||||||
EmitStatus(true);
|
EmitStatus(true);
|
||||||
lastRefresh_ = Mono();
|
lastRefresh_ = Mono();
|
||||||
|
|
@ -694,6 +820,11 @@ private:
|
||||||
|
|
||||||
// config
|
// config
|
||||||
std::string pcscf_, dev_, outDir_, local_, ealgOffer_, mediaBin_;
|
std::string pcscf_, dev_, outDir_, local_, ealgOffer_, mediaBin_;
|
||||||
|
std::string sipPolicy_; // SIP_TRANSPORT: auto | tcp | udp
|
||||||
|
int throttleWaitMin_ = ThrottleWaitMin; // grows on consecutive throttles, resets on success
|
||||||
|
int throttleCount_ = 0;
|
||||||
|
Transport transport_ = Transport::Tcp; // the protected leg's transport in effect
|
||||||
|
void SetTransport(Transport t) { transport_ = t; ctx_.transport = std::string(TransportName(t)); }
|
||||||
std::vector<std::string> emergencyExtra_;
|
std::vector<std::string> emergencyExtra_;
|
||||||
int pcscfPort_ = 5060;
|
int pcscfPort_ = 5060;
|
||||||
int rtpPort_ = 50004;
|
int rtpPort_ = 50004;
|
||||||
|
|
@ -705,7 +836,7 @@ private:
|
||||||
RegState reg_;
|
RegState reg_;
|
||||||
std::string route_, ppi_, ss_;
|
std::string route_, ppi_, ss_;
|
||||||
bool registered_ = false;
|
bool registered_ = false;
|
||||||
SipTcp sip_;
|
SipFlow sip_;
|
||||||
ServerPorts server_;
|
ServerPorts server_;
|
||||||
std::string subCallid_; // live reg-event subscription dialog (empty: none)
|
std::string subCallid_; // live reg-event subscription dialog (empty: none)
|
||||||
int portPs_ = 0; // P-CSCF protected server port (reconnect target)
|
int portPs_ = 0; // P-CSCF protected server port (reconnect target)
|
||||||
|
|
@ -781,6 +912,8 @@ private:
|
||||||
ctx_.id = imsd::aka::MakeIdentity(info->imsi, info->mcc, info->mnc);
|
ctx_.id = imsd::aka::MakeIdentity(info->imsi, info->mcc, info->mnc);
|
||||||
ctx_.local = local_;
|
ctx_.local = local_;
|
||||||
ctx_.ealgOffer = ealgOffer_;
|
ctx_.ealgOffer = ealgOffer_;
|
||||||
|
SetTransport(transport_);
|
||||||
|
Log(std::format("SIP transport: {} ({})", TransportName(transport_), sipPolicy_));
|
||||||
// USER_AGENT overrides (some networks fingerprint UAs — setting the
|
// USER_AGENT overrides (some networks fingerprint UAs — setting the
|
||||||
// stock firmware's build string gives oracle parity, journal/ims.md
|
// stock firmware's build string gives oracle parity, journal/ims.md
|
||||||
// s53); USER_AGENT= (empty) omits the header.
|
// s53); USER_AGENT= (empty) omits the header.
|
||||||
|
|
@ -793,7 +926,14 @@ private:
|
||||||
RefreshPani();
|
RefreshPani();
|
||||||
|
|
||||||
std::string resume = EnvOr("RESUME", "auto");
|
std::string resume = EnvOr("RESUME", "auto");
|
||||||
if (resume != "0" && TryResume()) return;
|
// A resume that the network refuses (403 after another process
|
||||||
|
// registered fresh for the same IMPI, a stale binding, …) is not
|
||||||
|
// fatal: the fresh registration below replaces its SAs. Only a
|
||||||
|
// failed FRESH registration is.
|
||||||
|
if (resume != "0") {
|
||||||
|
try { if (TryResume()) return; }
|
||||||
|
catch (const std::exception& e) { Log(std::format("resume failed: {} — registering fresh", e.what())); sip_.Close(); }
|
||||||
|
}
|
||||||
FreshRegister();
|
FreshRegister();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -804,6 +944,11 @@ private:
|
||||||
if (!sa) return false;
|
if (!sa) return false;
|
||||||
auto ps = LoadState();
|
auto ps = LoadState();
|
||||||
if (!ps || !ps->callid) { Log("no reg state file; resume falls back to fresh"); return false; }
|
if (!ps || !ps->callid) { Log("no reg state file; resume falls back to fresh"); return false; }
|
||||||
|
// The registration's transport is what the P-CSCF knows; a resume
|
||||||
|
// must reuse it. A forced policy that disagrees means a fresh start
|
||||||
|
// — decided before anything from the file is adopted.
|
||||||
|
Transport t = ps->transport.value_or("TCP") == "UDP" ? Transport::Udp : Transport::Tcp;
|
||||||
|
if (sipPolicy_ != "auto" && t != transport_) { Log("state file transport differs from SIP_TRANSPORT; fresh"); return false; }
|
||||||
|
|
||||||
route_ = ps->route.value_or(std::format("<sip:{};lr>", imsd::util::HostPort(pcscf_, sa->portPs)));
|
route_ = ps->route.value_or(std::format("<sip:{};lr>", imsd::util::HostPort(pcscf_, sa->portPs)));
|
||||||
// No fallback identity: an empty ppi omits P-Preferred-Identity and
|
// No fallback identity: an empty ppi omits P-Preferred-Identity and
|
||||||
|
|
@ -819,7 +964,10 @@ private:
|
||||||
Log(std::format("RESUME via existing SA (cseq {})", ps->cseq.value_or(1)));
|
Log(std::format("RESUME via existing SA (cseq {})", ps->cseq.value_or(1)));
|
||||||
|
|
||||||
portPs_ = sa->portPs;
|
portPs_ = sa->portPs;
|
||||||
if (!sip_.Connect(local_, PortUc, pcscf_, sa->portPs)) {
|
SetTransport(t);
|
||||||
|
// Local refusals (TIME_WAIT after a restart) keep retrying; two
|
||||||
|
// silences mean the flow is not coming back — fresh instead.
|
||||||
|
if (!sip_.Connect(local_, PortUc, pcscf_, sa->portPs, transport_, 10, 2)) {
|
||||||
Log("resume connect failed; falling back to fresh");
|
Log("resume connect failed; falling back to fresh");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -839,7 +987,7 @@ private:
|
||||||
if (auto e = imsd::sip::GrantedExpires(okMsg)) reg_.expiry = *e;
|
if (auto e = imsd::sip::GrantedExpires(okMsg)) reg_.expiry = *e;
|
||||||
UpdateRouteAndPpi(okMsg);
|
UpdateRouteAndPpi(okMsg);
|
||||||
LogBindings(okMsg);
|
LogBindings(okMsg);
|
||||||
PersistState(reg_, route_, ppi_, ctx_.aor);
|
PersistState(reg_, route_, ppi_, ctx_.aor, ctx_.transport);
|
||||||
Log("REGISTERED (resumed + true-refreshed)");
|
Log("REGISTERED (resumed + true-refreshed)");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -898,7 +1046,7 @@ private:
|
||||||
std::uint32_t spiPs = static_cast<std::uint32_t>(IntAfter(ss_, "spi-s=").value_or(0));
|
std::uint32_t spiPs = static_cast<std::uint32_t>(IntAfter(ss_, "spi-s=").value_or(0));
|
||||||
std::uint32_t spiPc = static_cast<std::uint32_t>(IntAfter(ss_, "spi-c=").value_or(0));
|
std::uint32_t spiPc = static_cast<std::uint32_t>(IntAfter(ss_, "spi-c=").value_or(0));
|
||||||
std::string ealg = EalgAfter(ss_);
|
std::string ealg = EalgAfter(ss_);
|
||||||
if (spiPs == 0) throw std::runtime_error("fresh-SA throttle active (spi-s=0); retry later");
|
if (spiPs == 0) throw ThrottledError("fresh-SA throttle active (spi-s=0)");
|
||||||
|
|
||||||
auto aka = Authenticate(rand16, autn16);
|
auto aka = Authenticate(rand16, autn16);
|
||||||
if (!aka) throw std::runtime_error("USIM AKA failed");
|
if (!aka) throw std::runtime_error("USIM AKA failed");
|
||||||
|
|
@ -915,7 +1063,37 @@ private:
|
||||||
|
|
||||||
ctx_.securityServer = ss_;
|
ctx_.securityServer = ss_;
|
||||||
portPs_ = portPs;
|
portPs_ = portPs;
|
||||||
if (!sip_.Connect(local_, PortUc, pcscf_, portPs)) throw std::runtime_error("protected TCP connect failed");
|
// The fallback is for a P-CSCF that never served TCP. Once a TCP
|
||||||
|
// registration has succeeded on this phone (the state file records
|
||||||
|
// it), a silent connect is an outage, not a policy — two unanswered
|
||||||
|
// SYNs during a radio gap must not become a second initial REGISTER
|
||||||
|
// (the throttle trigger on KPN).
|
||||||
|
bool tcpKnownGood = false;
|
||||||
|
if (auto ps = LoadState(); ps && ps->callid.has_value()) {
|
||||||
|
std::string t = ps->transport.value_or("TCP");
|
||||||
|
tcpKnownGood = (t == "TCP");
|
||||||
|
// A phone whose last registration was UDP goes straight to UDP:
|
||||||
|
// a TCP probe on every boot would be a second initial REGISTER
|
||||||
|
// per boot on a carrier that never answers it.
|
||||||
|
if (sipPolicy_ == "auto" && t == "UDP" && transport_ == Transport::Tcp) {
|
||||||
|
Log("state file records a UDP registration; skipping the TCP probe");
|
||||||
|
SetTransport(Transport::Udp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bool autoTcp = (sipPolicy_ == "auto" && transport_ == Transport::Tcp && !tcpKnownGood);
|
||||||
|
if (!sip_.Connect(local_, PortUc, pcscf_, portPs, transport_, 10, autoTcp ? 2 : 0)) {
|
||||||
|
if (autoTcp) {
|
||||||
|
// Two unanswered TCP connects: treat the P-CSCF as UDP-only
|
||||||
|
// and register again over UDP — a new challenge and a new SA
|
||||||
|
// pair (the SA setup flushes this one), same ports.
|
||||||
|
Log("protected TCP connect unanswered; registering fresh over UDP");
|
||||||
|
SetTransport(Transport::Udp);
|
||||||
|
FreshRegister();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw std::runtime_error(std::format("protected {} connect failed", TransportName(transport_)));
|
||||||
|
}
|
||||||
|
Log(std::format("protected leg over {}", TransportName(transport_)));
|
||||||
|
|
||||||
std::string cnonce = rng_.Token(16);
|
std::string cnonce = rng_.Token(16);
|
||||||
std::string response = imsd::aka::DigestAkav1(*nonceB64, aka->res, cnonce, ctx_.id.regUri, ctx_.id.impi, ctx_.id.domain);
|
std::string response = imsd::aka::DigestAkav1(*nonceB64, aka->res, cnonce, ctx_.id.regUri, ctx_.id.impi, ctx_.id.domain);
|
||||||
|
|
@ -938,7 +1116,7 @@ private:
|
||||||
LogBindings(ok);
|
LogBindings(ok);
|
||||||
if (route_.empty()) route_ = std::format("<sip:{};lr>", imsd::util::HostPort(pcscf_, portPs));
|
if (route_.empty()) route_ = std::format("<sip:{};lr>", imsd::util::HostPort(pcscf_, portPs));
|
||||||
ctx_.route = route_; ctx_.ppi = ppi_;
|
ctx_.route = route_; ctx_.ppi = ppi_;
|
||||||
PersistState(reg_, route_, ppi_, ctx_.aor);
|
PersistState(reg_, route_, ppi_, ctx_.aor, ctx_.transport);
|
||||||
Log("REGISTERED (fresh)");
|
Log("REGISTERED (fresh)");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1056,7 +1234,7 @@ private:
|
||||||
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_.spiUc, reg_.spiUs);
|
||||||
reg_.cseq = used;
|
reg_.cseq = used;
|
||||||
if (auto e = imsd::sip::GrantedExpires(msg)) reg_.expiry = *e;
|
if (auto e = imsd::sip::GrantedExpires(msg)) reg_.expiry = *e;
|
||||||
PersistState(reg_, route_, ppi_, ctx_.aor);
|
PersistState(reg_, route_, ppi_, ctx_.aor, ctx_.transport);
|
||||||
Log(std::format("keepalive re-REGISTER ok (cseq {})", used));
|
Log(std::format("keepalive re-REGISTER ok (cseq {})", used));
|
||||||
SubscribeRegEvent();
|
SubscribeRegEvent();
|
||||||
if (!registered_) { registered_ = true; EmitStatus(true); }
|
if (!registered_) { registered_ = true; EmitStatus(true); }
|
||||||
|
|
@ -1083,7 +1261,7 @@ private:
|
||||||
if (Mono() < nextReconnect_) return;
|
if (Mono() < nextReconnect_) return;
|
||||||
Log("client flow dead; reconnecting");
|
Log("client flow dead; reconnecting");
|
||||||
sip_.Close();
|
sip_.Close();
|
||||||
bool ok = sip_.Connect(local_, PortUc, pcscf_, portPs_, /*tries=*/2);
|
bool ok = sip_.Connect(local_, PortUc, pcscf_, portPs_, transport_, /*tries=*/2);
|
||||||
if (ok) {
|
if (ok) {
|
||||||
try {
|
try {
|
||||||
RefreshPani();
|
RefreshPani();
|
||||||
|
|
@ -1092,7 +1270,7 @@ private:
|
||||||
if (auto e = imsd::sip::GrantedExpires(msg)) reg_.expiry = *e;
|
if (auto e = imsd::sip::GrantedExpires(msg)) reg_.expiry = *e;
|
||||||
UpdateRouteAndPpi(msg);
|
UpdateRouteAndPpi(msg);
|
||||||
LogBindings(msg);
|
LogBindings(msg);
|
||||||
PersistState(reg_, route_, ppi_, ctx_.aor);
|
PersistState(reg_, route_, ppi_, ctx_.aor, ctx_.transport);
|
||||||
Log("reconnected + re-registered");
|
Log("reconnected + re-registered");
|
||||||
SubscribeRegEvent();
|
SubscribeRegEvent();
|
||||||
if (!registered_) { registered_ = true; EmitStatus(true); }
|
if (!registered_) { registered_ = true; EmitStatus(true); }
|
||||||
|
|
@ -1514,5 +1692,5 @@ int main(int argc, char** argv) {
|
||||||
pthread_join(engineTid, nullptr);
|
pthread_join(engineTid, nullptr);
|
||||||
g_bus_unown_name(owner);
|
g_bus_unown_name(owner);
|
||||||
g_main_loop_unref(MainLoop);
|
g_main_loop_unref(MainLoop);
|
||||||
return 0;
|
return FatalExit ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -257,6 +257,10 @@ export namespace imsd::engine {
|
||||||
return a; // 1xx other than 100<code<200 (only 100): nothing
|
return a; // 1xx other than 100<code<200 (only 100): nothing
|
||||||
if (code == 200) {
|
if (code == 200) {
|
||||||
a.push_back(Send(imsd::msg::BuildAck2xx(ctx_, d_, rng_.Token(20), std::string(imsd::sip::Header(msg, "To").value_or("")))));
|
a.push_back(Send(imsd::msg::BuildAck2xx(ctx_, d_, rng_.Token(20), std::string(imsd::sip::Header(msg, "To").value_or("")))));
|
||||||
|
// A 2xx retransmission (RFC 3261 13.3.1.4: the UAS repeats
|
||||||
|
// it until our ACK arrives) is ACKed again and nothing else —
|
||||||
|
// media is already running, or the call is already over.
|
||||||
|
if (state_ != CallState::Ringing && state_ != CallState::Dialing) return a;
|
||||||
if (cancelled_ && userCancelled_) {
|
if (cancelled_ && userCancelled_) {
|
||||||
// answered in the user's CANCEL race — we no longer want it
|
// answered in the user's CANCEL race — we no longer want it
|
||||||
nextCseq_++;
|
nextCseq_++;
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,10 @@ export namespace imsd::msg {
|
||||||
int portUc = imsd::util::PortUc;
|
int portUc = imsd::util::PortUc;
|
||||||
int portUs = imsd::util::PortUs;
|
int portUs = imsd::util::PortUs;
|
||||||
int initPort = 5060;
|
int initPort = 5060;
|
||||||
|
// Transport of the protected leg (REGISTER 2 onwards): the Via token
|
||||||
|
// of every request we send over it. TCP unless the engine fell back
|
||||||
|
// to, or was told to use, UDP.
|
||||||
|
std::string transport = "TCP";
|
||||||
std::string ealgOffer = "aes-cbc";
|
std::string ealgOffer = "aes-cbc";
|
||||||
// RFC 7254 IMEI URN for +sip.instance (e.g. urn:gsma:imei:35999999-
|
// RFC 7254 IMEI URN for +sip.instance (e.g. urn:gsma:imei:35999999-
|
||||||
// 000000-1); empty omits the parameter.
|
// 000000-1); empty omits the parameter.
|
||||||
|
|
@ -218,7 +222,7 @@ export namespace imsd::msg {
|
||||||
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) {
|
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) {
|
||||||
std::vector<std::string> l = {
|
std::vector<std::string> l = {
|
||||||
std::format("REGISTER {} SIP/2.0", c.id.regUri),
|
std::format("REGISTER {} SIP/2.0", c.id.regUri),
|
||||||
ViaLine(c, "TCP", c.portUs, branch),
|
ViaLine(c, c.transport, c.portUs, branch),
|
||||||
"Max-Forwards: 70",
|
"Max-Forwards: 70",
|
||||||
std::format("From: <{}>;tag={}", c.id.impu, ftag),
|
std::format("From: <{}>;tag={}", c.id.impu, ftag),
|
||||||
std::format("To: <{}>", c.id.impu),
|
std::format("To: <{}>", c.id.impu),
|
||||||
|
|
@ -252,7 +256,7 @@ export namespace imsd::msg {
|
||||||
: std::string_view(c.aor);
|
: std::string_view(c.aor);
|
||||||
std::vector<std::string> l = {
|
std::vector<std::string> l = {
|
||||||
std::format("SUBSCRIBE {} SIP/2.0", aor),
|
std::format("SUBSCRIBE {} SIP/2.0", aor),
|
||||||
ViaLine(c, "TCP", c.portUs, branch),
|
ViaLine(c, c.transport, c.portUs, branch),
|
||||||
"Max-Forwards: 70",
|
"Max-Forwards: 70",
|
||||||
std::format("Route: {}", c.route),
|
std::format("Route: {}", c.route),
|
||||||
std::format("From: <{}>;tag={}", aor, ftag),
|
std::format("From: <{}>;tag={}", aor, ftag),
|
||||||
|
|
@ -331,7 +335,7 @@ export namespace imsd::msg {
|
||||||
: "100rel, timer";
|
: "100rel, timer";
|
||||||
std::vector<std::string> l = {
|
std::vector<std::string> l = {
|
||||||
std::format("INVITE {} SIP/2.0", d.ruri),
|
std::format("INVITE {} SIP/2.0", d.ruri),
|
||||||
std::format("Via: SIP/2.0/TCP {};branch=z9hG4bK{};rport", imsd::util::HostPort(c.local, c.portUs), d.invBranch),
|
ViaLine(c, c.transport, c.portUs, d.invBranch),
|
||||||
"Max-Forwards: 70",
|
"Max-Forwards: 70",
|
||||||
std::format("Route: {}", c.route),
|
std::format("Route: {}", c.route),
|
||||||
std::format("From: <{}>;tag={}", FromUri(c, d), d.itag),
|
std::format("From: <{}>;tag={}", FromUri(c, d), d.itag),
|
||||||
|
|
@ -364,7 +368,7 @@ export namespace imsd::msg {
|
||||||
std::string to = d.dialogTo.value_or(std::format("<{}>", d.ruri));
|
std::string to = d.dialogTo.value_or(std::format("<{}>", d.ruri));
|
||||||
std::vector<std::string> l = {
|
std::vector<std::string> l = {
|
||||||
std::format("{} {} SIP/2.0", method, target),
|
std::format("{} {} SIP/2.0", method, target),
|
||||||
ViaLine(c, "TCP", c.portUs, viaBranch),
|
ViaLine(c, c.transport, c.portUs, viaBranch),
|
||||||
"Max-Forwards: 70",
|
"Max-Forwards: 70",
|
||||||
std::format("Route: {}", DialogRoute(c, d)),
|
std::format("Route: {}", DialogRoute(c, d)),
|
||||||
std::format("From: <{}>;tag={}", FromUri(c, d), d.itag),
|
std::format("From: <{}>;tag={}", FromUri(c, d), d.itag),
|
||||||
|
|
@ -391,7 +395,7 @@ export namespace imsd::msg {
|
||||||
std::string to = d.dialogTo.value_or(std::string(toHeader));
|
std::string to = d.dialogTo.value_or(std::string(toHeader));
|
||||||
std::vector<std::string> l = {
|
std::vector<std::string> l = {
|
||||||
std::format("ACK {} SIP/2.0", target),
|
std::format("ACK {} SIP/2.0", target),
|
||||||
ViaLine(c, "TCP", c.portUs, viaBranch),
|
ViaLine(c, c.transport, c.portUs, viaBranch),
|
||||||
"Max-Forwards: 70",
|
"Max-Forwards: 70",
|
||||||
std::format("Route: {}", DialogRoute(c, d)),
|
std::format("Route: {}", DialogRoute(c, d)),
|
||||||
std::format("From: <{}>;tag={}", FromUri(c, d), d.itag),
|
std::format("From: <{}>;tag={}", FromUri(c, d), d.itag),
|
||||||
|
|
@ -408,7 +412,7 @@ export namespace imsd::msg {
|
||||||
std::string BuildAckNon2xx(const Context& c, const Dialog& d, std::string_view toHeader) {
|
std::string BuildAckNon2xx(const Context& c, const Dialog& d, std::string_view toHeader) {
|
||||||
std::vector<std::string> l = {
|
std::vector<std::string> l = {
|
||||||
std::format("ACK {} SIP/2.0", d.ruri),
|
std::format("ACK {} SIP/2.0", d.ruri),
|
||||||
std::format("Via: SIP/2.0/TCP {};branch=z9hG4bK{};rport", imsd::util::HostPort(c.local, c.portUs), d.invBranch),
|
ViaLine(c, c.transport, c.portUs, d.invBranch),
|
||||||
"Max-Forwards: 70",
|
"Max-Forwards: 70",
|
||||||
std::format("Route: {}", c.route),
|
std::format("Route: {}", c.route),
|
||||||
std::format("From: <{}>;tag={}", FromUri(c, d), d.itag),
|
std::format("From: <{}>;tag={}", FromUri(c, d), d.itag),
|
||||||
|
|
@ -424,7 +428,7 @@ export namespace imsd::msg {
|
||||||
std::string BuildCancel(const Context& c, const Dialog& d) {
|
std::string BuildCancel(const Context& c, const Dialog& d) {
|
||||||
std::vector<std::string> l = {
|
std::vector<std::string> l = {
|
||||||
std::format("CANCEL {} SIP/2.0", d.ruri),
|
std::format("CANCEL {} SIP/2.0", d.ruri),
|
||||||
std::format("Via: SIP/2.0/TCP {};branch=z9hG4bK{};rport", imsd::util::HostPort(c.local, c.portUs), d.invBranch),
|
ViaLine(c, c.transport, c.portUs, d.invBranch),
|
||||||
"Max-Forwards: 70",
|
"Max-Forwards: 70",
|
||||||
std::format("Route: {}", c.route),
|
std::format("Route: {}", c.route),
|
||||||
std::format("From: <{}>;tag={}", FromUri(c, d), d.itag),
|
std::format("From: <{}>;tag={}", FromUri(c, d), d.itag),
|
||||||
|
|
|
||||||
|
|
@ -233,4 +233,36 @@ export namespace imsd::sip {
|
||||||
private:
|
private:
|
||||||
std::string buf_;
|
std::string buf_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// The SIP message carried by one UDP datagram (RFC 3261 18.3): a
|
||||||
|
// datagram is never accumulated with the next. Leading CRLFs (an RFC
|
||||||
|
// 5626 keepalive pong) are not a message → nullopt, no problem. A
|
||||||
|
// Content-Length that fits truncates the body to it; one that does not
|
||||||
|
// fit is a truncated datagram → nullopt with `problem` set; no
|
||||||
|
// Content-Length means the body is the rest of the datagram. A datagram
|
||||||
|
// without a header terminator is malformed → nullopt with `problem`.
|
||||||
|
std::optional<std::string> DatagramMessage(std::string_view d, std::string* problem = nullptr) {
|
||||||
|
std::size_t start = d.find_first_not_of("\r\n");
|
||||||
|
if (start == std::string_view::npos) return std::nullopt;
|
||||||
|
d.remove_prefix(start);
|
||||||
|
std::size_t idx = d.find("\r\n\r\n");
|
||||||
|
if (idx == std::string_view::npos) {
|
||||||
|
if (problem) *problem = std::format("datagram without a header terminator ({} bytes)", d.size());
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
std::size_t bodyStart = idx + 4;
|
||||||
|
std::size_t body = d.size() - bodyStart;
|
||||||
|
if (auto v = Header(d.substr(0, idx), "Content-Length")) {
|
||||||
|
std::size_t clen = 0; int digits = 0;
|
||||||
|
for (char c : *v) { if (c < '0' || c > '9' || digits == 9) break; clen = clen * 10 + static_cast<std::size_t>(c - '0'); digits++; }
|
||||||
|
if (digits) {
|
||||||
|
if (clen > body) {
|
||||||
|
if (problem) *problem = std::format("datagram truncated (Content-Length {} > {} body bytes)", clen, body);
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
return std::string(d.substr(0, bodyStart + clen));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::string(d);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -208,6 +208,13 @@ int main() {
|
||||||
Check(active && active->state == "active" && active->reason == "accepted", "200 emits active/accepted");
|
Check(active && active->state == "active" && active->reason == "accepted", "200 emits active/accepted");
|
||||||
Check(m.State() == CallState::Active, "state active");
|
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
|
// media leg exits code 3 (downlink dried up) -> BYE + terminated
|
||||||
auto amx = m.OnMediaExit(3);
|
auto amx = m.OnMediaExit(3);
|
||||||
Check(Sent(amx, "BYE ") != nullptr, "media far-end hangup sends BYE");
|
Check(Sent(amx, "BYE ") != nullptr, "media far-end hangup sends BYE");
|
||||||
|
|
|
||||||
|
|
@ -192,6 +192,21 @@ int main() {
|
||||||
Reg2, "protected REGISTER");
|
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
|
// ---- 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("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");
|
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");
|
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");
|
if (Failures == 0) std::println("Sip: all tests passed");
|
||||||
return Failures;
|
return Failures;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue