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.
239 lines
12 KiB
C++
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;
|
|
}
|