imsd/implementations/pcscf.cpp
Jorijn van der Graaf a89ca5a60c
qmi, imsd-pcscf: read the ims PDN's P-CSCF list from the modem's runtime settings
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.
2026-10-13 22:28:02 +02:00

239 lines
12 KiB
C++

// SPDX-License-Identifier: GPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// lint-disable-file fixed-width-types no-char-pointer
/*
imsd-pcscf — prints the P-CSCF list the network delivered in the ims PDN's
Protocol Configuration Options, read from the modem over QMI.
ModemManager owns the data call and never asks the modem for these fields;
a second WDS client gets them once it is bound to the same mux data port
(the binding ModemManager gives its own clients), so this is a reader on
the side: Bind Mux Data Port, Set IP Family 6, Get Current Settings, over
one AF_QIPCRTR socket, which is its own QMI client as far as the modem is
concerned — no proxy, no interference with ModemManager's. It never sends
Start Network or Stop Network: it changes nothing about the call. The
bytes are Imsd:Qmi's; this file is the socket, the service lookup and the
sequence.
Usage: imsd-pcscf --mux <id> [--ep-type 4] [--ep-iface 1] [--node 0]
[--profile <idx>] [--timeout <s>] [-v]
--mux the rmnet mux id of the ims netdev (`ip -d link show <dev>`)
--ep-type, --ep-iface
the modem's data endpoint: embedded (4), interface 1 for an
IPA-attached modem (what ModemManager binds for the `ipa`
driver)
--node the QRTR node of the modem's WDS service (0)
--profile the ims profile's 3GPP index: adds the profile's "PCSCF Address
Using PCO" flag to the output (did the modem ask the network at
all?)
--timeout the whole run's budget in seconds (10)
-v every request and answer in hex, on stderr
Output, one item per line:
<ipv6 address> the P-CSCF list, PCO order
v4 <address> an IPv4 P-CSCF (unusable on an IPv6 PDN)
fqdn <name> a P-CSCF name
flag pco-requested <0|1> the call's TLV 0x22
flag profile-pcscf-via-pco <0|1> the profile's flag (with --profile)
Exit: 0 at least one IPv6 address; 2 none; 1 an error (text on stderr —
`OutOfCall` means the bound client does not see a call on that mux).
*/
#include <errno.h>
#include <linux/qrtr.h>
#include <poll.h>
#include <sys/socket.h>
#include <unistd.h>
import std;
import Imsd;
namespace {
bool Verbose = false;
void Err(std::string_view s) { std::println(std::cerr, "imsd-pcscf: {}", s); }
std::string Hex(std::span<const std::uint8_t> b) {
std::string s;
for (std::uint8_t x : b) s += std::format("{}{:02x}", s.empty() ? "" : " ", x);
return s;
}
long NowMs() {
return std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now().time_since_epoch()).count();
}
// Ask the control port for the WDS servers; the first one on `node`.
// (The lookup is sent to node 1's control port — the local node on this
// platform — and to our own node's when that differs, as the modem's
// control port answers for the whole bus.)
std::optional<sockaddr_qrtr> FindWds(std::uint32_t node, long deadlineMs) {
int s = socket(AF_QIPCRTR, SOCK_DGRAM, 0);
if (s < 0) { Err(std::format("socket(AF_QIPCRTR): {}", std::strerror(errno))); return std::nullopt; }
qrtr_ctrl_pkt pkt{};
pkt.cmd = QRTR_TYPE_NEW_LOOKUP;
pkt.server.service = imsd::qmi::WdsService;
sockaddr_qrtr ctrl{};
ctrl.sq_family = AF_QIPCRTR;
ctrl.sq_node = 1;
ctrl.sq_port = QRTR_PORT_CTRL;
(void)!sendto(s, &pkt, sizeof pkt, 0, reinterpret_cast<sockaddr*>(&ctrl), sizeof ctrl);
sockaddr_qrtr me{};
socklen_t ml = sizeof me;
if (getsockname(s, reinterpret_cast<sockaddr*>(&me), &ml) == 0 && me.sq_node != 1) {
ctrl.sq_node = me.sq_node;
(void)!sendto(s, &pkt, sizeof pkt, 0, reinterpret_cast<sockaddr*>(&ctrl), sizeof ctrl);
}
std::optional<sockaddr_qrtr> found;
while (true) {
long left = deadlineMs - NowMs();
if (left <= 0) break;
pollfd p{s, POLLIN, 0};
if (poll(&p, 1, static_cast<int>(std::min(left, 2000L))) <= 0) break;
qrtr_ctrl_pkt in{};
sockaddr_qrtr from{};
socklen_t fl = sizeof from;
ssize_t n = recvfrom(s, &in, sizeof in, 0, reinterpret_cast<sockaddr*>(&from), &fl);
if (n < static_cast<ssize_t>(sizeof in)) continue;
if (in.cmd != QRTR_TYPE_NEW_SERVER) continue;
if (in.server.service == 0) break; // end of the dump
if (Verbose) Err(std::format("WDS server: instance {} at {}:{}", static_cast<unsigned>(in.server.instance), static_cast<unsigned>(in.server.node), static_cast<unsigned>(in.server.port)));
if (in.server.service == imsd::qmi::WdsService && in.server.node == node && !found) {
sockaddr_qrtr a{};
a.sq_family = AF_QIPCRTR;
a.sq_node = in.server.node;
a.sq_port = in.server.port;
found = a;
}
}
close(s);
return found;
}
// One QMI client: one socket, one transaction at a time.
class Wds {
public:
Wds(sockaddr_qrtr peer, long deadlineMs) : peer_(peer), deadline_(deadlineMs) { fd_ = socket(AF_QIPCRTR, SOCK_DGRAM, 0); }
~Wds() { if (fd_ >= 0) close(fd_); }
bool Open() const { return fd_ >= 0; }
std::uint16_t NextTxn() { return ++txn_; }
// Send a request and wait for its response (same transaction and
// message id); indications and strays are skipped.
std::optional<imsd::qmi::Response> Call(std::span<const std::uint8_t> req, std::uint16_t msg, std::string_view name) {
if (Verbose) Err(std::format("--> {}: {}", name, Hex(req)));
if (sendto(fd_, req.data(), req.size(), 0, reinterpret_cast<sockaddr*>(&peer_), sizeof peer_) < 0) {
Err(std::format("{}: sendto: {}", name, std::strerror(errno)));
return std::nullopt;
}
std::vector<std::uint8_t> buf(65536);
while (true) {
long left = deadline_ - NowMs();
if (left <= 0) { Err(std::format("{}: no answer in time", name)); return std::nullopt; }
pollfd p{fd_, POLLIN, 0};
int pr = poll(&p, 1, static_cast<int>(left));
if (pr <= 0) { Err(std::format("{}: no answer in time", name)); return std::nullopt; }
ssize_t n = recv(fd_, buf.data(), buf.size(), 0);
if (n <= 0) continue;
auto r = imsd::qmi::ParseMessage(std::span<const std::uint8_t>(buf.data(), static_cast<std::size_t>(n)));
if (!r) continue;
if (r->type != imsd::qmi::TypeResponse || r->txn != txn_ || r->msg != msg) {
if (Verbose) Err(std::format(" (skipped type {} txn {} msg 0x{:04x})", r->type, r->txn, r->msg));
continue;
}
if (Verbose) Err(std::format("<-- {}: {}", name, Hex(std::span<const std::uint8_t>(buf.data(), static_cast<std::size_t>(n)))));
return r;
}
}
private:
int fd_ = -1;
sockaddr_qrtr peer_;
long deadline_;
std::uint16_t txn_ = 0;
};
void Usage() {
std::println(std::cerr, "usage: imsd-pcscf --mux <id> [--ep-type 4] [--ep-iface 1] [--node 0] [--profile <idx>] [--timeout <s>] [-v]");
}
}
int main(int argc, char** argv) {
std::optional<int> mux;
std::optional<int> profile;
std::uint32_t epType = imsd::qmi::EndpointEmbedded;
std::uint32_t epIface = imsd::qmi::EndpointInterfaceIpa;
std::uint32_t node = 0;
double timeout = 10.0;
auto num = [&](int& i, const char* what) -> std::optional<long> {
if (i + 1 >= argc) { Err(std::format("{} needs a value", what)); return std::nullopt; }
std::string_view v(argv[++i]);
long out = 0;
auto [p, ec] = std::from_chars(v.data(), v.data() + v.size(), out);
if (ec != std::errc{} || p != v.data() + v.size()) { Err(std::format("{}: not a number: {}", what, v)); return std::nullopt; }
return out;
};
for (int i = 1; i < argc; i++) {
std::string_view a(argv[i]);
std::optional<long> v;
if (a == "--mux") { if (!(v = num(i, "--mux"))) return 1; mux = static_cast<int>(*v); }
else if (a == "--profile") { if (!(v = num(i, "--profile"))) return 1; profile = static_cast<int>(*v); }
else if (a == "--ep-type") { if (!(v = num(i, "--ep-type"))) return 1; epType = static_cast<std::uint32_t>(*v); }
else if (a == "--ep-iface") { if (!(v = num(i, "--ep-iface"))) return 1; epIface = static_cast<std::uint32_t>(*v); }
else if (a == "--node") { if (!(v = num(i, "--node"))) return 1; node = static_cast<std::uint32_t>(*v); }
else if (a == "--timeout") {
if (i + 1 >= argc) { Err("--timeout needs a value"); return 1; }
timeout = std::atof(argv[++i]);
if (timeout <= 0) { Err("--timeout: not a positive number"); return 1; }
}
else if (a == "-v") Verbose = true;
else { Usage(); return 1; }
}
if (!mux || *mux < 0 || *mux > 255) { Usage(); return 1; }
if (profile && (*profile < 0 || *profile > 255)) { Err("--profile: 0..255"); return 1; }
long deadline = NowMs() + static_cast<long>(timeout * 1000);
auto peer = FindWds(node, deadline);
if (!peer) { Err(std::format("no WDS service on qrtr node {}", node)); return 1; }
Wds c(*peer, deadline);
if (!c.Open()) { Err(std::format("socket(AF_QIPCRTR): {}", std::strerror(errno))); return 1; }
auto step = [&](std::vector<std::uint8_t> req, std::uint16_t msg, std::string_view name) -> std::optional<imsd::qmi::Response> {
auto r = c.Call(req, msg, name);
if (!r) return std::nullopt;
if (!r->Ok()) {
Err(std::format("{}: error {} ({})", name, r->error, imsd::qmi::ErrorName(r->error)));
return std::nullopt;
}
return r;
};
using namespace imsd::qmi;
if (!step(BindMuxDataPort(c.NextTxn(), epType, epIface, static_cast<std::uint8_t>(*mux)), MsgBindMuxDataPort, "Bind Mux Data Port")) return 1;
if (!step(SetIpFamily(c.NextTxn(), IpFamilyV6), MsgSetIpFamily, "Set IP Family")) return 1;
if (Verbose) {
if (auto r = step(GetPacketServiceStatus(c.NextTxn()), MsgGetPacketServiceStatus, "Get Packet Service Status"))
Err(std::format("packet service status: {}", PacketServiceStatus(*r) ? std::to_string(*PacketServiceStatus(*r)) : "?"));
}
auto settings = c.Call(GetCurrentSettings(c.NextTxn(), RequestedSettingsAll), MsgGetCurrentSettings, "Get Current Settings");
if (!settings) return 1;
if (!settings->Ok()) {
if (settings->error == ErrOutOfCall) Err(std::format("Get Current Settings: OutOfCall — the client bound to mux {} sees no call there (is that the ims netdev's mux id?)", *mux));
else Err(std::format("Get Current Settings: error {} ({})", settings->error, ErrorName(settings->error)));
return 1;
}
PcscfLists lists = ParsePcscfLists(*settings);
for (const std::string& a : lists.ipv6) std::println("{}", a);
for (const std::string& a : lists.ipv4) std::println("v4 {}", a);
for (const std::string& n : lists.fqdn) std::println("fqdn {}", n);
std::println("flag pco-requested {}", lists.viaPco ? 1 : 0);
if (profile) {
auto r = c.Call(GetProfileSettings(c.NextTxn(), static_cast<std::uint8_t>(*profile)), MsgGetProfileSettings, "Get Profile Settings");
if (r && r->Ok()) {
if (auto f = ProfilePcscfViaPco(*r)) std::println("flag profile-pcscf-via-pco {}", *f ? 1 : 0);
else Err(std::format("Get Profile Settings: profile {} carries no PCSCF-via-PCO flag", *profile));
} else if (r) {
Err(std::format("Get Profile Settings: error {} ({})", r->error, ErrorName(r->error)));
}
}
return lists.ipv6.empty() ? 2 : 0;
}