The network hands the P-CSCF addresses to the modem in the PDN's Protocol Configuration Options; ModemManager's bearer code does not ask the modem for them, qmicli (libqmi 1.39) prints none of them, and libqmi has no TLV for the IPv6 list (0x2E), which is why PCSCF= was a required setting. A second QMI WDS client reads them once it is bound to the bearer's mux data port (the binding ModemManager gives its own clients): Bind Mux Data Port, Set IP Family 6, Get Current Settings, over one AF_QIPCRTR socket. Imsd:Qmi holds the bytes and the readers (0x22 the flag, 0x23 IPv4, 0x24 FQDN, 0x2E IPv6 in Qualcomm's layout: a count, then 16 bytes each); imsd-pcscf is the socket, the service lookup and the sequence, and prints the list, one address per line; nothing runs it yet. It sends no Start Network and no Stop Network; the bearer stayed connected and the registration stood after its runs (the fp6 journal's probe capture 04, benches 38 and 39). The build, the CI cross-compile, the tarball and the apk gain the product; linux-headers is named in the sysroot for <linux/qrtr.h>. Tested: crafter-build test, 8 passed; the aarch64 build of this commit's helper on the dev phone (KPN) against the live ims bearer.
260 lines
12 KiB
C++
260 lines
12 KiB
C++
// 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<std::uint8_t> value;
|
|
};
|
|
|
|
namespace detail {
|
|
inline void Put16(std::vector<std::uint8_t>& b, std::uint16_t v) {
|
|
b.push_back(static_cast<std::uint8_t>(v & 0xFF));
|
|
b.push_back(static_cast<std::uint8_t>(v >> 8));
|
|
}
|
|
inline void Put32(std::vector<std::uint8_t>& b, std::uint32_t v) {
|
|
Put16(b, static_cast<std::uint16_t>(v & 0xFFFF));
|
|
Put16(b, static_cast<std::uint16_t>(v >> 16));
|
|
}
|
|
inline std::uint16_t Get16(std::span<const std::uint8_t> b, std::size_t at) {
|
|
return static_cast<std::uint16_t>(b[at] | (b[at + 1] << 8));
|
|
}
|
|
inline std::uint32_t Get32(std::span<const std::uint8_t> b, std::size_t at) {
|
|
return static_cast<std::uint32_t>(Get16(b, at)) | (static_cast<std::uint32_t>(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<const std::uint8_t> 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<std::uint8_t> TlvBytes(std::uint8_t type, std::span<const std::uint8_t> value) {
|
|
std::vector<std::uint8_t> b;
|
|
b.push_back(type);
|
|
detail::Put16(b, static_cast<std::uint16_t>(value.size()));
|
|
b.insert(b.end(), value.begin(), value.end());
|
|
return b;
|
|
}
|
|
inline std::vector<std::uint8_t> Tlv8(std::uint8_t type, std::uint8_t v) {
|
|
std::array<std::uint8_t, 1> a{v};
|
|
return TlvBytes(type, a);
|
|
}
|
|
inline std::vector<std::uint8_t> Tlv32(std::uint8_t type, std::uint32_t v) {
|
|
std::vector<std::uint8_t> a;
|
|
detail::Put32(a, v);
|
|
return TlvBytes(type, a);
|
|
}
|
|
|
|
// A request: the header, then the TLVs as given.
|
|
inline std::vector<std::uint8_t> Request(std::uint16_t txn, std::uint16_t msg, std::span<const std::uint8_t> tlvs) {
|
|
std::vector<std::uint8_t> b;
|
|
b.push_back(TypeRequest);
|
|
detail::Put16(b, txn);
|
|
detail::Put16(b, msg);
|
|
detail::Put16(b, static_cast<std::uint16_t>(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<std::uint8_t> BindMuxDataPort(std::uint16_t txn, std::uint32_t epType, std::uint32_t epIface, std::uint8_t mux) {
|
|
std::vector<std::uint8_t> ep;
|
|
detail::Put32(ep, epType);
|
|
detail::Put32(ep, epIface);
|
|
std::vector<std::uint8_t> t = TlvBytes(0x10, ep);
|
|
std::vector<std::uint8_t> 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<std::uint8_t> SetIpFamily(std::uint16_t txn, std::uint8_t family) {
|
|
return Request(txn, MsgSetIpFamily, Tlv8(0x01, family));
|
|
}
|
|
inline std::vector<std::uint8_t> GetPacketServiceStatus(std::uint16_t txn) {
|
|
return Request(txn, MsgGetPacketServiceStatus, {});
|
|
}
|
|
// Get Current Settings: TLV 0x10 the requested-settings mask u32.
|
|
inline std::vector<std::uint8_t> 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<std::uint8_t> GetProfileSettings(std::uint16_t txn, std::uint8_t index) {
|
|
std::array<std::uint8_t, 2> 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<Tlv> 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<Response> ParseMessage(std::span<const std::uint8_t> 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<std::ptrdiff_t>(off + 3), b.begin() + static_cast<std::ptrdiff_t>(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<std::string> ipv6; // TLV 0x2E, PCO order, canonical
|
|
std::vector<std::string> ipv4; // TLV 0x23, dotted quads
|
|
std::vector<std::string> 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<std::size_t>(t->value[0]))
|
|
for (std::size_t i = 0; i < t->value[0]; i++)
|
|
out.ipv6.push_back(detail::Ip6Text(std::span<const std::uint8_t>(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<std::size_t>(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<std::string> 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<const char*>(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<std::uint8_t> 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<bool> ProfilePcscfViaPco(const Response& r) {
|
|
const Tlv* t = r.Find(0x1F);
|
|
if (!t || t->value.size() != 1) return std::nullopt;
|
|
return t->value[0] != 0;
|
|
}
|
|
}
|