From 7978b94502a3c073e99af1040affa634644e351f Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Tue, 8 Sep 2026 20:37:47 +0200 Subject: [PATCH 1/3] messages: sign our requests with the public identity, not the IMSI IMPU MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every request we originated — INVITE, CANCEL, both ACKs, in-dialog BYE and friends — put the IMSI-derived temporary IMPU in From. 3GPP allows that identity in REGISTER only; the same lesson was learned for the reg-event SUBSCRIBE (480) and never carried to calls. Most P-CSCFs overwrite From and hid it; a Telia node did not, and a reporter's IMSI appeared on the callee's screen (field report 2026-09-01). A strict P-CSCF may reject the INVITE outright. CallerId(): the registered sip: public identity (P-Associated-URI), else the tel: one, and the temporary IMPU only before either is learned. One helper feeds all five builders, so a dialog's From never drifts. As UAS the dialog's local URI is the INVITE's To (RFC 3261 12.2.1.1), stored on the Dialog, so an incoming call's BYE is signed the way the network addressed us. The identity is persisted in the state file and restored on warm resume, so a call placed before the refresh 200 re-learns it cannot fall back. DUMP_SIP now also writes the last outgoing INVITE (imsd-invite-out.raw): the one request a field log could never show. The byte-pinned INVITE fixture moves to the tel: identity its test context knows; new scenarios cover the sip: identity, the tel: fallback, the pre-learning case and the UAS BYE. Bench-verified on KPN 2026-09-08: outgoing INVITE From is the registered sip: identity, call accepted and carried; caller ID at the far end unchanged. --- README.md | 3 ++- implementations/main.cpp | 24 ++++++++++++++++-------- interfaces/Imsd-Engine.cppm | 1 + interfaces/Imsd-Messages.cppm | 30 +++++++++++++++++++++++++----- tests/Engine/main.cpp | 21 ++++++++++++++++++++- tests/Messages/main.cpp | 34 +++++++++++++++++++++++++++++++++- 6 files changed, 97 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 2b631e6..b4451ca 100644 --- a/README.md +++ b/README.md @@ -164,7 +164,8 @@ 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 | | `RTP_PORT` | `50004` | local RTP port advertised in SDP | | `PRECOND` | `0` | `1` offers SDP QoS preconditions | -| `DUMP_SIP` | `0` | `1` writes raw REGISTER-200/SUBSCRIBE-200/INVITE dumps (mode 0600) for debugging — they contain your IMSI/MSISDN and addresses | +| `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 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 | | `IMSD_MEDIA` | *(auto)* | path to `imsd-media` (default: next to `imsd`, else `/usr/libexec/imsd-media`) | diff --git a/implementations/main.cpp b/implementations/main.cpp index 0ee566c..c4705cf 100644 --- a/implementations/main.cpp +++ b/implementations/main.cpp @@ -411,12 +411,12 @@ std::string StateDir() { // tiny JSON reader/writer for the persisted registration context. 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) { +void PersistState(const RegState& r, const std::string& route, const std::string& ppi, const std::string& aor) { std::string j = std::format( "{{\"callid\": \"{}\", \"ftag\": \"{}\", \"cseq\": {}, \"spi_uc\": {}, " "\"spi_us\": {}, \"expiry\": {}, \"route\": \"{}\", \"ppi\": \"{}\", " - "\"contact_user\": \"{}\"}}", - r.callid, r.ftag, r.cseq, r.spiUc, r.spiUs, r.expiry, route, ppi, + "\"aor\": \"{}\", \"contact_user\": \"{}\"}}", + r.callid, r.ftag, r.cseq, r.spiUc, r.spiUs, r.expiry, route, ppi, aor, r.contactUser); std::string path = StatePath(); std::error_code ec; @@ -441,7 +441,7 @@ void DumpRaw(std::string_view name, std::string_view msg) { } struct PersistedState { - std::optional callid, ftag, route, ppi, contactUser; + std::optional callid, ftag, route, ppi, aor, contactUser; std::optional cseq, spiUc, spiUs, expiry; }; std::optional LoadState() { @@ -456,6 +456,7 @@ std::optional LoadState() { ps.ftag = QuotedAfter(s, "\"ftag\": \""); ps.route = QuotedAfter(s, "\"route\": \""); ps.ppi = QuotedAfter(s, "\"ppi\": \""); + ps.aor = QuotedAfter(s, "\"aor\": \""); ps.contactUser = QuotedAfter(s, "\"contact_user\": \""); ps.cseq = IntAfter(s, "\"cseq\": "); ps.spiUc = IntAfter(s, "\"spi_uc\": "); @@ -809,6 +810,10 @@ private: // is re-learned from the next 200's P-Associated-URI. ppi_ = ps->ppi.value_or(""); 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_.portUc = PortUc; ctx_.portUs = PortUs; Log(std::format("RESUME via existing SA (cseq {})", ps->cseq.value_or(1))); @@ -834,7 +839,7 @@ private: if (auto e = imsd::sip::GrantedExpires(okMsg)) reg_.expiry = *e; UpdateRouteAndPpi(okMsg); LogBindings(okMsg); - PersistState(reg_, route_, ppi_); + PersistState(reg_, route_, ppi_, ctx_.aor); Log("REGISTERED (resumed + true-refreshed)"); return true; } @@ -933,7 +938,7 @@ private: LogBindings(ok); if (route_.empty()) route_ = std::format("", imsd::util::HostPort(pcscf_, portPs)); ctx_.route = route_; ctx_.ppi = ppi_; - PersistState(reg_, route_, ppi_); + PersistState(reg_, route_, ppi_, ctx_.aor); Log("REGISTERED (fresh)"); } @@ -1051,7 +1056,7 @@ private: auto [msg, used] = Reregister(reg_.callid, reg_.ftag, reg_.cseq + 1, reg_.spiUc, reg_.spiUs); reg_.cseq = used; if (auto e = imsd::sip::GrantedExpires(msg)) reg_.expiry = *e; - PersistState(reg_, route_, ppi_); + PersistState(reg_, route_, ppi_, ctx_.aor); Log(std::format("keepalive re-REGISTER ok (cseq {})", used)); SubscribeRegEvent(); if (!registered_) { registered_ = true; EmitStatus(true); } @@ -1087,7 +1092,7 @@ private: if (auto e = imsd::sip::GrantedExpires(msg)) reg_.expiry = *e; UpdateRouteAndPpi(msg); LogBindings(msg); - PersistState(reg_, route_, ppi_); + PersistState(reg_, route_, ppi_, ctx_.aor); Log("reconnected + re-registered"); SubscribeRegEvent(); if (!registered_) { registered_ = true; EmitStatus(true); } @@ -1242,6 +1247,9 @@ private: for (const auto& a : actions) { switch (a.type) { 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"); } break; case T::SendResponse: diff --git a/interfaces/Imsd-Engine.cppm b/interfaces/Imsd-Engine.cppm index b91eef1..2a372d0 100644 --- a/interfaces/Imsd-Engine.cppm +++ b/interfaces/Imsd-Engine.cppm @@ -120,6 +120,7 @@ export namespace imsd::engine { d_.itag = rng_.Token(10); // the To tag we mint = our dialog tag 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 to = imsd::sip::Header(invite_, "To")) d_.localUri = AngleUri(*to).value_or(""); if (auto ct = imsd::sip::Header(invite_, "Contact")) d_.remoteTarget = AngleUri(*ct); d_.ruri = d_.remoteTarget.value_or(""); // The UAS route set is the Record-Route in RECEIVED order diff --git a/interfaces/Imsd-Messages.cppm b/interfaces/Imsd-Messages.cppm index 1d8bce1..d70acdf 100644 --- a/interfaces/Imsd-Messages.cppm +++ b/interfaces/Imsd-Messages.cppm @@ -290,8 +290,28 @@ export namespace imsd::msg { std::optional dialogTo; // remote To/From incl. tag, once known std::vector recordRoute; // route set, UAC order (see DialogRoute) std::optional 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 // when none was recorded (imsd._dialog_route). inline std::string DialogRoute(const Context& c, const Dialog& d) { @@ -314,7 +334,7 @@ export namespace imsd::msg { std::format("Via: SIP/2.0/TCP {};branch=z9hG4bK{};rport", imsd::util::HostPort(c.local, c.portUs), d.invBranch), "Max-Forwards: 70", std::format("Route: {}", c.route), - std::format("From: <{}>;tag={}", c.id.impu, d.itag), + std::format("From: <{}>;tag={}", FromUri(c, d), d.itag), std::format("To: <{}>", d.ruri), std::format("Call-ID: {}", d.callid), "CSeq: 1 INVITE", @@ -347,7 +367,7 @@ export namespace imsd::msg { ViaLine(c, "TCP", c.portUs, viaBranch), "Max-Forwards: 70", std::format("Route: {}", DialogRoute(c, d)), - std::format("From: <{}>;tag={}", c.id.impu, d.itag), + std::format("From: <{}>;tag={}", FromUri(c, d), d.itag), std::format("To: {}", to), std::format("Call-ID: {}", d.callid), std::format("CSeq: {} {}", cseq, method), @@ -374,7 +394,7 @@ export namespace imsd::msg { ViaLine(c, "TCP", c.portUs, viaBranch), "Max-Forwards: 70", std::format("Route: {}", DialogRoute(c, d)), - std::format("From: <{}>;tag={}", c.id.impu, d.itag), + std::format("From: <{}>;tag={}", FromUri(c, d), d.itag), std::format("To: {}", to), std::format("Call-ID: {}", d.callid), "CSeq: 1 ACK", @@ -391,7 +411,7 @@ export namespace imsd::msg { std::format("Via: SIP/2.0/TCP {};branch=z9hG4bK{};rport", imsd::util::HostPort(c.local, c.portUs), d.invBranch), "Max-Forwards: 70", std::format("Route: {}", c.route), - std::format("From: <{}>;tag={}", c.id.impu, d.itag), + std::format("From: <{}>;tag={}", FromUri(c, d), d.itag), std::format("To: {}", toHeader), std::format("Call-ID: {}", d.callid), "CSeq: 1 ACK", @@ -407,7 +427,7 @@ export namespace imsd::msg { std::format("Via: SIP/2.0/TCP {};branch=z9hG4bK{};rport", imsd::util::HostPort(c.local, c.portUs), d.invBranch), "Max-Forwards: 70", std::format("Route: {}", c.route), - std::format("From: <{}>;tag={}", c.id.impu, d.itag), + std::format("From: <{}>;tag={}", FromUri(c, d), d.itag), std::format("To: <{}>", d.ruri), std::format("Call-ID: {}", d.callid), "CSeq: 1 CANCEL", diff --git a/tests/Engine/main.cpp b/tests/Engine/main.cpp index 2ad2f25..504e15f 100644 --- a/tests/Engine/main.cpp +++ b/tests/Engine/main.cpp @@ -164,6 +164,8 @@ int main() { CallMachine m(c, rng, "ims-call-1", "1233", 50004, false); auto start = m.Start(); Check(Sent(start, "INVITE ") != nullptr, "Start sends INVITE"); + Check(Sent(start, "INVITE ")->text.contains("\r\nFrom: ;tag="), "MO INVITE From is our public (tel:) identity, not the IMSI IMPU"); + Check(!Sent(start, "INVITE ")->text.contains("From: 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("To: " ";tag=caller1\r\n"), "BYE To is the caller (their tag)"); - Check(bye && bye->text.contains("From: ;tag="), "BYE From is us (our tag)"); + Check(bye && bye->text.contains("From: ;tag="), "UAS BYE From is the INVITE's To URI (our tag), not the IMSI IMPU"); + Check(bye && !bye->text.contains("001010123456789@ims"), "UAS BYE carries no IMSI-derived identity"); // UAS route set: Record-Route in RECEIVED order Check(bye && bye->text.contains("Route: , " "\r\n"), "BYE routes via Record-Route in received order"); const Action* t = Find(ah, Action::Type::State); @@ -828,6 +831,22 @@ 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"); } + // ---- 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: ;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: ;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"); return Failures; } diff --git a/tests/Messages/main.cpp b/tests/Messages/main.cpp index 3ee9ba2..0c9eea6 100644 --- a/tests/Messages/main.cpp +++ b/tests/Messages/main.cpp @@ -117,7 +117,7 @@ namespace { "Via: SIP/2.0/TCP [2001:db8::db43]:45062;branch=z9hG4bKINVBRANCH0000000000;rport\r\n" "Max-Forwards: 70\r\n" "Route: \r\n" - "From: ;tag=ITAG123456\r\n" + "From: ;tag=ITAG123456\r\n" "To: \r\n" "Call-ID: CALLID789@2001:db8::db43\r\n" "CSeq: 1 INVITE\r\n" @@ -230,6 +230,38 @@ int main() { 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: ;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: \r\n"), "P-Preferred-Identity unchanged"); + Check(BuildCancel(c2, d).contains("\r\nFrom: ;tag=it\r\n"), "CANCEL From follows"); + Check(BuildAckNon2xx(c2, d, ";tag=r").contains("\r\nFrom: ;tag=it\r\n"), "non-2xx ACK From follows"); + d.dialogTo = ";tag=r"; d.remoteTarget = "sip:x@[2001:db8::9]:5060"; + Check(BuildAck2xx(c2, d, "ab", ";tag=r").contains("\r\nFrom: ;tag=it\r\n"), "2xx ACK From follows"); + Check(BuildInDialog(c2, d, "BYE", 2, "bb").contains("\r\nFrom: ;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: ;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: ;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: ;tag=it\r\n"), "UAS BYE From is the INVITE's To URI"); + } + // ---- 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) { From dce941e0823e9b9b0a60845ef28ebd0c21d0f1f2 Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Tue, 8 Sep 2026 20:38:14 +0200 Subject: [PATCH 2/3] imsd 0.3.4 0.3.3 plus: our requests are signed with the registered public identity instead of the IMSI-derived temporary IMPU (the caller-ID leak a Telia P-CSCF exposed), the identity survives a warm resume, and DUMP_SIP also keeps the last outgoing INVITE. --- implementations/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/implementations/main.cpp b/implementations/main.cpp index c4705cf..be113ef 100644 --- a/implementations/main.cpp +++ b/implementations/main.cpp @@ -41,7 +41,7 @@ import Imsd; namespace { // ---- static config (env-overridable, same knobs as imsd.py) --------------- -constexpr const char* Version = "0.3.3"; +constexpr const char* Version = "0.3.4"; constexpr const char* BusName = "net.catcrafts.IMS1"; constexpr const char* ObjPath = "/net/catcrafts/IMS1"; constexpr const char* Iface = "net.catcrafts.IMS1"; From 587b06b5831f4f4b14598c7d6843d3aec23aed18 Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Thu, 17 Sep 2026 13:45:19 +0200 Subject: [PATCH 3/3] ims-pdn-up: connect the ims bearer by profile index, not by APN string On carriers whose attach PDN is granted IPv4-only (Odido, Swisscom, O2 UK so far) the modem refuses every AP-side IPv6 bearer request made by APN string with pdn-ipv6-call-disallowed before it reaches the air, so the ims PDN never came up and imsd looped. A request made by 3GPP profile index is treated as the IMS-class call the modem's own engine makes and is exempt: it goes on the air as IPv6 and the network grants it. That is the path 81voltd uses. Look the ims profile up by APN in the modem's profile list and create the bearer with profile-id=,ip-type=; fall back to the APN string when no such profile exists. A profile-indexed bearer reports no APN, so the reuse match accepts the profile id too. New knobs in /etc/imsd.env: IMS_APN (default ims) and IMS_PROFILE_ID (default: looked up; "none" forces the APN-string path). Verified on KPN with the wall induced by a forced IPv4 attach: the APN-string request is refused, the profile-indexed request connects, a second run reuses the bearer, imsd registers over it; attach and mobile data untouched. --- packaging/ims-pdn-up.sh | 40 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/packaging/ims-pdn-up.sh b/packaging/ims-pdn-up.sh index 9632ae2..5c50877 100644 --- a/packaging/ims-pdn-up.sh +++ b/packaging/ims-pdn-up.sh @@ -11,7 +11,14 @@ # # Configuration, via the unit's EnvironmentFile /etc/imsd.env (also read # directly so manual runs behave the same): +# IMS_APN ims APN name (default ims) # 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 # connect attempts start counting anyway (default 300) @@ -34,6 +41,9 @@ envval() { # $1 = key — for manual runs; under systemd the vars are inherited } IP_TYPE=${IMS_IP_TYPE:-$(envval IMS_IP_TYPE)} 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=${REG_TIMEOUT:-300} @@ -48,11 +58,30 @@ while :; do done 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_ims_bearer() { # $1 = required bearer.status.connected value for B in $(bearer_paths); do INFO=$(kv -b "$B") || continue - echo "$INFO" | grep -q '^bearer\.properties\.apn *: *ims$' || continue + if [ -n "$PROFILE_ID" ]; then + 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\.status\.connected *: *$1\$" || continue echo "$B" @@ -90,10 +119,15 @@ while [ -z "$BEARER" ]; do [ "$n" -gt 10 ] && { log "bearer connect failed after 10 attempts"; exit 1; } B=$(find_ims_bearer no) # reuse a stale disconnected ims bearer if [ -z "$B" ]; then - OUT=$(mmcli -m "$MODEM" --create-bearer="apn=ims,ip-type=$IP_TYPE" 2>&1) + if [ -n "$PROFILE_ID" ]; then + 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') if [ -n "$B" ]; then - log "created bearer $B (ip-type=$IP_TYPE)" + log "created bearer $B ($SPEC)" else log "create-bearer attempt $n failed: $(squash "$OUT"); retrying in 10 s" sleep 10