// SPDX-License-Identifier: GPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // lint-disable-file fixed-width-types /* Imsd:Qmi — the bytes of the QMI WDS exchanges imsd-pcscf has with the modem to learn the ims PDN's P-CSCF list, and the readers of the answers. Over QRTR a QMI message is {type u8, transaction u16, message u16, length u16} followed by TLVs {type u8, length u16, value}, integers little endian. The modem hands the P-CSCF addresses the network put in the PDN's Protocol Configuration Options to a WDS client bound to the data port that carries the call (Bind Mux Data Port, Set IP Family, Get Current Settings) — in the TLVs libqmi models (0x22 the flag, 0x23 the IPv4 list, 0x24 the FQDN list) and in one it does not: 0x2E, the IPv6 list, Qualcomm's layout as read off the Fairphone 6 (a count byte, then 16 bytes per address, PCO order). The socket, the service lookup and the sequence are the helper's (implementations/pcscf.cpp); this partition only shapes bytes and reads answers, pinned by tests/Qmi on a response in the exact shape the modem returned, addresses re-encoded to the documentation prefix. */ export module Imsd:Qmi; import std; import :Util; export namespace imsd::qmi { constexpr std::uint32_t WdsService = 1; constexpr std::uint8_t TypeRequest = 0x00; constexpr std::uint8_t TypeResponse = 0x02; constexpr std::uint8_t TypeIndication = 0x04; constexpr std::uint16_t MsgGetPacketServiceStatus = 0x0022; constexpr std::uint16_t MsgGetProfileSettings = 0x002B; constexpr std::uint16_t MsgGetCurrentSettings = 0x002D; constexpr std::uint16_t MsgSetIpFamily = 0x004D; constexpr std::uint16_t MsgBindMuxDataPort = 0x00A2; // Get Current Settings "Requested Settings": every bit libqmi defines // (0..18). The P-CSCF ones are 10 (address), 11 (server address list) // and 12 (domain name list); the whole mask is what the probe used. constexpr std::uint32_t RequestedSettingsAll = 0x7FFFF; // The data endpoint of an IPA-attached modem as ModemManager binds it: // type embedded (4), interface number 1. constexpr std::uint32_t EndpointEmbedded = 4; constexpr std::uint32_t EndpointInterfaceIpa = 1; constexpr std::uint8_t IpFamilyV6 = 6; constexpr std::uint8_t ProfileType3gpp = 0; constexpr std::uint8_t PacketServiceConnected = 2; constexpr std::uint16_t ErrOutOfCall = 15; constexpr std::uint16_t ErrNoEffect = 26; struct Tlv { std::uint8_t type = 0; std::vector value; }; namespace detail { inline void Put16(std::vector& b, std::uint16_t v) { b.push_back(static_cast(v & 0xFF)); b.push_back(static_cast(v >> 8)); } inline void Put32(std::vector& b, std::uint32_t v) { Put16(b, static_cast(v & 0xFFFF)); Put16(b, static_cast(v >> 16)); } inline std::uint16_t Get16(std::span b, std::size_t at) { return static_cast(b[at] | (b[at + 1] << 8)); } inline std::uint32_t Get32(std::span b, std::size_t at) { return static_cast(Get16(b, at)) | (static_cast(Get16(b, at + 2)) << 16); } // RFC 5952 text of 16 address bytes (via the canonicaliser: the // partition has no inet_ntop) inline std::string Ip6Text(std::span a) { std::string s; for (std::size_t i = 0; i < 16; i += 2) s += std::format("{}{:x}", i ? ":" : "", (a[i] << 8) | a[i + 1]); return imsd::util::CanonicalIp6(s).value_or(s); } } inline std::vector TlvBytes(std::uint8_t type, std::span value) { std::vector b; b.push_back(type); detail::Put16(b, static_cast(value.size())); b.insert(b.end(), value.begin(), value.end()); return b; } inline std::vector Tlv8(std::uint8_t type, std::uint8_t v) { std::array a{v}; return TlvBytes(type, a); } inline std::vector Tlv32(std::uint8_t type, std::uint32_t v) { std::vector a; detail::Put32(a, v); return TlvBytes(type, a); } // A request: the header, then the TLVs as given. inline std::vector Request(std::uint16_t txn, std::uint16_t msg, std::span tlvs) { std::vector b; b.push_back(TypeRequest); detail::Put16(b, txn); detail::Put16(b, msg); detail::Put16(b, static_cast(tlvs.size())); b.insert(b.end(), tlvs.begin(), tlvs.end()); return b; } // Bind Mux Data Port: TLV 0x10 {endpoint type u32, interface number // u32}, TLV 0x11 mux id u8 — the binding ModemManager gives its own // bearer clients, which is what lets this client read their call. inline std::vector BindMuxDataPort(std::uint16_t txn, std::uint32_t epType, std::uint32_t epIface, std::uint8_t mux) { std::vector ep; detail::Put32(ep, epType); detail::Put32(ep, epIface); std::vector t = TlvBytes(0x10, ep); std::vector m = Tlv8(0x11, mux); t.insert(t.end(), m.begin(), m.end()); return Request(txn, MsgBindMuxDataPort, t); } // Set IP Family: TLV 0x01 u8 (4 or 6) — which family's settings the // client asks about. inline std::vector SetIpFamily(std::uint16_t txn, std::uint8_t family) { return Request(txn, MsgSetIpFamily, Tlv8(0x01, family)); } inline std::vector GetPacketServiceStatus(std::uint16_t txn) { return Request(txn, MsgGetPacketServiceStatus, {}); } // Get Current Settings: TLV 0x10 the requested-settings mask u32. inline std::vector GetCurrentSettings(std::uint16_t txn, std::uint32_t mask) { return Request(txn, MsgGetCurrentSettings, Tlv32(0x10, mask)); } // Get Profile Settings: TLV 0x01 {profile type u8 (0 = 3GPP), index u8}. inline std::vector GetProfileSettings(std::uint16_t txn, std::uint8_t index) { std::array id{ProfileType3gpp, index}; return Request(txn, MsgGetProfileSettings, TlvBytes(0x01, id)); } struct Response { std::uint8_t type = 0; std::uint16_t txn = 0; std::uint16_t msg = 0; std::vector tlvs; bool hasResult = false; // TLV 0x02 {result u16, error u16} present std::uint16_t result = 0; std::uint16_t error = 0; bool Ok() const { return hasResult && result == 0; } const Tlv* Find(std::uint8_t t) const { for (const Tlv& x : tlvs) if (x.type == t) return &x; return nullptr; } }; // A received message: the header and every TLV; nullopt when the buffer // is shorter than its header says or a TLV overruns it. inline std::optional ParseMessage(std::span b) { if (b.size() < 7) return std::nullopt; Response r; r.type = b[0]; r.txn = detail::Get16(b, 1); r.msg = detail::Get16(b, 3); std::size_t len = detail::Get16(b, 5); if (7 + len > b.size()) return std::nullopt; std::size_t off = 7; std::size_t end = 7 + len; while (off + 3 <= end) { Tlv t; t.type = b[off]; std::size_t n = detail::Get16(b, off + 1); if (off + 3 + n > end) return std::nullopt; t.value.assign(b.begin() + static_cast(off + 3), b.begin() + static_cast(off + 3 + n)); if (t.type == 0x02 && n == 4) { r.hasResult = true; r.result = detail::Get16(t.value, 0); r.error = detail::Get16(t.value, 2); } r.tlvs.push_back(std::move(t)); off += 3 + n; } if (off != end) return std::nullopt; return r; } // The QMI protocol error names imsd-pcscf can meet (libqmi's nicks). inline std::string_view ErrorName(std::uint16_t e) { switch (e) { case 0: return "None"; case 1: return "MalformedMessage"; case 3: return "Internal"; case 9: return "InvalidHandle"; case 10: return "InvalidProfile"; case 14: return "CallFailed"; case 15: return "OutOfCall"; case 17: return "MissingArgument"; case 23: return "DeviceInUse"; case 26: return "NoEffect"; case 46: return "GeneralError"; case 48: return "InvalidArgument"; case 71: return "InvalidQmiCommand"; case 82: return "AccessDenied"; default: return "?"; } } // What a Get Current Settings answer says about the P-CSCF. struct PcscfLists { bool viaPco = false; // TLV 0x22: the modem asked for P-CSCF addresses in the PCO std::vector ipv6; // TLV 0x2E, PCO order, canonical std::vector ipv4; // TLV 0x23, dotted quads std::vector fqdn; // TLV 0x24 }; // Tolerant: a list whose count does not match its length is read as // empty, never as garbage. inline PcscfLists ParsePcscfLists(const Response& r) { PcscfLists out; if (const Tlv* f = r.Find(0x22); f && f->value.size() == 1) out.viaPco = f->value[0] != 0; if (const Tlv* t = r.Find(0x2E); t && !t->value.empty() && t->value.size() == 1 + 16 * static_cast(t->value[0])) for (std::size_t i = 0; i < t->value[0]; i++) out.ipv6.push_back(detail::Ip6Text(std::span(t->value).subspan(1 + 16 * i, 16))); if (const Tlv* t = r.Find(0x23); t && !t->value.empty() && t->value.size() == 1 + 4 * static_cast(t->value[0])) for (std::size_t i = 0; i < t->value[0]; i++) { std::uint32_t v = detail::Get32(t->value, 1 + 4 * i); // little-endian u32, a.b.c.d = the high byte first out.ipv4.push_back(std::format("{}.{}.{}.{}", v >> 24, (v >> 16) & 0xFF, (v >> 8) & 0xFF, v & 0xFF)); } if (const Tlv* t = r.Find(0x24); t && !t->value.empty()) { std::vector names; std::size_t off = 1; bool good = true; for (std::size_t i = 0; i < t->value[0] && good; i++) { if (off + 2 > t->value.size()) { good = false; break; } std::size_t n = detail::Get16(t->value, off); if (off + 2 + n > t->value.size()) { good = false; break; } names.emplace_back(reinterpret_cast(t->value.data() + off + 2), n); off += 2 + n; } if (good && off == t->value.size()) out.fqdn = std::move(names); } return out; } // Get Packet Service Status: TLV 0x01 u8 (2 = connected) — whether the // bound client sees the port's call. inline std::optional PacketServiceStatus(const Response& r) { const Tlv* t = r.Find(0x01); if (!t || t->value.size() != 1) return std::nullopt; return t->value[0]; } // Get Profile Settings: TLV 0x1F "PCSCF Address Using PCO" u8 — whether // the profile has the modem ask the network at all. inline std::optional ProfilePcscfViaPco(const Response& r) { const Tlv* t = r.Find(0x1F); if (!t || t->value.size() != 1) return std::nullopt; return t->value[0] != 0; } }