mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
2bea2e0bfc
- Convert refreshAccessPoints, refreshVpnConnections, and refreshSavedConnections from synchronous to async using callMethodAsync/uponReplyInvoke with shared_ptr lifetime management - Fix activateVpnConnection to use callMethodAsync to avoid blocking the main event loop - Fix proxy lifetimes: wrap locally-created sdbus::IProxy objects in shared_ptr and capture in reply lambdas so they outlive the call - Fix GetAll calls to use org.freedesktop.DBus.Properties interface with the target interface as an argument - Add activating-state detection so VPN button updates immediately - Fix -Wconversion and -Wshadow compiler warnings
1241 lines
52 KiB
C++
1241 lines
52 KiB
C++
#include "dbus/network/network_service.h"
|
|
|
|
#include "core/log.h"
|
|
#include "dbus/system_bus.h"
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cstdio>
|
|
#include <map>
|
|
#include <sdbus-c++/IProxy.h>
|
|
#include <sdbus-c++/Types.h>
|
|
#include <set>
|
|
#include <vector>
|
|
|
|
namespace {
|
|
|
|
constexpr Logger kLog("network");
|
|
|
|
const sdbus::ServiceName k_nmBusName{"org.freedesktop.NetworkManager"};
|
|
const sdbus::ObjectPath k_nmObjectPath{"/org/freedesktop/NetworkManager"};
|
|
constexpr auto k_nmInterface = "org.freedesktop.NetworkManager";
|
|
constexpr auto k_nmDeviceInterface = "org.freedesktop.NetworkManager.Device";
|
|
constexpr auto k_nmDeviceWirelessInterface = "org.freedesktop.NetworkManager.Device.Wireless";
|
|
constexpr auto k_nmSettingsInterface = "org.freedesktop.NetworkManager.Settings";
|
|
const sdbus::ObjectPath k_nmSettingsObjectPath{"/org/freedesktop/NetworkManager/Settings"};
|
|
constexpr auto k_nmSettingsConnectionInterface = "org.freedesktop.NetworkManager.Settings.Connection";
|
|
|
|
// NM80211ApSecurityFlags bits we care about.
|
|
constexpr std::uint32_t k_nm80211ApSecNone = 0x0;
|
|
constexpr auto k_nmActiveConnectionInterface = "org.freedesktop.NetworkManager.Connection.Active";
|
|
constexpr auto k_nmAccessPointInterface = "org.freedesktop.NetworkManager.AccessPoint";
|
|
constexpr auto k_nmIp4ConfigInterface = "org.freedesktop.NetworkManager.IP4Config";
|
|
constexpr auto k_propertiesInterface = "org.freedesktop.DBus.Properties";
|
|
|
|
// NMDeviceType values from NetworkManager D-Bus API.
|
|
constexpr std::uint32_t k_nmDeviceTypeWifi = 2;
|
|
|
|
// NMActiveConnectionState
|
|
constexpr std::uint32_t k_nmActiveConnectionStateActivating = 1;
|
|
constexpr std::uint32_t k_nmActiveConnectionStateActivated = 2;
|
|
|
|
template <typename T>
|
|
T getPropertyOr(sdbus::IProxy& proxy, std::string_view interfaceName, std::string_view propertyName, T fallback) {
|
|
try {
|
|
const sdbus::Variant value = proxy.getProperty(propertyName).onInterface(std::string(interfaceName));
|
|
return value.get<T>();
|
|
} catch (const sdbus::Error&) {
|
|
return fallback;
|
|
}
|
|
}
|
|
|
|
std::string ipv4FromUint(std::uint32_t addrLe) {
|
|
// NM stores IPv4 addresses as native-byte-order uint32 in network order bytes.
|
|
// I.e. the bytes a.b.c.d are laid out in memory low->high as a,b,c,d.
|
|
std::array<std::uint8_t, 4> bytes{};
|
|
bytes[0] = static_cast<std::uint8_t>(addrLe & 0xffU);
|
|
bytes[1] = static_cast<std::uint8_t>((addrLe >> 8) & 0xffU);
|
|
bytes[2] = static_cast<std::uint8_t>((addrLe >> 16) & 0xffU);
|
|
bytes[3] = static_cast<std::uint8_t>((addrLe >> 24) & 0xffU);
|
|
char buf[32];
|
|
std::snprintf(buf, sizeof(buf), "%u.%u.%u.%u", bytes[0], bytes[1], bytes[2], bytes[3]);
|
|
return std::string(buf);
|
|
}
|
|
|
|
std::string firstIpv4FromConfig(sdbus::IConnection& conn, const std::string& ip4ConfigPath) {
|
|
if (ip4ConfigPath.empty() || ip4ConfigPath == "/") {
|
|
return {};
|
|
}
|
|
try {
|
|
auto proxy = sdbus::createProxy(conn, k_nmBusName, sdbus::ObjectPath{ip4ConfigPath});
|
|
// Prefer "AddressData" (vector<dict<string,variant>>) since "Addresses" is deprecated.
|
|
try {
|
|
const sdbus::Variant value = proxy->getProperty("AddressData").onInterface(k_nmIp4ConfigInterface);
|
|
const auto data = value.get<std::vector<std::map<std::string, sdbus::Variant>>>();
|
|
for (const auto& entry : data) {
|
|
auto it = entry.find("address");
|
|
if (it != entry.end()) {
|
|
try {
|
|
return it->second.get<std::string>();
|
|
} catch (const sdbus::Error&) {
|
|
}
|
|
}
|
|
}
|
|
} catch (const sdbus::Error&) {
|
|
}
|
|
// Fallback: legacy Addresses (vector<vector<uint32>> — addr, prefix, gateway).
|
|
try {
|
|
const sdbus::Variant value = proxy->getProperty("Addresses").onInterface(k_nmIp4ConfigInterface);
|
|
const auto data = value.get<std::vector<std::vector<std::uint32_t>>>();
|
|
if (!data.empty() && !data.front().empty()) {
|
|
return ipv4FromUint(data.front().front());
|
|
}
|
|
} catch (const sdbus::Error&) {
|
|
}
|
|
} catch (const sdbus::Error&) {
|
|
}
|
|
return {};
|
|
}
|
|
|
|
// Tracks in-flight async refresh operations so we only emit state changes after all complete.
|
|
struct PendingRefresh {
|
|
std::vector<AccessPointInfo> capturedAps;
|
|
std::vector<VpnConnectionInfo> capturedVpns;
|
|
std::vector<std::string> capturedSaved;
|
|
std::atomic<int> pendingOps{0};
|
|
std::function<void()> onAllComplete;
|
|
};
|
|
|
|
} // namespace
|
|
|
|
const char* NetworkService::glyphForState(const NetworkState& state) noexcept {
|
|
if (state.vpnActive) {
|
|
return "shield-check";
|
|
}
|
|
if (state.kind == NetworkConnectivity::Wired) {
|
|
return state.connected ? "ethernet" : "ethernet-off";
|
|
}
|
|
return wifiGlyphForState(state);
|
|
}
|
|
|
|
const char* NetworkService::wifiGlyphForState(const NetworkState& state) noexcept {
|
|
if (!state.wirelessEnabled) {
|
|
return "wifi-off";
|
|
}
|
|
if (state.kind == NetworkConnectivity::Unknown) {
|
|
return "wifi-question";
|
|
}
|
|
if (state.kind == NetworkConnectivity::Wireless && state.connected) {
|
|
return wifiGlyphForSignal(state.signalStrength);
|
|
}
|
|
return "wifi-exclamation";
|
|
}
|
|
|
|
const char* NetworkService::wifiGlyphForSignal(std::uint8_t signal) noexcept {
|
|
if (signal >= 80) {
|
|
return "wifi";
|
|
}
|
|
if (signal >= 60) {
|
|
return "wifi-3";
|
|
}
|
|
if (signal >= 35) {
|
|
return "wifi-2";
|
|
}
|
|
if (signal >= 15) {
|
|
return "wifi-1";
|
|
}
|
|
return "wifi-0";
|
|
}
|
|
|
|
NetworkService::NetworkService(SystemBus& bus) : m_bus(bus) {
|
|
m_nm = sdbus::createProxy(m_bus.connection(), k_nmBusName, k_nmObjectPath);
|
|
|
|
m_nm->uponSignal("PropertiesChanged")
|
|
.onInterface(k_propertiesInterface)
|
|
.call([this](const std::string& interfaceName, const std::map<std::string, sdbus::Variant>& changedProperties,
|
|
const std::vector<std::string>& /*invalidatedProperties*/) {
|
|
if (interfaceName != k_nmInterface) {
|
|
return;
|
|
}
|
|
bool wirelessNowOn = false;
|
|
if (auto it = changedProperties.find("WirelessEnabled"); it != changedProperties.end()) {
|
|
try {
|
|
wirelessNowOn = it->second.get<bool>();
|
|
} catch (const sdbus::Error&) {
|
|
}
|
|
}
|
|
if (changedProperties.contains("PrimaryConnection") || changedProperties.contains("ActiveConnections") ||
|
|
changedProperties.contains("WirelessEnabled") || changedProperties.contains("State") ||
|
|
changedProperties.contains("Connectivity")) {
|
|
rebindActiveConnection();
|
|
}
|
|
if (wirelessNowOn) {
|
|
// NM powered the radio on but the wifi device is still transitioning
|
|
// out of Unavailable, so calling RequestScan now would be rejected.
|
|
// NM starts its own scan as soon as the device reaches Disconnected;
|
|
// just mark ourselves scanning and snapshot LastScan so the device
|
|
// PropertiesChanged watcher clears the flag when the scan finishes.
|
|
std::int64_t baseline = 0;
|
|
try {
|
|
std::vector<sdbus::ObjectPath> devices;
|
|
m_nm->callMethod("GetDevices").onInterface(k_nmInterface).storeResultsTo(devices);
|
|
for (const auto& devicePath : devices) {
|
|
try {
|
|
auto device = sdbus::createProxy(m_bus.connection(), k_nmBusName, devicePath);
|
|
const auto deviceType = getPropertyOr<std::uint32_t>(*device, k_nmDeviceInterface, "DeviceType", 0U);
|
|
if (deviceType != k_nmDeviceTypeWifi) {
|
|
continue;
|
|
}
|
|
const auto lastScan =
|
|
getPropertyOr<std::int64_t>(*device, k_nmDeviceWirelessInterface, "LastScan", std::int64_t{0});
|
|
if (lastScan > baseline) {
|
|
baseline = lastScan;
|
|
}
|
|
} catch (const sdbus::Error&) {
|
|
}
|
|
}
|
|
} catch (const sdbus::Error&) {
|
|
}
|
|
m_scanning = true;
|
|
m_scanBaselineLastScan = baseline;
|
|
refresh();
|
|
}
|
|
});
|
|
|
|
rebindActiveConnection();
|
|
}
|
|
|
|
NetworkService::~NetworkService() = default;
|
|
|
|
void NetworkService::setChangeCallback(ChangeCallback callback) { m_changeCallback = std::move(callback); }
|
|
|
|
void NetworkService::refresh() {
|
|
auto pending = std::make_shared<PendingRefresh>();
|
|
pending->capturedAps = m_accessPoints;
|
|
pending->capturedVpns = m_vpnConnections;
|
|
pending->capturedSaved = m_savedSsids;
|
|
pending->pendingOps = 3;
|
|
|
|
pending->onAllComplete = [this, pending]() {
|
|
NetworkState next = readState();
|
|
const bool apsChanged = pending->capturedAps != m_accessPoints;
|
|
const bool vpnsChanged = pending->capturedVpns != m_vpnConnections;
|
|
const bool savedChanged = pending->capturedSaved != m_savedSsids;
|
|
const bool stateChanged = next != m_state;
|
|
const bool wirelessEnabledChanged = next.wirelessEnabled != m_state.wirelessEnabled;
|
|
const NetworkChangeOrigin origin =
|
|
wirelessEnabledChanged ? consumeWirelessEnabledChangeOrigin(next.wirelessEnabled)
|
|
: NetworkChangeOrigin::External;
|
|
m_state = std::move(next);
|
|
if ((stateChanged || apsChanged || vpnsChanged || savedChanged) && m_changeCallback) {
|
|
m_changeCallback(m_state, origin);
|
|
}
|
|
};
|
|
|
|
auto onOpComplete = [pending]() {
|
|
if (--pending->pendingOps == 0) {
|
|
pending->onAllComplete();
|
|
}
|
|
};
|
|
|
|
refreshAccessPoints(onOpComplete);
|
|
refreshVpnConnections(onOpComplete);
|
|
refreshSavedConnections(onOpComplete);
|
|
}
|
|
|
|
void NetworkService::requestScan() {
|
|
std::int64_t baseline = 0;
|
|
bool anyRequested = false;
|
|
try {
|
|
std::vector<sdbus::ObjectPath> devices;
|
|
m_nm->callMethod("GetDevices").onInterface(k_nmInterface).storeResultsTo(devices);
|
|
for (const auto& devicePath : devices) {
|
|
try {
|
|
auto device = sdbus::createProxy(m_bus.connection(), k_nmBusName, devicePath);
|
|
const auto deviceType = getPropertyOr<std::uint32_t>(*device, k_nmDeviceInterface, "DeviceType", 0U);
|
|
if (deviceType != k_nmDeviceTypeWifi) {
|
|
continue;
|
|
}
|
|
const auto lastScan =
|
|
getPropertyOr<std::int64_t>(*device, k_nmDeviceWirelessInterface, "LastScan", std::int64_t{0});
|
|
if (lastScan > baseline) {
|
|
baseline = lastScan;
|
|
}
|
|
const std::map<std::string, sdbus::Variant> options;
|
|
device->callMethod("RequestScan").onInterface(k_nmDeviceWirelessInterface).withArguments(options);
|
|
anyRequested = true;
|
|
} catch (const sdbus::Error& e) {
|
|
kLog.debug("RequestScan failed on {}: {}", std::string(devicePath), e.what());
|
|
}
|
|
}
|
|
} catch (const sdbus::Error& e) {
|
|
kLog.warn("GetDevices failed: {}", e.what());
|
|
}
|
|
if (anyRequested) {
|
|
m_scanning = true;
|
|
m_scanBaselineLastScan = baseline;
|
|
refresh();
|
|
}
|
|
}
|
|
|
|
bool NetworkService::activateAccessPoint(const AccessPointInfo& ap) {
|
|
if (ap.devicePath.empty() || ap.path.empty()) {
|
|
return false;
|
|
}
|
|
|
|
// Only try ActivateConnection("/") when we actually have a saved profile for
|
|
// this SSID — NM matches by best fit, and a stray saved connection (e.g. for
|
|
// another device, or a profile we thought was forgotten) would otherwise be
|
|
// silently reused with whatever PSK it carries. When there is no saved
|
|
// profile we go straight to AddAndActivateConnection so NM creates a fresh
|
|
// one and (for secured APs) calls GetSecrets against our agent.
|
|
if (hasSavedConnection(ap.ssid)) {
|
|
try {
|
|
const sdbus::ObjectPath emptyConnectionPath{"/"};
|
|
const sdbus::ObjectPath devicePath{ap.devicePath};
|
|
const sdbus::ObjectPath apPath{ap.path};
|
|
sdbus::ObjectPath activePath;
|
|
m_nm->callMethod("ActivateConnection")
|
|
.onInterface(k_nmInterface)
|
|
.withArguments(emptyConnectionPath, devicePath, apPath)
|
|
.storeResultsTo(activePath);
|
|
kLog.info("activating ap ssid={} active={}", ap.ssid, std::string(activePath));
|
|
return true;
|
|
} catch (const sdbus::Error& e) {
|
|
kLog.debug("ActivateConnection(/) failed for ssid={}: {}; trying AddAndActivate", ap.ssid, e.what());
|
|
}
|
|
}
|
|
|
|
try {
|
|
using SettingsDict = std::map<std::string, std::map<std::string, sdbus::Variant>>;
|
|
SettingsDict settings;
|
|
if (ap.secured) {
|
|
// Minimal secured-wifi settings — NM fills in ssid from the specific_object
|
|
// and calls GetSecrets against us for the PSK.
|
|
settings["802-11-wireless-security"]["key-mgmt"] = sdbus::Variant{std::string("wpa-psk")};
|
|
}
|
|
const sdbus::ObjectPath devicePath{ap.devicePath};
|
|
const sdbus::ObjectPath apPath{ap.path};
|
|
sdbus::ObjectPath connectionPath;
|
|
sdbus::ObjectPath activePath;
|
|
m_nm->callMethod("AddAndActivateConnection")
|
|
.onInterface(k_nmInterface)
|
|
.withArguments(settings, devicePath, apPath)
|
|
.storeResultsTo(connectionPath, activePath);
|
|
kLog.info("add+activate ap ssid={} conn={} active={}", ap.ssid, std::string(connectionPath),
|
|
std::string(activePath));
|
|
return true;
|
|
} catch (const sdbus::Error& e) {
|
|
kLog.warn("AddAndActivateConnection failed ssid={} err={}", ap.ssid, e.what());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool NetworkService::activateVpnConnection(const VpnConnectionInfo& vpn) {
|
|
if (vpn.path.empty()) {
|
|
return false;
|
|
}
|
|
try {
|
|
// Async: ActivateConnection can involve polkit/agent interactions, and a
|
|
// synchronous call can stall the main loop while authorization is pending.
|
|
const std::string vpnName = vpn.name;
|
|
const std::string vpnPath = vpn.path;
|
|
m_nm->callMethodAsync("ActivateConnection")
|
|
.onInterface(k_nmInterface)
|
|
.withArguments(sdbus::ObjectPath{vpnPath}, sdbus::ObjectPath{"/"}, sdbus::ObjectPath{"/"})
|
|
.uponReplyInvoke([this, vpnName, vpnPath](std::optional<sdbus::Error> err, sdbus::ObjectPath activePath) {
|
|
if (err.has_value()) {
|
|
kLog.warn("ActivateConnection(vpn) failed name={} path={}: {}", vpnName, vpnPath, err->what());
|
|
} else {
|
|
kLog.info("activating vpn name={} active={}", vpnName, std::string(activePath));
|
|
}
|
|
refresh();
|
|
});
|
|
return true;
|
|
} catch (const sdbus::Error& e) {
|
|
kLog.warn("ActivateConnection(vpn) failed name={} path={} err={}", vpn.name, vpn.path, e.what());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool NetworkService::deactivateVpnConnection(const VpnConnectionInfo& vpn) {
|
|
if (vpn.path.empty()) {
|
|
return false;
|
|
}
|
|
try {
|
|
std::vector<sdbus::ObjectPath> activeConnections;
|
|
const sdbus::Variant activeVar = m_nm->getProperty("ActiveConnections").onInterface(k_nmInterface);
|
|
activeConnections = activeVar.get<std::vector<sdbus::ObjectPath>>();
|
|
for (const auto& activePath : activeConnections) {
|
|
try {
|
|
auto active = sdbus::createProxy(m_bus.connection(), k_nmBusName, activePath);
|
|
const auto profilePath =
|
|
getPropertyOr<sdbus::ObjectPath>(*active, k_nmActiveConnectionInterface, "Connection", sdbus::ObjectPath{});
|
|
const auto activeState = getPropertyOr<std::uint32_t>(*active, k_nmActiveConnectionInterface, "State", 0U);
|
|
if (profilePath != vpn.path || activeState != k_nmActiveConnectionStateActivated) {
|
|
continue;
|
|
}
|
|
// Async: DeactivateConnection on a system-owned profile is gated by polkit,
|
|
// and a sync call would freeze the main loop while the polkit agent prompts
|
|
// (or while polkit waits for an agent to register). Fire-and-forget here.
|
|
const std::string activePathStr = std::string(activePath);
|
|
const std::string vpnName = vpn.name;
|
|
m_nm->callMethodAsync("DeactivateConnection")
|
|
.onInterface(k_nmInterface)
|
|
.withArguments(sdbus::ObjectPath{activePathStr})
|
|
.uponReplyInvoke([activePathStr, vpnName](std::optional<sdbus::Error> err) {
|
|
if (err.has_value()) {
|
|
kLog.warn("DeactivateConnection(vpn) failed name={} active={}: {}", vpnName, activePathStr,
|
|
err->what());
|
|
} else {
|
|
kLog.info("deactivated vpn name={} active={}", vpnName, activePathStr);
|
|
}
|
|
});
|
|
return true;
|
|
} catch (const sdbus::Error&) {
|
|
}
|
|
}
|
|
} catch (const sdbus::Error& e) {
|
|
kLog.warn("DeactivateConnection(vpn) lookup failed path={}: {}", vpn.path, e.what());
|
|
return false;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void NetworkService::setWirelessEnabled(bool enabled) {
|
|
if (enabled != m_state.wirelessEnabled) {
|
|
m_pendingLocalWirelessEnabled = enabled;
|
|
}
|
|
try {
|
|
m_nm->setProperty("WirelessEnabled").onInterface(k_nmInterface).toValue(enabled);
|
|
} catch (const sdbus::Error& e) {
|
|
if (m_pendingLocalWirelessEnabled == enabled) {
|
|
m_pendingLocalWirelessEnabled.reset();
|
|
}
|
|
kLog.warn("WirelessEnabled write failed: {}", e.what());
|
|
}
|
|
}
|
|
|
|
void NetworkService::disconnect() {
|
|
if (m_activeConnectionPath.empty() || m_activeConnectionPath == "/") {
|
|
return;
|
|
}
|
|
// Async: DeactivateConnection on a system-owned profile is gated by polkit,
|
|
// and a sync call would freeze the main loop while the polkit agent prompts
|
|
// (or while polkit waits for an agent to register). Fire-and-forget here.
|
|
const std::string activePath = m_activeConnectionPath;
|
|
try {
|
|
m_nm->callMethodAsync("DeactivateConnection")
|
|
.onInterface(k_nmInterface)
|
|
.withArguments(sdbus::ObjectPath{activePath})
|
|
.uponReplyInvoke([activePath](std::optional<sdbus::Error> err) {
|
|
if (err.has_value()) {
|
|
kLog.warn("DeactivateConnection failed path={}: {}", activePath, err->what());
|
|
} else {
|
|
kLog.info("deactivated connection path={}", activePath);
|
|
}
|
|
});
|
|
} catch (const sdbus::Error& e) {
|
|
kLog.warn("DeactivateConnection dispatch failed: {}", e.what());
|
|
}
|
|
}
|
|
|
|
namespace {
|
|
// State machine for an in-flight forgetSsid operation. Owned by lambdas
|
|
// captured via shared_ptr; lives until the last D-Bus reply lands.
|
|
struct ForgetOp {
|
|
std::string ssid;
|
|
std::unique_ptr<sdbus::IProxy> settings;
|
|
std::vector<std::unique_ptr<sdbus::IProxy>> targets;
|
|
int matched = 0;
|
|
int removed = 0;
|
|
int failed = 0;
|
|
int pendingGetSettings = 0;
|
|
int pendingDeletes = 0;
|
|
bool listingDone = false;
|
|
std::function<void()> onComplete;
|
|
};
|
|
|
|
bool ssidFromSettings(const std::map<std::string, std::map<std::string, sdbus::Variant>>& cfg, std::string& out) {
|
|
auto wifiIt = cfg.find("802-11-wireless");
|
|
if (wifiIt == cfg.end())
|
|
return false;
|
|
auto ssidIt = wifiIt->second.find("ssid");
|
|
if (ssidIt == wifiIt->second.end())
|
|
return false;
|
|
try {
|
|
const auto bytes = ssidIt->second.get<std::vector<std::uint8_t>>();
|
|
out.assign(bytes.begin(), bytes.end());
|
|
return true;
|
|
} catch (const sdbus::Error&) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
void maybeFinishForget(const std::shared_ptr<ForgetOp>& op) {
|
|
if (op->listingDone && op->pendingGetSettings == 0 && op->pendingDeletes == 0) {
|
|
kLog.info("forgetSsid ssid=\"{}\" matched={} removed={} failed={}", op->ssid, op->matched, op->removed,
|
|
op->failed);
|
|
if (op->onComplete)
|
|
op->onComplete();
|
|
}
|
|
}
|
|
} // namespace
|
|
|
|
void NetworkService::forgetSsid(const std::string& ssid) {
|
|
if (ssid.empty()) {
|
|
return;
|
|
}
|
|
// Tear down the live connection before deleting the saved profile, so a
|
|
// subsequent reconnect attempt cannot silently reuse the still-active
|
|
// connection (which would skip the password prompt). Async — see disconnect().
|
|
if (m_state.kind == NetworkConnectivity::Wireless && m_state.connected && m_state.ssid == ssid) {
|
|
disconnect();
|
|
}
|
|
|
|
auto op = std::make_shared<ForgetOp>();
|
|
op->ssid = ssid;
|
|
op->onComplete = [this]() {
|
|
// Final refresh rebuilds the UI (no Forget button, no active tint) without
|
|
// waiting for an NM PropertiesChanged signal to land.
|
|
refresh();
|
|
};
|
|
|
|
try {
|
|
op->settings = sdbus::createProxy(m_bus.connection(), k_nmBusName, k_nmSettingsObjectPath);
|
|
} catch (const sdbus::Error& e) {
|
|
kLog.warn("forgetSsid: settings proxy failed ssid=\"{}\": {}", ssid, e.what());
|
|
refresh();
|
|
return;
|
|
}
|
|
|
|
auto& bus = m_bus;
|
|
op->settings->callMethodAsync("ListConnections")
|
|
.onInterface(k_nmSettingsInterface)
|
|
.uponReplyInvoke([op, &bus](std::optional<sdbus::Error> err, std::vector<sdbus::ObjectPath> paths) {
|
|
if (err.has_value()) {
|
|
kLog.warn("forgetSsid: ListConnections failed ssid=\"{}\": {}", op->ssid, err->what());
|
|
op->listingDone = true;
|
|
maybeFinishForget(op);
|
|
return;
|
|
}
|
|
for (const auto& connectionPath : paths) {
|
|
std::unique_ptr<sdbus::IProxy> conn;
|
|
try {
|
|
conn = sdbus::createProxy(bus.connection(), k_nmBusName, connectionPath);
|
|
} catch (const sdbus::Error& e) {
|
|
kLog.debug("forgetSsid: proxy failed for {}: {}", std::string(connectionPath), e.what());
|
|
continue;
|
|
}
|
|
auto* connRaw = conn.get();
|
|
op->targets.push_back(std::move(conn));
|
|
++op->pendingGetSettings;
|
|
const std::string pathStr{connectionPath};
|
|
connRaw->callMethodAsync("GetSettings")
|
|
.onInterface(k_nmSettingsConnectionInterface)
|
|
.uponReplyInvoke(
|
|
[op, connRaw, pathStr](std::optional<sdbus::Error> getErr,
|
|
std::map<std::string, std::map<std::string, sdbus::Variant>> cfg) {
|
|
--op->pendingGetSettings;
|
|
if (getErr.has_value()) {
|
|
kLog.debug("forgetSsid: GetSettings failed for {}: {}", pathStr, getErr->what());
|
|
maybeFinishForget(op);
|
|
return;
|
|
}
|
|
std::string foundSsid;
|
|
if (!ssidFromSettings(cfg, foundSsid) || foundSsid != op->ssid) {
|
|
maybeFinishForget(op);
|
|
return;
|
|
}
|
|
++op->matched;
|
|
++op->pendingDeletes;
|
|
connRaw->callMethodAsync("Delete")
|
|
.onInterface(k_nmSettingsConnectionInterface)
|
|
.uponReplyInvoke([op, pathStr](std::optional<sdbus::Error> delErr) {
|
|
--op->pendingDeletes;
|
|
if (delErr.has_value()) {
|
|
// Common cause: system-owned profile + no polkit agent
|
|
// running, so Delete is denied. Surface the real error
|
|
// name — otherwise indistinguishable from "nothing happened".
|
|
++op->failed;
|
|
kLog.warn("forgetSsid: Delete refused for {} ssid=\"{}\": {}", pathStr, op->ssid,
|
|
delErr->what());
|
|
} else {
|
|
++op->removed;
|
|
}
|
|
maybeFinishForget(op);
|
|
});
|
|
maybeFinishForget(op);
|
|
});
|
|
}
|
|
op->listingDone = true;
|
|
maybeFinishForget(op);
|
|
});
|
|
}
|
|
|
|
bool NetworkService::hasSavedConnection(const std::string& ssid) const {
|
|
if (ssid.empty()) {
|
|
return false;
|
|
}
|
|
return std::find(m_savedSsids.begin(), m_savedSsids.end(), ssid) != m_savedSsids.end();
|
|
}
|
|
|
|
void NetworkService::refreshSavedConnections(std::function<void()> onComplete) {
|
|
try {
|
|
auto settings =
|
|
std::shared_ptr<sdbus::IProxy>(sdbus::createProxy(m_bus.connection(), k_nmBusName, k_nmSettingsObjectPath));
|
|
settings->callMethodAsync("ListConnections")
|
|
.onInterface(k_nmSettingsInterface)
|
|
.uponReplyInvoke([this, settings, onComplete](std::optional<sdbus::Error> err,
|
|
std::vector<sdbus::ObjectPath> connectionPaths) {
|
|
if (err.has_value()) {
|
|
kLog.debug("refreshSavedConnections ListConnections failed: {}", err->what());
|
|
if (onComplete)
|
|
onComplete();
|
|
return;
|
|
}
|
|
|
|
if (connectionPaths.empty()) {
|
|
m_savedSsids.clear();
|
|
if (onComplete)
|
|
onComplete();
|
|
return;
|
|
}
|
|
|
|
auto opState = std::make_shared<std::pair<std::vector<std::string>, std::atomic<int>>>(
|
|
std::make_pair(std::vector<std::string>{}, static_cast<int>(connectionPaths.size())));
|
|
|
|
for (const auto& connectionPath : connectionPaths) {
|
|
try {
|
|
auto connection = std::shared_ptr<sdbus::IProxy>(
|
|
sdbus::createProxy(m_bus.connection(), k_nmBusName, connectionPath));
|
|
connection->callMethodAsync("GetSettings")
|
|
.onInterface(k_nmSettingsConnectionInterface)
|
|
.uponReplyInvoke([this, connection, opState, onComplete](
|
|
std::optional<sdbus::Error> settingsErr,
|
|
std::map<std::string, std::map<std::string, sdbus::Variant>> cfg) {
|
|
if (!settingsErr.has_value()) {
|
|
auto wifiIt = cfg.find("802-11-wireless");
|
|
if (wifiIt != cfg.end()) {
|
|
auto ssidIt = wifiIt->second.find("ssid");
|
|
if (ssidIt != wifiIt->second.end()) {
|
|
try {
|
|
const auto bytes = ssidIt->second.get<std::vector<std::uint8_t>>();
|
|
std::string ssid(bytes.begin(), bytes.end());
|
|
if (!ssid.empty()) {
|
|
opState->first.push_back(std::move(ssid));
|
|
}
|
|
} catch (const sdbus::Error&) {
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (--opState->second == 0) {
|
|
std::ranges::sort(opState->first);
|
|
opState->first.erase(std::unique(opState->first.begin(), opState->first.end()),
|
|
opState->first.end());
|
|
m_savedSsids = std::move(opState->first);
|
|
if (onComplete)
|
|
onComplete();
|
|
}
|
|
});
|
|
} catch (const sdbus::Error&) {
|
|
if (--opState->second == 0) {
|
|
std::ranges::sort(opState->first);
|
|
opState->first.erase(std::unique(opState->first.begin(), opState->first.end()),
|
|
opState->first.end());
|
|
m_savedSsids = std::move(opState->first);
|
|
if (onComplete)
|
|
onComplete();
|
|
}
|
|
}
|
|
}
|
|
});
|
|
} catch (const sdbus::Error& e) {
|
|
kLog.debug("refreshSavedConnections: {}", e.what());
|
|
if (onComplete)
|
|
onComplete();
|
|
}
|
|
}
|
|
|
|
void NetworkService::refreshVpnConnections(std::function<void()> onComplete) {
|
|
try {
|
|
auto settings =
|
|
std::shared_ptr<sdbus::IProxy>(sdbus::createProxy(m_bus.connection(), k_nmBusName, k_nmSettingsObjectPath));
|
|
settings->callMethodAsync("ListConnections")
|
|
.onInterface(k_nmSettingsInterface)
|
|
.uponReplyInvoke([this, settings, onComplete](std::optional<sdbus::Error> err,
|
|
std::vector<sdbus::ObjectPath> connectionPaths) {
|
|
if (err.has_value()) {
|
|
kLog.debug("refreshVpnConnections ListConnections failed: {}", err->what());
|
|
if (onComplete)
|
|
onComplete();
|
|
return;
|
|
}
|
|
|
|
if (connectionPaths.empty()) {
|
|
m_vpnConnections.clear();
|
|
if (onComplete)
|
|
onComplete();
|
|
return;
|
|
}
|
|
|
|
auto opState = std::make_shared<std::pair<
|
|
std::pair<std::vector<VpnConnectionInfo>, std::set<std::string>>,
|
|
std::atomic<int>>>(std::make_pair(
|
|
std::make_pair(std::vector<VpnConnectionInfo>{}, std::set<std::string>{}),
|
|
static_cast<int>(connectionPaths.size())));
|
|
|
|
for (const auto& connectionPath : connectionPaths) {
|
|
try {
|
|
auto connection = std::shared_ptr<sdbus::IProxy>(
|
|
sdbus::createProxy(m_bus.connection(), k_nmBusName, connectionPath));
|
|
connection->callMethodAsync("GetSettings")
|
|
.onInterface(k_nmSettingsConnectionInterface)
|
|
.uponReplyInvoke([this, connection, opState, connectionPath, onComplete](
|
|
std::optional<sdbus::Error> getErr,
|
|
std::map<std::string, std::map<std::string, sdbus::Variant>> cfg) {
|
|
if (!getErr.has_value()) {
|
|
auto connIt = cfg.find("connection");
|
|
if (connIt != cfg.end()) {
|
|
auto typeIt = connIt->second.find("type");
|
|
if (typeIt != connIt->second.end()) {
|
|
try {
|
|
const auto type = typeIt->second.get<std::string>();
|
|
const bool hasVpnSection = cfg.contains("vpn");
|
|
const bool vpnLikeType = type == "vpn" || type == "wireguard";
|
|
if (vpnLikeType || hasVpnSection) {
|
|
VpnConnectionInfo info;
|
|
info.path = std::string(connectionPath);
|
|
auto idIt = connIt->second.find("id");
|
|
if (idIt != connIt->second.end()) {
|
|
try {
|
|
info.name = idIt->second.get<std::string>();
|
|
} catch (const sdbus::Error&) {
|
|
}
|
|
}
|
|
if (info.name.empty()) {
|
|
info.name = info.path;
|
|
}
|
|
info.active = false;
|
|
opState->first.second.insert(info.path);
|
|
opState->first.first.push_back(std::move(info));
|
|
}
|
|
} catch (const sdbus::Error&) {
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (--opState->second == 0) {
|
|
// Mark active VPNs
|
|
try {
|
|
std::vector<sdbus::ObjectPath> activeConnections;
|
|
const sdbus::Variant activeVar =
|
|
m_nm->getProperty("ActiveConnections").onInterface(k_nmInterface);
|
|
activeConnections = activeVar.get<std::vector<sdbus::ObjectPath>>();
|
|
for (const auto& activePath : activeConnections) {
|
|
try {
|
|
auto active = sdbus::createProxy(m_bus.connection(), k_nmBusName, activePath);
|
|
const auto state =
|
|
getPropertyOr<std::uint32_t>(*active, k_nmActiveConnectionInterface, "State", 0U);
|
|
if (state != k_nmActiveConnectionStateActivating &&
|
|
state != k_nmActiveConnectionStateActivated) {
|
|
continue;
|
|
}
|
|
const auto profilePath =
|
|
getPropertyOr<sdbus::ObjectPath>(*active, k_nmActiveConnectionInterface,
|
|
"Connection", sdbus::ObjectPath{});
|
|
const std::string profilePathStr = std::string(profilePath);
|
|
if (!opState->first.second.contains(profilePathStr)) {
|
|
continue;
|
|
}
|
|
for (auto& vpn : opState->first.first) {
|
|
if (vpn.path == profilePathStr) {
|
|
vpn.active = true;
|
|
break;
|
|
}
|
|
}
|
|
} catch (const sdbus::Error&) {
|
|
}
|
|
}
|
|
} catch (const sdbus::Error& e) {
|
|
kLog.debug("refreshVpnConnections active list: {}", e.what());
|
|
}
|
|
std::ranges::sort(opState->first.first,
|
|
[](const VpnConnectionInfo& a, const VpnConnectionInfo& b) {
|
|
if (a.active != b.active) {
|
|
return a.active;
|
|
}
|
|
return a.name < b.name;
|
|
});
|
|
m_vpnConnections = std::move(opState->first.first);
|
|
if (onComplete)
|
|
onComplete();
|
|
}
|
|
});
|
|
} catch (const sdbus::Error&) {
|
|
if (--opState->second == 0) {
|
|
std::ranges::sort(opState->first.first,
|
|
[](const VpnConnectionInfo& a, const VpnConnectionInfo& b) {
|
|
if (a.active != b.active) {
|
|
return a.active;
|
|
}
|
|
return a.name < b.name;
|
|
});
|
|
m_vpnConnections = std::move(opState->first.first);
|
|
if (onComplete)
|
|
onComplete();
|
|
}
|
|
}
|
|
}
|
|
});
|
|
} catch (const sdbus::Error& e) {
|
|
kLog.debug("refreshVpnConnections: {}", e.what());
|
|
if (onComplete)
|
|
onComplete();
|
|
}
|
|
}
|
|
|
|
void NetworkService::ensureWifiDeviceSubscribed(const std::string& devicePath) {
|
|
if (m_wifiDevices.contains(devicePath)) {
|
|
return;
|
|
}
|
|
try {
|
|
auto proxy = sdbus::createProxy(m_bus.connection(), k_nmBusName, sdbus::ObjectPath{devicePath});
|
|
proxy->uponSignal("PropertiesChanged")
|
|
.onInterface(k_propertiesInterface)
|
|
.call([this](const std::string& interfaceName, const std::map<std::string, sdbus::Variant>& changedProperties,
|
|
const std::vector<std::string>& /*invalidatedProperties*/) {
|
|
if (interfaceName == k_nmDeviceWirelessInterface) {
|
|
if (auto it = changedProperties.find("LastScan"); it != changedProperties.end()) {
|
|
try {
|
|
const auto lastScan = it->second.get<std::int64_t>();
|
|
if (m_scanning && lastScan > m_scanBaselineLastScan) {
|
|
m_scanning = false;
|
|
}
|
|
} catch (const sdbus::Error&) {
|
|
}
|
|
}
|
|
if (changedProperties.contains("AccessPoints") || changedProperties.contains("LastScan")) {
|
|
refresh();
|
|
}
|
|
} else if (interfaceName == k_nmDeviceInterface) {
|
|
if (changedProperties.contains("State")) {
|
|
refresh();
|
|
}
|
|
}
|
|
});
|
|
m_wifiDevices.emplace(devicePath, std::move(proxy));
|
|
} catch (const sdbus::Error& e) {
|
|
kLog.debug("wifi device subscribe failed {}: {}", devicePath, e.what());
|
|
}
|
|
}
|
|
|
|
void NetworkService::refreshAccessPoints(std::function<void()> onComplete) {
|
|
try {
|
|
m_nm->callMethodAsync("GetDevices")
|
|
.onInterface(k_nmInterface)
|
|
.uponReplyInvoke([this, onComplete](std::optional<sdbus::Error> err,
|
|
std::vector<sdbus::ObjectPath> devices) {
|
|
if (err.has_value()) {
|
|
kLog.debug("refreshAccessPoints GetDevices failed: {}", err->what());
|
|
if (onComplete)
|
|
onComplete();
|
|
return;
|
|
}
|
|
|
|
if (devices.empty()) {
|
|
m_accessPoints.clear();
|
|
if (onComplete)
|
|
onComplete();
|
|
return;
|
|
}
|
|
|
|
// One slot per device; non-WiFi devices decrement immediately without contributing APs.
|
|
const int totalDevices = static_cast<int>(devices.size());
|
|
auto deviceState =
|
|
std::make_shared<std::pair<std::vector<AccessPointInfo>, std::atomic<int>>>(
|
|
std::make_pair(std::vector<AccessPointInfo>{}, totalDevices));
|
|
|
|
for (const auto& devicePath : devices) {
|
|
try {
|
|
auto device = std::shared_ptr<sdbus::IProxy>(
|
|
sdbus::createProxy(m_bus.connection(), k_nmBusName, devicePath));
|
|
// GetAll on DBus.Properties with the wireless interface arg: succeeds only for
|
|
// WiFi devices and also gives us ActiveAccessPoint — no sync reads needed.
|
|
device->callMethodAsync("GetAll")
|
|
.onInterface(k_propertiesInterface)
|
|
.withArguments(k_nmDeviceWirelessInterface)
|
|
.uponReplyInvoke([this, device, deviceState, devicePath, onComplete](
|
|
std::optional<sdbus::Error> wifiErr,
|
|
std::map<std::string, sdbus::Variant> wifiProps) {
|
|
if (wifiErr.has_value()) {
|
|
// Not a WiFi device — just decrement and possibly finish.
|
|
if (--deviceState->second == 0) {
|
|
finishRefreshAccessPoints(deviceState->first, onComplete);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// WiFi device confirmed. Subscribe for scan/state signals.
|
|
ensureWifiDeviceSubscribed(devicePath);
|
|
|
|
std::string activeApPath;
|
|
if (auto it = wifiProps.find("ActiveAccessPoint"); it != wifiProps.end()) {
|
|
try {
|
|
activeApPath = it->second.get<sdbus::ObjectPath>();
|
|
} catch (const sdbus::Error&) {
|
|
}
|
|
}
|
|
|
|
device->callMethodAsync("GetAccessPoints")
|
|
.onInterface(k_nmDeviceWirelessInterface)
|
|
.uponReplyInvoke([this, device, deviceState, devicePath, activeApPath, onComplete](
|
|
std::optional<sdbus::Error> apErr,
|
|
std::vector<sdbus::ObjectPath> apPaths) {
|
|
if (apErr.has_value() || apPaths.empty()) {
|
|
if (--deviceState->second == 0) {
|
|
finishRefreshAccessPoints(deviceState->first, onComplete);
|
|
}
|
|
return;
|
|
}
|
|
|
|
const int pendingAps = static_cast<int>(apPaths.size());
|
|
auto apState =
|
|
std::make_shared<std::pair<std::vector<AccessPointInfo>, std::atomic<int>>>(
|
|
std::make_pair(std::vector<AccessPointInfo>{}, pendingAps));
|
|
|
|
for (const auto& apPath : apPaths) {
|
|
try {
|
|
auto ap = std::shared_ptr<sdbus::IProxy>(
|
|
sdbus::createProxy(m_bus.connection(), k_nmBusName, apPath));
|
|
ap->callMethodAsync("GetAll")
|
|
.onInterface(k_propertiesInterface)
|
|
.withArguments(k_nmAccessPointInterface)
|
|
.uponReplyInvoke(
|
|
[this, ap, deviceState, apState, devicePath, activeApPath, apPath,
|
|
onComplete](std::optional<sdbus::Error> propErr,
|
|
std::map<std::string, sdbus::Variant> properties) {
|
|
if (!propErr.has_value()) {
|
|
AccessPointInfo info;
|
|
info.path = apPath;
|
|
info.devicePath = devicePath;
|
|
info.active =
|
|
!activeApPath.empty() && apPath == activeApPath;
|
|
try {
|
|
const auto ssidBytes =
|
|
properties.at("Ssid").get<std::vector<std::uint8_t>>();
|
|
info.ssid.assign(ssidBytes.begin(), ssidBytes.end());
|
|
} catch (const sdbus::Error&) {
|
|
}
|
|
try {
|
|
info.strength =
|
|
properties.at("Strength").get<std::uint8_t>();
|
|
} catch (const sdbus::Error&) {
|
|
}
|
|
const auto wpaFlags = [&properties]() {
|
|
try {
|
|
return properties.at("WpaFlags").get<std::uint32_t>();
|
|
} catch (const sdbus::Error&) {
|
|
return 0U;
|
|
}
|
|
}();
|
|
const auto rsnFlags = [&properties]() {
|
|
try {
|
|
return properties.at("RsnFlags").get<std::uint32_t>();
|
|
} catch (const sdbus::Error&) {
|
|
return 0U;
|
|
}
|
|
}();
|
|
info.secured = (wpaFlags != k_nm80211ApSecNone) ||
|
|
(rsnFlags != k_nm80211ApSecNone);
|
|
if (!info.ssid.empty()) {
|
|
apState->first.push_back(std::move(info));
|
|
}
|
|
}
|
|
if (--apState->second == 0) {
|
|
for (auto& apInfo : apState->first) {
|
|
deviceState->first.push_back(std::move(apInfo));
|
|
}
|
|
if (--deviceState->second == 0) {
|
|
finishRefreshAccessPoints(deviceState->first, onComplete);
|
|
}
|
|
}
|
|
});
|
|
} catch (const sdbus::Error&) {
|
|
if (--apState->second == 0) {
|
|
for (auto& apInfo : apState->first) {
|
|
deviceState->first.push_back(std::move(apInfo));
|
|
}
|
|
if (--deviceState->second == 0) {
|
|
finishRefreshAccessPoints(deviceState->first, onComplete);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
});
|
|
} catch (const sdbus::Error&) {
|
|
if (--deviceState->second == 0) {
|
|
finishRefreshAccessPoints(deviceState->first, onComplete);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
} catch (const sdbus::Error& e) {
|
|
kLog.debug("refreshAccessPoints: {}", e.what());
|
|
if (onComplete)
|
|
onComplete();
|
|
}
|
|
}
|
|
|
|
void NetworkService::finishRefreshAccessPoints(std::vector<AccessPointInfo>& aps,
|
|
std::function<void()> onComplete) {
|
|
// Deduplicate by SSID, keeping the strongest (and marking active if any entry is active).
|
|
std::vector<AccessPointInfo> deduped;
|
|
deduped.reserve(aps.size());
|
|
for (auto& ap : aps) {
|
|
auto it = std::find_if(deduped.begin(), deduped.end(),
|
|
[&](const AccessPointInfo& other) { return other.ssid == ap.ssid; });
|
|
if (it == deduped.end()) {
|
|
deduped.push_back(std::move(ap));
|
|
continue;
|
|
}
|
|
if (ap.active) {
|
|
it->active = true;
|
|
}
|
|
if (ap.strength > it->strength) {
|
|
it->strength = ap.strength;
|
|
it->path = ap.path;
|
|
it->devicePath = ap.devicePath;
|
|
it->secured = ap.secured;
|
|
}
|
|
}
|
|
std::ranges::sort(deduped, [](const AccessPointInfo& a, const AccessPointInfo& b) {
|
|
if (a.active != b.active) {
|
|
return a.active;
|
|
}
|
|
return a.strength > b.strength;
|
|
});
|
|
|
|
m_accessPoints = std::move(deduped);
|
|
if (onComplete)
|
|
onComplete();
|
|
}
|
|
|
|
void NetworkService::rebindActiveConnection() {
|
|
std::string newPath;
|
|
try {
|
|
const sdbus::Variant value = m_nm->getProperty("PrimaryConnection").onInterface(k_nmInterface);
|
|
newPath = value.get<sdbus::ObjectPath>();
|
|
} catch (const sdbus::Error& e) {
|
|
kLog.debug("PrimaryConnection unavailable: {}", e.what());
|
|
}
|
|
|
|
if (newPath != m_activeConnectionPath) {
|
|
m_activeConnectionPath = newPath;
|
|
m_activeConnection.reset();
|
|
if (!newPath.empty() && newPath != "/") {
|
|
try {
|
|
m_activeConnection = sdbus::createProxy(m_bus.connection(), k_nmBusName, sdbus::ObjectPath{newPath});
|
|
m_activeConnection->uponSignal("PropertiesChanged")
|
|
.onInterface(k_propertiesInterface)
|
|
.call([this](const std::string& interfaceName,
|
|
const std::map<std::string, sdbus::Variant>& changedProperties,
|
|
const std::vector<std::string>& /*invalidatedProperties*/) {
|
|
if (interfaceName != k_nmActiveConnectionInterface) {
|
|
return;
|
|
}
|
|
if (changedProperties.contains("Devices") || changedProperties.contains("State") ||
|
|
changedProperties.contains("Ip4Config")) {
|
|
rebindActiveConnection();
|
|
}
|
|
});
|
|
} catch (const sdbus::Error& e) {
|
|
kLog.debug("active connection proxy failed: {}", e.what());
|
|
m_activeConnection.reset();
|
|
}
|
|
}
|
|
}
|
|
|
|
std::string newDevicePath;
|
|
if (m_activeConnection != nullptr) {
|
|
try {
|
|
const sdbus::Variant value =
|
|
m_activeConnection->getProperty("Devices").onInterface(k_nmActiveConnectionInterface);
|
|
const auto devices = value.get<std::vector<sdbus::ObjectPath>>();
|
|
if (!devices.empty()) {
|
|
newDevicePath = devices.front();
|
|
}
|
|
} catch (const sdbus::Error&) {
|
|
}
|
|
}
|
|
rebindActiveDevice(newDevicePath);
|
|
|
|
refresh();
|
|
}
|
|
|
|
void NetworkService::rebindActiveDevice(const std::string& devicePath) {
|
|
if (devicePath == m_activeDevicePath && m_activeDevice != nullptr) {
|
|
return;
|
|
}
|
|
m_activeDevicePath = devicePath;
|
|
m_activeDevice.reset();
|
|
rebindActiveAccessPoint({});
|
|
|
|
if (devicePath.empty() || devicePath == "/") {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
m_activeDevice = sdbus::createProxy(m_bus.connection(), k_nmBusName, sdbus::ObjectPath{devicePath});
|
|
m_activeDevice->uponSignal("PropertiesChanged")
|
|
.onInterface(k_propertiesInterface)
|
|
.call([this](const std::string& interfaceName, const std::map<std::string, sdbus::Variant>& changedProperties,
|
|
const std::vector<std::string>& /*invalidatedProperties*/) {
|
|
if (interfaceName == k_nmDeviceInterface) {
|
|
if (changedProperties.contains("Ip4Config") || changedProperties.contains("State") ||
|
|
changedProperties.contains("Interface")) {
|
|
refresh();
|
|
}
|
|
} else if (interfaceName == k_nmDeviceWirelessInterface) {
|
|
if (changedProperties.contains("ActiveAccessPoint")) {
|
|
std::string apPath;
|
|
try {
|
|
apPath = changedProperties.at("ActiveAccessPoint").get<sdbus::ObjectPath>();
|
|
} catch (const sdbus::Error&) {
|
|
}
|
|
rebindActiveAccessPoint(apPath);
|
|
refresh();
|
|
}
|
|
}
|
|
});
|
|
} catch (const sdbus::Error& e) {
|
|
kLog.debug("device proxy failed: {}", e.what());
|
|
m_activeDevice.reset();
|
|
return;
|
|
}
|
|
|
|
// If this is a wireless device, also bind the current access point.
|
|
const auto deviceType = getPropertyOr<std::uint32_t>(*m_activeDevice, k_nmDeviceInterface, "DeviceType", 0U);
|
|
if (deviceType == k_nmDeviceTypeWifi) {
|
|
std::string apPath;
|
|
try {
|
|
const sdbus::Variant value =
|
|
m_activeDevice->getProperty("ActiveAccessPoint").onInterface(k_nmDeviceWirelessInterface);
|
|
apPath = value.get<sdbus::ObjectPath>();
|
|
} catch (const sdbus::Error&) {
|
|
}
|
|
rebindActiveAccessPoint(apPath);
|
|
}
|
|
}
|
|
|
|
void NetworkService::rebindActiveAccessPoint(const std::string& apPath) {
|
|
if (apPath == m_activeApPath && m_activeAp != nullptr) {
|
|
return;
|
|
}
|
|
m_activeApPath = apPath;
|
|
m_activeAp.reset();
|
|
if (apPath.empty() || apPath == "/") {
|
|
return;
|
|
}
|
|
try {
|
|
m_activeAp = sdbus::createProxy(m_bus.connection(), k_nmBusName, sdbus::ObjectPath{apPath});
|
|
m_activeAp->uponSignal("PropertiesChanged")
|
|
.onInterface(k_propertiesInterface)
|
|
.call([this](const std::string& interfaceName, const std::map<std::string, sdbus::Variant>& changedProperties,
|
|
const std::vector<std::string>& /*invalidatedProperties*/) {
|
|
if (interfaceName != k_nmAccessPointInterface) {
|
|
return;
|
|
}
|
|
if (changedProperties.contains("Strength") || changedProperties.contains("Ssid")) {
|
|
refresh();
|
|
}
|
|
});
|
|
} catch (const sdbus::Error& e) {
|
|
kLog.debug("AP proxy failed: {}", e.what());
|
|
m_activeAp.reset();
|
|
}
|
|
}
|
|
|
|
NetworkState NetworkService::readState() {
|
|
NetworkState next;
|
|
|
|
next.wirelessEnabled = getPropertyOr<bool>(*m_nm, k_nmInterface, "WirelessEnabled", false);
|
|
next.scanning = m_scanning;
|
|
next.vpnActive = false;
|
|
|
|
// Check primary connection: detect VPN type and connection state
|
|
if (m_activeConnection != nullptr) {
|
|
const auto type =
|
|
getPropertyOr<std::string>(*m_activeConnection, k_nmActiveConnectionInterface, "Type", std::string{});
|
|
next.vpnActive = (type == "vpn" || type == "wireguard");
|
|
const auto state = getPropertyOr<std::uint32_t>(*m_activeConnection, k_nmActiveConnectionInterface, "State", 0U);
|
|
next.connected = state == k_nmActiveConnectionStateActivated;
|
|
}
|
|
|
|
// Also check if any VPN profile is active (in case it's not the primary connection)
|
|
if (!next.vpnActive) {
|
|
for (const auto& vpn : m_vpnConnections) {
|
|
if (vpn.active) {
|
|
next.vpnActive = true;
|
|
next.connected = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (m_activeDevice == nullptr) {
|
|
return next;
|
|
}
|
|
|
|
const auto deviceType = getPropertyOr<std::uint32_t>(*m_activeDevice, k_nmDeviceInterface, "DeviceType", 0U);
|
|
next.interfaceName = getPropertyOr<std::string>(*m_activeDevice, k_nmDeviceInterface, "Interface", "");
|
|
|
|
const auto ip4ConfigPath =
|
|
getPropertyOr<sdbus::ObjectPath>(*m_activeDevice, k_nmDeviceInterface, "Ip4Config", sdbus::ObjectPath{});
|
|
next.ipv4 = firstIpv4FromConfig(m_bus.connection(), ip4ConfigPath);
|
|
|
|
if (deviceType == k_nmDeviceTypeWifi) {
|
|
next.kind = NetworkConnectivity::Wireless;
|
|
if (m_activeAp != nullptr) {
|
|
try {
|
|
const sdbus::Variant ssidVar = m_activeAp->getProperty("Ssid").onInterface(k_nmAccessPointInterface);
|
|
const auto ssidBytes = ssidVar.get<std::vector<std::uint8_t>>();
|
|
next.ssid.assign(ssidBytes.begin(), ssidBytes.end());
|
|
} catch (const sdbus::Error&) {
|
|
}
|
|
next.signalStrength =
|
|
getPropertyOr<std::uint8_t>(*m_activeAp, k_nmAccessPointInterface, "Strength", std::uint8_t{0});
|
|
}
|
|
} else {
|
|
// Ethernet, bridge, bond, VLAN, team, tun, and other wired-like device types
|
|
// are all displayed as wired — we don't need to enumerate every NMDeviceType.
|
|
next.kind = NetworkConnectivity::Wired;
|
|
}
|
|
|
|
return next;
|
|
}
|
|
|
|
NetworkChangeOrigin NetworkService::consumeWirelessEnabledChangeOrigin(bool enabled) {
|
|
if (!m_pendingLocalWirelessEnabled.has_value()) {
|
|
return NetworkChangeOrigin::External;
|
|
}
|
|
const bool matchesLocalRequest = *m_pendingLocalWirelessEnabled == enabled;
|
|
m_pendingLocalWirelessEnabled.reset();
|
|
return matchesLocalRequest ? NetworkChangeOrigin::Noctalia : NetworkChangeOrigin::External;
|
|
}
|
|
|
|
void NetworkService::emitChangedIfNeeded(NetworkState next) {
|
|
if (next == m_state) {
|
|
return;
|
|
}
|
|
const bool wirelessEnabledChanged = next.wirelessEnabled != m_state.wirelessEnabled;
|
|
const NetworkChangeOrigin origin =
|
|
wirelessEnabledChanged ? consumeWirelessEnabledChangeOrigin(next.wirelessEnabled) : NetworkChangeOrigin::External;
|
|
m_state = std::move(next);
|
|
if (m_changeCallback) {
|
|
m_changeCallback(m_state, origin);
|
|
}
|
|
}
|