Compare commits

..
7 changed files with 20 additions and 135 deletions

View file

@ -164,8 +164,7 @@ PCSCF=2001:db8::105
| `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 |
| `CODECS` | *(empty — defaults)* | comma-separated codec preference list over `AMR-WB`, `AMR` (or `AMR-NB`), `PCMA`, `PCMU`: restricts and orders both the codecs offered on an outgoing call and those accepted from an inbound offer (default: offer AMR-WB + AMR, accept all four in that order). A bench knob — a network whose gateway transcodes every caller up to AMR-WB otherwise never lets the narrowband path run | | `DUMP_SIP` | `0` | `1` writes raw REGISTER-200/SUBSCRIBE-200/INVITE dumps (mode 0600) for debugging — they contain your IMSI/MSISDN and addresses |
| `DUMP_SIP` | `0` | `1` writes raw REGISTER-200/SUBSCRIBE-200 dumps, the last inbound INVITE (`imsd-invite-in.raw`) and the last outgoing INVITE (`imsd-invite-out.raw`), mode 0600, for debugging — they contain your IMSI/MSISDN and addresses |
| `DUMP_DIR`, `OUT_DIR` | state dir | where dumps / per-call media stats land | | `DUMP_DIR`, `OUT_DIR` | state dir | where dumps / per-call media stats land |
| `IMSD_MEDIA` | *(auto)* | path to `imsd-media` (default: next to `imsd`, else `/usr/libexec/imsd-media`) | | `IMSD_MEDIA` | *(auto)* | path to `imsd-media` (default: next to `imsd`, else `/usr/libexec/imsd-media`) |

View file

@ -41,7 +41,7 @@ import Imsd;
namespace { namespace {
// ---- static config (env-overridable, same knobs as imsd.py) --------------- // ---- static config (env-overridable, same knobs as imsd.py) ---------------
constexpr const char* Version = "0.3.4"; constexpr const char* Version = "0.3.3";
constexpr const char* BusName = "net.catcrafts.IMS1"; constexpr const char* BusName = "net.catcrafts.IMS1";
constexpr const char* ObjPath = "/net/catcrafts/IMS1"; constexpr const char* ObjPath = "/net/catcrafts/IMS1";
constexpr const char* Iface = "net.catcrafts.IMS1"; constexpr const char* Iface = "net.catcrafts.IMS1";
@ -411,12 +411,12 @@ 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) {
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\": \"{}\"}}", "\"contact_user\": \"{}\"}}",
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,
r.contactUser); r.contactUser);
std::string path = StatePath(); std::string path = StatePath();
std::error_code ec; std::error_code ec;
@ -441,7 +441,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, contactUser;
std::optional<long> cseq, spiUc, spiUs, expiry; std::optional<long> cseq, spiUc, spiUs, expiry;
}; };
std::optional<PersistedState> LoadState() { std::optional<PersistedState> LoadState() {
@ -456,7 +456,6 @@ std::optional<PersistedState> LoadState() {
ps.ftag = QuotedAfter(s, "\"ftag\": \""); ps.ftag = QuotedAfter(s, "\"ftag\": \"");
ps.route = QuotedAfter(s, "\"route\": \""); ps.route = QuotedAfter(s, "\"route\": \"");
ps.ppi = QuotedAfter(s, "\"ppi\": \""); ps.ppi = QuotedAfter(s, "\"ppi\": \"");
ps.aor = QuotedAfter(s, "\"aor\": \"");
ps.contactUser = QuotedAfter(s, "\"contact_user\": \""); ps.contactUser = QuotedAfter(s, "\"contact_user\": \"");
ps.cseq = IntAfter(s, "\"cseq\": "); ps.cseq = IntAfter(s, "\"cseq\": ");
ps.spiUc = IntAfter(s, "\"spi_uc\": "); ps.spiUc = IntAfter(s, "\"spi_uc\": ");
@ -810,10 +809,6 @@ private:
// is re-learned from the next 200's P-Associated-URI. // is re-learned from the next 200's P-Associated-URI.
ppi_ = ps->ppi.value_or(""); ppi_ = ps->ppi.value_or("");
ss_ = sa->securityServer; ss_ = sa->securityServer;
// The public identity our requests are signed with (From) — restored
// here so a call placed before the refresh 200 re-learns it does not
// fall back to the barred temporary IMPU.
ctx_.aor = ps->aor.value_or("");
ctx_.route = route_; ctx_.ppi = ppi_; ctx_.securityServer = ss_; ctx_.route = route_; ctx_.ppi = ppi_; ctx_.securityServer = ss_;
ctx_.portUc = PortUc; ctx_.portUs = PortUs; ctx_.portUc = PortUc; ctx_.portUs = PortUs;
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)));
@ -839,7 +834,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_);
Log("REGISTERED (resumed + true-refreshed)"); Log("REGISTERED (resumed + true-refreshed)");
return true; return true;
} }
@ -938,7 +933,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_);
Log("REGISTERED (fresh)"); Log("REGISTERED (fresh)");
} }
@ -1056,7 +1051,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_);
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); }
@ -1092,7 +1087,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_);
Log("reconnected + re-registered"); Log("reconnected + re-registered");
SubscribeRegEvent(); SubscribeRegEvent();
if (!registered_) { registered_ = true; EmitStatus(true); } if (!registered_) { registered_ = true; EmitStatus(true); }
@ -1247,9 +1242,6 @@ private:
for (const auto& a : actions) { for (const auto& a : actions) {
switch (a.type) { switch (a.type) {
case T::SendClient: case T::SendClient:
// the outgoing INVITE is the one request a field log could
// never show (DUMP_SIP dumped inbound messages only)
if (a.text.starts_with("INVITE ")) DumpRaw("imsd-invite-out.raw", a.text);
if (!sip_.Send(a.text)) { sendOk = false; Log("client send failed"); } if (!sip_.Send(a.text)) { sendOk = false; Log("client send failed"); }
break; break;
case T::SendResponse: case T::SendResponse:

View file

@ -120,7 +120,6 @@ export namespace imsd::engine {
d_.itag = rng_.Token(10); // the To tag we mint = our dialog tag d_.itag = rng_.Token(10); // the To tag we mint = our dialog tag
d_.invBranch = rng_.Token(20); // unused as UAS; kept initialized d_.invBranch = rng_.Token(20); // unused as UAS; kept initialized
if (auto from = imsd::sip::Header(invite_, "From")) d_.dialogTo = std::string(*from); if (auto from = imsd::sip::Header(invite_, "From")) d_.dialogTo = std::string(*from);
if (auto to = imsd::sip::Header(invite_, "To")) d_.localUri = AngleUri(*to).value_or("");
if (auto ct = imsd::sip::Header(invite_, "Contact")) d_.remoteTarget = AngleUri(*ct); if (auto ct = imsd::sip::Header(invite_, "Contact")) d_.remoteTarget = AngleUri(*ct);
d_.ruri = d_.remoteTarget.value_or(""); d_.ruri = d_.remoteTarget.value_or("");
// The UAS route set is the Record-Route in RECEIVED order // The UAS route set is the Record-Route in RECEIVED order

View file

@ -290,28 +290,8 @@ export namespace imsd::msg {
std::optional<std::string> dialogTo; // remote To/From incl. tag, once known std::optional<std::string> dialogTo; // remote To/From incl. tag, once known
std::vector<std::string> recordRoute; // route set, UAC order (see DialogRoute) std::vector<std::string> recordRoute; // route set, UAC order (see DialogRoute)
std::optional<std::string> remoteTarget; // remote Contact URI std::optional<std::string> remoteTarget; // remote Contact URI
std::string localUri; // our URI in this dialog: the UAS takes the
// INVITE's To URI; empty = CallerId(c) (UAC)
}; };
// The identity we sign our own requests with: the registered public
// identity (P-Associated-URI sip: entry), else the tel: one, and the
// IMSI-derived temporary IMPU only before either is learned. 3GPP allows
// the temporary IMPU in REGISTER alone; a P-CSCF that does not overwrite
// From shows it to the callee (Telia, 2026-09-01: the reporter's IMSI on
// the far phone) and a strict one may reject the request.
inline std::string_view CallerId(const Context& c) {
if (!c.aor.empty()) return c.aor;
if (!c.ppi.empty()) return c.ppi;
return c.id.impu;
}
// From URI of a request inside a dialog (RFC 3261 12.2.1.1: the dialog's
// local URI — as UAS that is the INVITE's To, as UAC our caller identity).
inline std::string_view FromUri(const Context& c, const Dialog& d) {
return d.localUri.empty() ? CallerId(c) : std::string_view(d.localUri);
}
// Dialog route set: the reversed Record-Route, or the registration Route // Dialog route set: the reversed Record-Route, or the registration Route
// when none was recorded (imsd._dialog_route). // when none was recorded (imsd._dialog_route).
inline std::string DialogRoute(const Context& c, const Dialog& d) { inline std::string DialogRoute(const Context& c, const Dialog& d) {
@ -334,7 +314,7 @@ export namespace imsd::msg {
std::format("Via: SIP/2.0/TCP {};branch=z9hG4bK{};rport", imsd::util::HostPort(c.local, c.portUs), d.invBranch), std::format("Via: SIP/2.0/TCP {};branch=z9hG4bK{};rport", imsd::util::HostPort(c.local, 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={}", c.id.impu, d.itag),
std::format("To: <{}>", d.ruri), std::format("To: <{}>", d.ruri),
std::format("Call-ID: {}", d.callid), std::format("Call-ID: {}", d.callid),
"CSeq: 1 INVITE", "CSeq: 1 INVITE",
@ -367,7 +347,7 @@ export namespace imsd::msg {
ViaLine(c, "TCP", c.portUs, viaBranch), ViaLine(c, "TCP", 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={}", c.id.impu, d.itag),
std::format("To: {}", to), std::format("To: {}", to),
std::format("Call-ID: {}", d.callid), std::format("Call-ID: {}", d.callid),
std::format("CSeq: {} {}", cseq, method), std::format("CSeq: {} {}", cseq, method),
@ -394,7 +374,7 @@ export namespace imsd::msg {
ViaLine(c, "TCP", c.portUs, viaBranch), ViaLine(c, "TCP", 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={}", c.id.impu, d.itag),
std::format("To: {}", to), std::format("To: {}", to),
std::format("Call-ID: {}", d.callid), std::format("Call-ID: {}", d.callid),
"CSeq: 1 ACK", "CSeq: 1 ACK",
@ -411,7 +391,7 @@ export namespace imsd::msg {
std::format("Via: SIP/2.0/TCP {};branch=z9hG4bK{};rport", imsd::util::HostPort(c.local, c.portUs), d.invBranch), std::format("Via: SIP/2.0/TCP {};branch=z9hG4bK{};rport", imsd::util::HostPort(c.local, 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={}", c.id.impu, d.itag),
std::format("To: {}", toHeader), std::format("To: {}", toHeader),
std::format("Call-ID: {}", d.callid), std::format("Call-ID: {}", d.callid),
"CSeq: 1 ACK", "CSeq: 1 ACK",
@ -427,7 +407,7 @@ export namespace imsd::msg {
std::format("Via: SIP/2.0/TCP {};branch=z9hG4bK{};rport", imsd::util::HostPort(c.local, c.portUs), d.invBranch), std::format("Via: SIP/2.0/TCP {};branch=z9hG4bK{};rport", imsd::util::HostPort(c.local, 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={}", c.id.impu, d.itag),
std::format("To: <{}>", d.ruri), std::format("To: <{}>", d.ruri),
std::format("Call-ID: {}", d.callid), std::format("Call-ID: {}", d.callid),
"CSeq: 1 CANCEL", "CSeq: 1 CANCEL",

View file

@ -11,14 +11,7 @@
# #
# Configuration, via the unit's EnvironmentFile /etc/imsd.env (also read # Configuration, via the unit's EnvironmentFile /etc/imsd.env (also read
# directly so manual runs behave the same): # directly so manual runs behave the same):
# IMS_APN ims APN name (default ims)
# IMS_IP_TYPE bearer ip-type (default ipv6) # IMS_IP_TYPE bearer ip-type (default ipv6)
# IMS_PROFILE_ID 3GPP profile index to connect through (default: looked up
# by APN in the modem's profile list; empty/none = connect by
# APN string). A profile-indexed call is exempt from the
# modem's attach-family forcing that refuses an APN-string
# IPv6 request when the attach PDN was granted IPv4-only
# (journal/ims 2026-09-16/17 bench: Odido, O2 UK, Swisscom).
# IMS_REG_TIMEOUT max seconds to wait for network registration before # IMS_REG_TIMEOUT max seconds to wait for network registration before
# connect attempts start counting anyway (default 300) # connect attempts start counting anyway (default 300)
@ -41,9 +34,6 @@ envval() { # $1 = key — for manual runs; under systemd the vars are inherited
} }
IP_TYPE=${IMS_IP_TYPE:-$(envval IMS_IP_TYPE)} IP_TYPE=${IMS_IP_TYPE:-$(envval IMS_IP_TYPE)}
IP_TYPE=${IP_TYPE:-ipv6} IP_TYPE=${IP_TYPE:-ipv6}
IMS_APN=${IMS_APN:-$(envval IMS_APN)}
IMS_APN=${IMS_APN:-ims}
PROFILE_ID=${IMS_PROFILE_ID:-$(envval IMS_PROFILE_ID)}
REG_TIMEOUT=${IMS_REG_TIMEOUT:-$(envval IMS_REG_TIMEOUT)} REG_TIMEOUT=${IMS_REG_TIMEOUT:-$(envval IMS_REG_TIMEOUT)}
REG_TIMEOUT=${REG_TIMEOUT:-300} REG_TIMEOUT=${REG_TIMEOUT:-300}
@ -58,30 +48,11 @@ while :; do
done done
log "modem $MODEM" log "modem $MODEM"
# ---- the ims profile index (WDS profile list), unless configured
ims_profile_index() { # $1 = apn
qmicli -d qrtr://0 --wds-get-profile-list=3gpp 2>/dev/null | awk -v apn="$1" '
/^[ \t]*\[[0-9]+\] 3gpp/ { idx = $1; gsub(/[^0-9]/, "", idx) }
/APN:/ { a = $0; sub(/.*APN: '"'"'/, "", a); sub(/'"'"'.*/, "", a);
if (tolower(a) == tolower(apn) && idx != "") { print idx; exit } }'
}
if [ -z "$PROFILE_ID" ]; then
PROFILE_ID=$(ims_profile_index "$IMS_APN")
[ -n "$PROFILE_ID" ] && log "ims profile: index $PROFILE_ID (apn $IMS_APN)" || log "ims profile: none for apn $IMS_APN, connecting by APN string"
elif [ "$PROFILE_ID" = none ]; then
PROFILE_ID=
fi
# ---- find a connected ims bearer; else find-or-create one and connect it # ---- find a connected ims bearer; else find-or-create one and connect it
find_ims_bearer() { # $1 = required bearer.status.connected value find_ims_bearer() { # $1 = required bearer.status.connected value
for B in $(bearer_paths); do for B in $(bearer_paths); do
INFO=$(kv -b "$B") || continue INFO=$(kv -b "$B") || continue
if [ -n "$PROFILE_ID" ]; then echo "$INFO" | grep -q '^bearer\.properties\.apn *: *ims$' || continue
echo "$INFO" | grep -q "^bearer\.properties\.profile-id *: *$PROFILE_ID\$" ||
echo "$INFO" | grep -q "^bearer\.properties\.apn *: *$IMS_APN\$" || continue
else
echo "$INFO" | grep -q "^bearer\.properties\.apn *: *$IMS_APN\$" || continue
fi
echo "$INFO" | grep -q "^bearer\.properties\.ip-type *: *$IP_TYPE\$" || continue echo "$INFO" | grep -q "^bearer\.properties\.ip-type *: *$IP_TYPE\$" || continue
echo "$INFO" | grep -q "^bearer\.status\.connected *: *$1\$" || continue echo "$INFO" | grep -q "^bearer\.status\.connected *: *$1\$" || continue
echo "$B" echo "$B"
@ -119,15 +90,10 @@ while [ -z "$BEARER" ]; do
[ "$n" -gt 10 ] && { log "bearer connect failed after 10 attempts"; exit 1; } [ "$n" -gt 10 ] && { log "bearer connect failed after 10 attempts"; exit 1; }
B=$(find_ims_bearer no) # reuse a stale disconnected ims bearer B=$(find_ims_bearer no) # reuse a stale disconnected ims bearer
if [ -z "$B" ]; then if [ -z "$B" ]; then
if [ -n "$PROFILE_ID" ]; then OUT=$(mmcli -m "$MODEM" --create-bearer="apn=ims,ip-type=$IP_TYPE" 2>&1)
SPEC="profile-id=$PROFILE_ID,ip-type=$IP_TYPE"
else
SPEC="apn=$IMS_APN,ip-type=$IP_TYPE"
fi
OUT=$(mmcli -m "$MODEM" --create-bearer="$SPEC" 2>&1)
B=$(printf '%s' "$OUT" | sed -n 's,.*\(/org/freedesktop/ModemManager1/Bearer/[0-9]*\).*,\1,p') B=$(printf '%s' "$OUT" | sed -n 's,.*\(/org/freedesktop/ModemManager1/Bearer/[0-9]*\).*,\1,p')
if [ -n "$B" ]; then if [ -n "$B" ]; then
log "created bearer $B ($SPEC)" log "created bearer $B (ip-type=$IP_TYPE)"
else else
log "create-bearer attempt $n failed: $(squash "$OUT"); retrying in 10 s" log "create-bearer attempt $n failed: $(squash "$OUT"); retrying in 10 s"
sleep 10 sleep 10

View file

@ -164,8 +164,6 @@ int main() {
CallMachine m(c, rng, "ims-call-1", "1233", 50004, false); CallMachine m(c, rng, "ims-call-1", "1233", 50004, false);
auto start = m.Start(); auto start = m.Start();
Check(Sent(start, "INVITE ") != nullptr, "Start sends INVITE"); Check(Sent(start, "INVITE ") != nullptr, "Start sends INVITE");
Check(Sent(start, "INVITE ")->text.contains("\r\nFrom: <tel:+31611111111>;tag="), "MO INVITE From is our public (tel:) identity, not the IMSI IMPU");
Check(!Sent(start, "INVITE ")->text.contains("From: <sip:001010123456789@"), "MO INVITE From never the temporary IMPU once an identity is known");
Check(Has(start, Action::Type::SetDeadline), "Start arms ring timeout"); Check(Has(start, Action::Type::SetDeadline), "Start arms ring timeout");
Check(m.State() == CallState::Dialing, "starts dialing"); Check(m.State() == CallState::Dialing, "starts dialing");
@ -612,8 +610,7 @@ int main() {
Check(bye && bye->text.starts_with("BYE sip:origgw@[2001:db8::9]:5060 SIP/2.0"), "BYE targets the caller's Contact"); Check(bye && bye->text.starts_with("BYE sip:origgw@[2001:db8::9]:5060 SIP/2.0"), "BYE targets the caller's Contact");
Check(bye && bye->text.contains("CSeq: 1 BYE\r\n"), "our UAS CSeq space starts at 1"); Check(bye && bye->text.contains("CSeq: 1 BYE\r\n"), "our UAS CSeq space starts at 1");
Check(bye && bye->text.contains("To: <sip:+31612345678@ims.mnc001.mcc001.3gppnetwork.org;user=phone>" ";tag=caller1\r\n"), "BYE To is the caller (their tag)"); Check(bye && bye->text.contains("To: <sip:+31612345678@ims.mnc001.mcc001.3gppnetwork.org;user=phone>" ";tag=caller1\r\n"), "BYE To is the caller (their tag)");
Check(bye && bye->text.contains("From: <sip:+31611111111@ims.mnc001.mcc001.3gppnetwork.org;user=phone>;tag="), "UAS BYE From is the INVITE's To URI (our tag), not the IMSI IMPU"); Check(bye && bye->text.contains("From: <sip:001010123456789@ims.mnc001.mcc001." "3gppnetwork.org>;tag="), "BYE From is us (our tag)");
Check(bye && !bye->text.contains("001010123456789@ims"), "UAS BYE carries no IMSI-derived identity");
// UAS route set: Record-Route in RECEIVED order // UAS route set: Record-Route in RECEIVED order
Check(bye && bye->text.contains("Route: <sip:[2001:db8::105]:5100;lr>, " "<sip:[2001:db8::9]:5060;lr>\r\n"), "BYE routes via Record-Route in received order"); Check(bye && bye->text.contains("Route: <sip:[2001:db8::105]:5100;lr>, " "<sip:[2001:db8::9]:5060;lr>\r\n"), "BYE routes via Record-Route in received order");
const Action* t = Find(ah, Action::Type::State); const Action* t = Find(ah, Action::Type::State);
@ -831,22 +828,6 @@ int main() {
Check(inv && inv->text.contains("a=rtpmap:100 telephone-event/8000\r\n") && !inv->text.contains("telephone-event/16000"), "CODECS=PCMA,PCMU: narrowband DTMF only"); Check(inv && inv->text.contains("a=rtpmap:100 telephone-event/8000\r\n") && !inv->text.contains("telephone-event/16000"), "CODECS=PCMA,PCMU: narrowband DTMF only");
} }
// ---- MO with the registered sip: identity learned (the normal case on a
// live network): From, CANCEL and BYE all carry it, never the IMSI
{
Context ca = MakeCtx();
ca.aor = "sip:+31611111111@ims.mnc001.mcc001.3gppnetwork.org";
imsd::util::Rng rng(61);
CallMachine m(ca, rng, "ims-call-30", "1233", 50004, false);
auto start = m.Start();
const Action* inv = Sent(start, "INVITE ");
Check(inv && inv->text.contains("\r\nFrom: <sip:+31611111111@ims.mnc001.mcc001.3gppnetwork.org>;tag="), "MO INVITE From is the registered sip: identity");
auto cancel = m.OnHangup();
const Action* can = Sent(cancel, "CANCEL ");
Check(can && can->text.contains("\r\nFrom: <sip:+31611111111@ims.mnc001.mcc001.3gppnetwork.org>;tag="), "CANCEL From is the same identity");
Check(can && !can->text.contains("001010123456789@ims"), "CANCEL carries no IMSI");
}
if (Failures == 0) std::println("Engine: all tests passed"); if (Failures == 0) std::println("Engine: all tests passed");
return Failures; return Failures;
} }

View file

@ -117,7 +117,7 @@ namespace {
"Via: SIP/2.0/TCP [2001:db8::db43]:45062;branch=z9hG4bKINVBRANCH0000000000;rport\r\n" "Via: SIP/2.0/TCP [2001:db8::db43]:45062;branch=z9hG4bKINVBRANCH0000000000;rport\r\n"
"Max-Forwards: 70\r\n" "Max-Forwards: 70\r\n"
"Route: <sip:[2001:db8::105]:6000;lr>\r\n" "Route: <sip:[2001:db8::105]:6000;lr>\r\n"
"From: <tel:+31611111111>;tag=ITAG123456\r\n" "From: <sip:001010123456789@ims.mnc001.mcc001.3gppnetwork.org>;tag=ITAG123456\r\n"
"To: <sip:1233;phone-context=ims.mnc001.mcc001.3gppnetwork.org@ims.mnc001.mcc001.3gppnetwork.org;user=phone>\r\n" "To: <sip:1233;phone-context=ims.mnc001.mcc001.3gppnetwork.org@ims.mnc001.mcc001.3gppnetwork.org;user=phone>\r\n"
"Call-ID: CALLID789@2001:db8::db43\r\n" "Call-ID: CALLID789@2001:db8::db43\r\n"
"CSeq: 1 INVITE\r\n" "CSeq: 1 INVITE\r\n"
@ -230,38 +230,6 @@ int main() {
Eq(BuildInvite(c, d, Sdp, /*precond=*/false), Invite, "INVITE + AMR-WB SDP offer"); Eq(BuildInvite(c, d, Sdp, /*precond=*/false), Invite, "INVITE + AMR-WB SDP offer");
} }
// ---- From carries our PUBLIC identity, never the IMSI-derived temporary
// IMPU once anything better is known (ledger F1: a Telia P-CSCF showed
// the reporter's IMSI to the callee). aor > tel: ppi > temp IMPU; the
// same URI on every request of the dialog; a UAS signs with the
// INVITE's To.
{
Dialog d;
d.ruri = RuriFor("1233", c.id.domain);
d.callid = "CID@x"; d.itag = "it"; d.invBranch = "ib";
Context c2 = MakeCtx();
c2.aor = "sip:+31611111111@ims.mnc001.mcc001.3gppnetwork.org";
std::string inv = BuildInvite(c2, d, "v=0\r\n", false);
Check(inv.contains("\r\nFrom: <sip:+31611111111@ims.mnc001.mcc001.3gppnetwork.org>;tag=it\r\n"), "INVITE From is the registered sip: identity when known");
Check(!inv.contains("001010123456789@ims"), "INVITE never carries the IMSI IMPU once the aor is known");
Check(inv.contains("\r\nP-Preferred-Identity: <tel:+31611111111>\r\n"), "P-Preferred-Identity unchanged");
Check(BuildCancel(c2, d).contains("\r\nFrom: <sip:+31611111111@ims.mnc001.mcc001.3gppnetwork.org>;tag=it\r\n"), "CANCEL From follows");
Check(BuildAckNon2xx(c2, d, "<sip:x@y>;tag=r").contains("\r\nFrom: <sip:+31611111111@ims.mnc001.mcc001.3gppnetwork.org>;tag=it\r\n"), "non-2xx ACK From follows");
d.dialogTo = "<sip:x@y>;tag=r"; d.remoteTarget = "sip:x@[2001:db8::9]:5060";
Check(BuildAck2xx(c2, d, "ab", "<sip:x@y>;tag=r").contains("\r\nFrom: <sip:+31611111111@ims.mnc001.mcc001.3gppnetwork.org>;tag=it\r\n"), "2xx ACK From follows");
Check(BuildInDialog(c2, d, "BYE", 2, "bb").contains("\r\nFrom: <sip:+31611111111@ims.mnc001.mcc001.3gppnetwork.org>;tag=it\r\n"), "BYE From follows");
// only tel: known (pre-aor): the tel: identity, as the pinned INVITE shows
Check(BuildInvite(c, d, "v=0\r\n", false).contains("\r\nFrom: <tel:+31611111111>;tag=it\r\n"), "INVITE From falls back to the tel: identity");
// nothing learned yet: the temporary IMPU is all there is
Context c4 = MakeCtx();
c4.ppi.clear();
Check(BuildInvite(c4, d, "v=0\r\n", false).contains("\r\nFrom: <sip:001010123456789@ims.mnc001.mcc001.3gppnetwork.org>;tag=it\r\n"), "INVITE From is the temporary IMPU only before any identity is learned");
// UAS: the dialog's local URI is the INVITE's To, whatever we registered as
Dialog du = d;
du.localUri = "sip:+31611111111@ims.mnc001.mcc001.3gppnetwork.org;user=phone";
Check(BuildInDialog(c4, du, "BYE", 1, "cc").contains("\r\nFrom: <sip:+31611111111@ims.mnc001.mcc001.3gppnetwork.org;user=phone>;tag=it\r\n"), "UAS BYE From is the INVITE's To URI");
}
// ---- emergency INVITE: byte-identical to the plain INVITE except the // ---- emergency INVITE: byte-identical to the plain INVITE except the
// Request-URI and To carry the service URN (RFC 5031 / TS 24.229 5.1.6) // Request-URI and To carry the service URN (RFC 5031 / TS 24.229 5.1.6)
{ {