You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

667 lines
22 KiB
C++

// SPDX-FileCopyrightText: 2026 Reion Wong <reionwong@gmail.com>
// SPDX-License-Identifier: GPL-3.0-or-later
#include "networkmanager/client.h"
#include "networkmanager/variants.h"
#include <algorithm>
#include <strings.h>
#include <pwd.h>
#include <time.h>
#include <unistd.h>
#include "base/event_loop.h"
namespace cutefish::networkmanager {
using namespace std::chrono_literals;
using base::dbus::Error;
using base::dbus::Message;
using base::dbus::ObjectPath;
using base::dbus::Reply;
using namespace variants;
// A scan sees its access points over a few seconds before LastScan, and
// NetworkManager starts scans at least 8 s apart while connected.
static constexpr int64_t kScanSpreadMs = 5000;
// A scan drops the access points it missed as it ends; age drops them 250 s after one.
static constexpr int64_t kAgedOutAfterMs = 60 * 1000;
static constexpr int kMissedScansForgotten = 2;
// LastScan and LastSeen count CLOCK_BOOTTIME, which runs on through suspend.
static int64_t bootTimeMs()
{
timespec now;
clock_gettime(CLOCK_BOOTTIME, &now);
return int64_t(now.tv_sec) * 1000 + now.tv_nsec / 1000000;
}
static bool equalsIgnoringCase(const std::string& a, const std::string& b)
{
return a.size() == b.size() && strcasecmp(a.c_str(), b.c_str()) == 0;
}
Client::Client(base::dbus::Connection* bus)
: m_bus(bus)
{
m_changedTimer.setSingleShot(true);
m_changedTimer.setInterval(0ms);
m_changedTimer.onTimeout = [this] {
pruneRemembered();
changed.emit();
};
m_retryTimer.setSingleShot(true);
m_retryTimer.setInterval(2s);
m_retryTimer.onTimeout = [this] { refresh(); };
if (!m_bus)
return;
// Answers at once with the current owner, then on every change.
m_ownerWatch = m_bus->watchName(service, [this](const std::string& owner) {
reset();
if (!owner.empty())
load(owner);
});
}
Client::~Client() = default;
static thread_local std::unique_ptr<Client> sharedClient;
static thread_local base::dbus::Connection* instanceBus = nullptr;
Client* Client::instance()
{
if (!sharedClient) {
base::EventLoop* loop = base::EventLoop::current();
sharedClient = std::make_unique<Client>(instanceBus ? instanceBus : base::dbus::Connection::system(loop));
// Before the loop closes the bus the client's calls and matches live on.
if (loop)
loop->addDestroyHook([] { sharedClient.reset(); });
}
return sharedClient.get();
}
void Client::setInstanceBus(base::dbus::Connection* bus)
{
instanceBus = bus;
sharedClient.reset();
}
bool Client::has(const std::string& path, const std::string& interface) const
{
const auto object = m_objects.find(path);
return object != m_objects.end() && object->second.count(interface);
}
Client::VariantMap Client::properties(const std::string& path, const std::string& interface) const
{
const auto object = m_objects.find(path);
if (object != m_objects.end()) {
const auto found = object->second.find(interface);
return found == object->second.end() ? VariantMap() : found->second;
}
if (interface == accessPointInterface) {
const auto remembered = m_remembered.find(path);
if (remembered != m_remembered.end())
return remembered->second.properties;
}
return {};
}
Client::Variant Client::property(const std::string& path, const std::string& interface, const std::string& name) const
{
const auto object = m_objects.find(path);
if (object != m_objects.end()) {
const auto found = object->second.find(interface);
if (found == object->second.end())
return {};
return value(found->second, name);
}
if (interface == accessPointInterface) {
const auto remembered = m_remembered.find(path);
if (remembered != m_remembered.end())
return value(remembered->second.properties, name);
}
return {};
}
Client::Variant Client::manager(const std::string& name) const
{
return property(managerPath, managerInterface, name);
}
std::vector<std::string> Client::devices() const
{
return toStringList(manager("Devices"));
}
uint32_t Client::deviceType(const std::string& device) const
{
return toUInt(property(device, deviceInterface, "DeviceType"));
}
uint32_t Client::deviceState(const std::string& device) const
{
// StateReason carries the state and its reason together.
const Variant stateReason = property(device, deviceInterface, "StateReason");
if (stateReason.signature() == "(uu)" && !stateReason.items().empty())
return toUInt(stateReason.items().front());
return toUInt(property(device, deviceInterface, "State"));
}
std::string Client::activeConnection(const std::string& device) const
{
return objectPath(property(device, deviceInterface, "ActiveConnection"));
}
std::vector<std::string> Client::activeConnections() const
{
return toStringList(manager("ActiveConnections"));
}
std::vector<std::string> Client::accessPoints(const std::string& device) const
{
return toStringList(property(device, wirelessInterface, "AccessPoints"));
}
std::vector<std::string> Client::rememberedAccessPoints(const std::string& device) const
{
// A map keeps them sorted.
std::vector<std::string> remembered;
for (const auto& [path, accessPoint] : m_remembered) {
if (accessPoint.device == device && listedAccessPoint(device, accessPoint.properties).empty())
remembered.push_back(path);
}
return remembered;
}
bool Client::remembers(const std::string& accessPoint) const
{
return m_remembered.count(accessPoint);
}
std::string Client::accessPointDevice(const std::string& accessPoint) const
{
const auto remembered = m_remembered.find(accessPoint);
if (remembered != m_remembered.end())
return remembered->second.device;
if (!has(accessPoint, accessPointInterface))
return {};
const auto device = m_accessPointDevices.find(accessPoint);
return device == m_accessPointDevices.end() ? std::string() : device->second;
}
std::string Client::scannedAccessPoint(const std::string& accessPoint) const
{
if (has(accessPoint, accessPointInterface))
return accessPoint;
const auto remembered = m_remembered.find(accessPoint);
if (remembered == m_remembered.end())
return {};
return listedAccessPoint(remembered->second.device, remembered->second.properties);
}
std::string Client::listedAccessPoint(const std::string& device, const VariantMap& accessPoint) const
{
const std::string bssid = toString(value(accessPoint, "HwAddress"));
const std::string ssid = toBytes(value(accessPoint, "Ssid"));
std::string sameNetwork;
int strongest = -1;
for (const std::string& path : accessPoints(device)) {
const auto object = m_objects.find(path);
if (object == m_objects.end())
continue;
const auto found = object->second.find(accessPointInterface);
if (found == object->second.end())
continue;
const VariantMap& listed = found->second;
if (!bssid.empty() && equalsIgnoringCase(toString(value(listed, "HwAddress")), bssid))
return path;
const int strength = toInt(value(listed, "Strength"));
if (ssid.empty() || toBytes(value(listed, "Ssid")) != ssid || strength <= strongest)
continue;
bool sameSecurity = true;
for (const char* name : {"Mode", "Flags", "WpaFlags", "RsnFlags"})
sameSecurity = sameSecurity && toUInt(value(listed, name)) == toUInt(value(accessPoint, name));
if (!sameSecurity)
continue;
sameNetwork = path;
strongest = strength;
}
return sameNetwork;
}
std::vector<std::string> Client::connections() const
{
std::vector<std::string> paths;
paths.reserve(m_settings.size());
for (const auto& [path, settings] : m_settings)
paths.push_back(path);
return paths;
}
bool Client::readable(const std::string& connection) const
{
return m_settings.count(connection);
}
Settings Client::settings(const std::string& connection) const
{
const auto found = m_settings.find(connection);
return found == m_settings.end() ? Settings() : found->second;
}
Client::Variant Client::setting(const std::string& connection, const std::string& setting, const std::string& key) const
{
const auto profile = m_settings.find(connection);
if (profile == m_settings.end())
return {};
const auto group = profile->second.find(setting);
if (group == profile->second.end())
return {};
return value(group->second, key);
}
std::string Client::hotspotConnection() const
{
for (const auto& [connection, settings] : m_settings) {
const std::string mode = toString(setting(connection, "802-11-wireless", "mode"));
if ((mode == "ap" || mode == "adhoc") && toString(setting(connection, "ipv4", "method")) == "shared")
return connection;
}
return {};
}
std::string Client::permission(const std::string& name) const
{
const auto found = m_permissions.find(name);
return found == m_permissions.end() ? std::string() : found->second;
}
bool Client::sharesNewConnections() const
{
return permission("org.freedesktop.NetworkManager.settings.modify.system") == "yes";
}
std::string Client::userPermission()
{
const passwd* pw = ::getpwuid(::getuid());
return "user:" + std::string(pw ? pw->pw_name : "") + ":";
}
void Client::call(const std::string& path, const std::string& interface, const std::string& method,
Variant::Items arguments, const CallContext* context, Completion completion,
std::chrono::milliseconds timeout)
{
if (m_owner.empty()) {
if (completion)
completion(Reply({}, Error{"org.freedesktop.DBus.Error.ServiceUnknown", "NetworkManager is not running"}));
return;
}
Message message = m_bus->newMethodCall(m_owner, path, interface, method);
for (const Variant& argument : arguments)
message.appendAs(argument);
if (timeout >= authorizedTimeout)
message.setAllowInteractiveAuthorization(true);
const uint64_t id = ++m_nextPending;
auto finished = std::make_shared<bool>(false);
std::weak_ptr<char> token = (context ? context : &m_context)->token();
base::dbus::PendingCall pending = m_bus->callAsync(
message,
[this, id, finished, token, completion = std::move(completion)](const Reply& reply) {
*finished = true;
// The bus holds the slot until this returns.
m_pending.erase(id);
if (completion && !token.expired())
completion(reply);
},
timeout);
if (!*finished)
m_pending.emplace(id, std::move(pending));
}
void Client::setProperty(const std::string& path, const std::string& interface, const std::string& name,
const Variant& value, const CallContext* context, Completion completion)
{
call(path, propertiesInterface, "Set",
{Variant(interface), Variant(name), base::dbus::Type<Variant>::toVariant(value)}, context,
std::move(completion));
}
void Client::refresh()
{
if (m_loading || m_loaded || !m_bus)
return;
const uint64_t generation = m_generation;
base::dbus::Proxy daemon(m_bus, "org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus");
const uint64_t id = ++m_nextPending;
auto finished = std::make_shared<bool>(false);
base::dbus::PendingCall pending = daemon.callAsync(
"GetNameOwner",
[this, id, finished, generation](const Reply& reply) {
*finished = true;
m_pending.erase(id);
if (generation != m_generation || m_loading || m_loaded)
return;
if (const auto owner = reply.value<std::string>())
load(*owner);
},
service);
if (!*finished)
m_pending.emplace(id, std::move(pending));
}
void Client::reset()
{
++m_generation;
m_retryTimer.stop();
m_matches.clear();
m_owner.clear();
m_loading = false;
m_loaded = false;
std::vector<std::string> paths;
for (const auto& [path, object] : m_objects)
paths.push_back(path);
m_objects.clear();
m_settings.clear();
m_settingsRequests.clear();
m_permissions.clear();
m_accessPointDevices.clear();
m_remembered.clear();
for (const std::string& path : paths)
objectRemoved.emit(path);
if (m_ready) {
m_ready = false;
readyChanged.emit();
}
scheduleChanged();
}
void Client::load(const std::string& owner)
{
m_owner = owner;
m_loading = true;
// Subscribed before the fetch: a signal that arrives before its reply
// describes a state the reply already holds, so the handlers ignore
// everything while loading.
subscribe(owner);
const uint64_t generation = m_generation;
call(rootPath, objectManagerInterface, "GetManagedObjects", {}, nullptr, [this, generation](const Reply& reply) {
if (generation != m_generation)
return;
const auto objects = reply.value<std::map<ObjectPath, std::map<std::string, VariantMap>>>();
if (!objects) {
// NetworkManager most likely quit while answering.
reset();
m_retryTimer.start();
return;
}
for (const auto& [path, interfaces] : *objects)
addInterfaces(path.path, interfaces);
m_loading = false;
m_loaded = true;
fetchPermissions();
updateReady();
scheduleChanged();
});
}
void Client::subscribe(const std::string& owner)
{
const auto bind = [this, &owner](const std::string& path, const std::string& interface, const std::string& member,
std::function<void(const Message&)> handler) {
base::dbus::MatchRule rule;
rule.sender = owner;
rule.path = path;
rule.interface = interface;
rule.member = member;
m_matches.push_back(m_bus->subscribe(rule, std::move(handler)));
};
bind(rootPath, objectManagerInterface, "InterfacesAdded", [this](const Message& m) { interfacesAdded(m); });
bind(rootPath, objectManagerInterface, "InterfacesRemoved", [this](const Message& m) { interfacesRemoved(m); });
bind({}, propertiesInterface, "PropertiesChanged", [this](const Message& m) { propertiesChanged(m); });
bind({}, connectionInterface, "Updated", [this](const Message& m) { connectionUpdated(m); });
// The reason of an active connection's state exists only on this signal.
bind({}, activeInterface, "StateChanged", [this](const Message& m) { activeStateSignal(m); });
bind(managerPath, managerInterface, "CheckPermissions", [this](const Message&) { fetchPermissions(); });
}
void Client::addInterfaces(const std::string& path, const std::map<std::string, VariantMap>& interfaces)
{
auto& object = m_objects[path];
for (const auto& [interface, added] : interfaces) {
VariantMap& properties = object[interface];
for (const auto& [name, property] : added)
properties[name] = property;
}
if (interfaces.count(connectionInterface))
fetchSettings(path);
if (interfaces.count(wirelessInterface))
noteAccessPoints(path);
}
void Client::interfacesAdded(const Message& message)
{
if (m_loading)
return;
ObjectPath path;
std::map<std::string, VariantMap> interfaces;
if (!message.read(path, interfaces))
return;
addInterfaces(path.path, interfaces);
scheduleChanged();
}
void Client::interfacesRemoved(const Message& message)
{
if (m_loading)
return;
ObjectPath path;
std::vector<std::string> interfaces;
if (!message.read(path, interfaces))
return;
const auto object = m_objects.find(path.path);
if (object == m_objects.end())
return;
const auto removes = [&interfaces](const std::string& interface) {
return std::find(interfaces.begin(), interfaces.end(), interface) != interfaces.end();
};
if (removes(accessPointInterface)) {
const auto found = object->second.find(accessPointInterface);
rememberAccessPoint(path.path, found == object->second.end() ? VariantMap() : found->second);
}
for (const std::string& interface : interfaces)
object->second.erase(interface);
if (removes(connectionInterface)) {
m_settings.erase(path.path);
m_settingsRequests.erase(path.path);
}
if (object->second.empty()) {
m_objects.erase(object);
objectRemoved.emit(path.path);
}
updateReady();
scheduleChanged();
}
void Client::propertiesChanged(const Message& message)
{
std::string interface;
VariantMap properties;
// Counters tick for whoever set a refresh rate; nothing here shows them.
if (m_loading || !message.read(interface, properties) || interface == statisticsInterface)
return;
const std::string path = message.path();
const auto object = m_objects.find(path);
if (object == m_objects.end())
return;
const auto cached = object->second.find(interface);
if (cached == object->second.end())
return;
for (const auto& [name, property] : properties)
cached->second[name] = property;
if (interface == wirelessInterface) {
if (properties.count("AccessPoints"))
noteAccessPoints(path);
if (properties.count("LastScan"))
scanFinished(path);
}
scheduleChanged();
}
void Client::connectionUpdated(const Message& message)
{
if (m_loading || !has(message.path(), connectionInterface))
return;
fetchSettings(message.path());
}
void Client::activeStateSignal(const Message& message)
{
uint32_t state = 0;
uint32_t reason = 0;
if (m_loading || !message.read(state, reason))
return;
const std::string path = message.path();
if (has(path, activeInterface))
m_objects[path][activeInterface]["State"] = Variant(state);
activeStateChanged.emit(path, state, reason);
scheduleChanged();
}
void Client::fetchSettings(const std::string& path)
{
// A newer request makes an older reply stale, whichever arrives first.
const uint64_t request = ++m_nextRequest;
m_settingsRequests[path] = request;
const uint64_t generation = m_generation;
call(path, connectionInterface, "GetSettings", {}, nullptr, [this, generation, path, request](const Reply& reply) {
const auto pending = m_settingsRequests.find(path);
if (generation != m_generation || pending == m_settingsRequests.end() || pending->second != request)
return;
m_settingsRequests.erase(pending);
// Another user's private profile: it exists, but its settings are not ours to read.
if (const auto settings = reply.value<Settings>())
m_settings[path] = *settings;
else
m_settings.erase(path);
updateReady();
scheduleChanged();
});
}
void Client::fetchPermissions()
{
if (m_loading)
return;
const uint64_t request = ++m_nextRequest;
m_permissionsRequest = request;
const uint64_t generation = m_generation;
call(managerPath, managerInterface, "GetPermissions", {}, nullptr, [this, generation, request](const Reply& reply) {
if (generation != m_generation || request != m_permissionsRequest)
return;
m_permissions = reply.value<std::map<std::string, std::string>>().value_or(std::map<std::string, std::string>());
scheduleChanged();
});
}
void Client::scheduleChanged()
{
if (!m_changedTimer.isActive())
m_changedTimer.start();
}
void Client::updateReady()
{
// Once ready, later profile fetches don't take the network lists away again.
if (m_ready || !m_loaded || !m_settingsRequests.empty())
return;
m_ready = true;
readyChanged.emit();
}
// NetworkManager drops every access point when Wi-Fi goes off, the device
// goes down or asleep, or it starts a hotspot.
bool Client::keepsAccessPoints(const std::string& device) const
{
return has(device, wirelessInterface) && toBool(manager("WirelessEnabled"))
&& deviceState(device) >= DeviceDisconnected
&& toUInt(property(device, wirelessInterface, "Mode")) != ModeAp;
}
void Client::noteAccessPoints(const std::string& device)
{
for (const std::string& accessPoint : accessPoints(device))
m_accessPointDevices[accessPoint] = device;
}
void Client::rememberAccessPoint(const std::string& path, const VariantMap& properties)
{
const auto found = m_accessPointDevices.find(path);
if (found == m_accessPointDevices.end())
return;
const std::string device = found->second;
m_accessPointDevices.erase(found);
if (device.empty() || !keepsAccessPoints(device))
return;
// Seen by the last scan and gone long after it: it aged out rather than went away.
const int64_t lastScan = toInt64(property(device, wirelessInterface, "LastScan"), -1);
const int64_t lastSeen = contains(properties, "LastSeen") ? toInt64(value(properties, "LastSeen"), -1) : -1;
if (lastScan < 0 || lastSeen < 0 || lastSeen * 1000 < lastScan - kScanSpreadMs
|| bootTimeMs() - lastScan < kAgedOutAfterMs)
return;
m_remembered[path] = RememberedAccessPoint{device, properties, 0};
}
void Client::scanFinished(const std::string& device)
{
for (auto it = m_remembered.begin(); it != m_remembered.end();) {
if (it->second.device == device
&& (!listedAccessPoint(device, it->second.properties).empty()
|| ++it->second.missedScans >= kMissedScansForgotten))
it = m_remembered.erase(it);
else
++it;
}
}
void Client::pruneRemembered()
{
for (auto it = m_remembered.begin(); it != m_remembered.end();) {
if (keepsAccessPoints(it->second.device))
++it;
else
it = m_remembered.erase(it);
}
}
} // namespace cutefish::networkmanager