diff --git a/CMakeLists.txt b/CMakeLists.txt index 7161c4e4..d85a2db5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -71,6 +71,7 @@ boption(SERVICE_GREETD "Greetd" ON) boption(SERVICE_UPOWER "UPower" ON) boption(SERVICE_NOTIFICATIONS "Notifications" ON) boption(BLUETOOTH "Bluetooth" ON) +boption(NETWORK "Network" ON) include(cmake/install-qml-module.cmake) include(cmake/util.cmake) @@ -117,7 +118,7 @@ if (WAYLAND) list(APPEND QT_FPDEPS WaylandClient) endif() -if (SERVICE_STATUS_NOTIFIER OR SERVICE_MPRIS OR SERVICE_UPOWER OR SERVICE_NOTIFICATIONS OR BLUETOOTH) +if (SERVICE_STATUS_NOTIFIER OR SERVICE_MPRIS OR SERVICE_UPOWER OR SERVICE_NOTIFICATIONS OR BLUETOOTH OR NETWORK) set(DBUS ON) endif() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 52db00a5..c95ecf71 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -33,3 +33,7 @@ add_subdirectory(services) if (BLUETOOTH) add_subdirectory(bluetooth) endif() + +if (NETWORK) + add_subdirectory(network) +endif() diff --git a/src/network/CMakeLists.txt b/src/network/CMakeLists.txt new file mode 100644 index 00000000..16110178 --- /dev/null +++ b/src/network/CMakeLists.txt @@ -0,0 +1,92 @@ +# NetworkManager DBus +set_source_files_properties(nm/org.freedesktop.NetworkManager.xml PROPERTIES + CLASSNAME DBusNetworkManagerProxy + NO_NAMESPACE TRUE +) + +qt_add_dbus_interface(NM_DBUS_INTERFACES + nm/org.freedesktop.NetworkManager.xml + nm/dbus_nm_backend +) + +set_source_files_properties(nm/org.freedesktop.NetworkManager.Device.xml PROPERTIES + CLASSNAME DBusNMDeviceProxy + NO_NAMESPACE TRUE +) + +qt_add_dbus_interface(NM_DBUS_INTERFACES + nm/org.freedesktop.NetworkManager.Device.xml + nm/dbus_nm_device +) + +set_source_files_properties(nm/org.freedesktop.NetworkManager.Device.Wireless.xml PROPERTIES + CLASSNAME DBusNMWirelessProxy + NO_NAMESPACE TRUE +) + +qt_add_dbus_interface(NM_DBUS_INTERFACES + nm/org.freedesktop.NetworkManager.Device.Wireless.xml + nm/dbus_nm_wireless +) + +set_source_files_properties(nm/org.freedesktop.NetworkManager.AccessPoint.xml PROPERTIES + CLASSNAME DBusNMAccessPointProxy + NO_NAMESPACE TRUE +) + +qt_add_dbus_interface(NM_DBUS_INTERFACES + nm/org.freedesktop.NetworkManager.AccessPoint.xml + nm/dbus_nm_accesspoint +) + +set_source_files_properties(nm/org.freedesktop.NetworkManager.Settings.Connection.xml PROPERTIES + CLASSNAME DBusNMConnectionSettingsProxy + INCLUDE ${CMAKE_CURRENT_SOURCE_DIR}/nm/dbus_types.hpp +) + +qt_add_dbus_interface(NM_DBUS_INTERFACES + nm/org.freedesktop.NetworkManager.Settings.Connection.xml + nm/dbus_nm_connection_settings +) + +set_source_files_properties(nm/org.freedesktop.NetworkManager.Connection.Active.xml PROPERTIES + CLASSNAME DBusNMActiveConnectionProxy + NO_NAMESPACE TRUE +) + +qt_add_dbus_interface(NM_DBUS_INTERFACES + nm/org.freedesktop.NetworkManager.Connection.Active.xml + nm/dbus_nm_active_connection +) + +qt_add_library(quickshell-network STATIC + frontend.cpp + nm/backend.cpp + nm/device.cpp + nm/connection.cpp + nm/accesspoint.cpp + nm/wireless.cpp + nm/utils.cpp + nm/enums.hpp + ${NM_DBUS_INTERFACES} +) + +# dbus headers +target_include_directories(quickshell-network PRIVATE + ${CMAKE_CURRENT_BINARY_DIR} +) + +qt_add_qml_module(quickshell-network + URI Quickshell.Network + VERSION 0.1 + DEPENDENCIES QtQml +) + +qs_add_module_deps_light(quickshell-network Quickshell) +install_qml_module(quickshell-network) + +target_link_libraries(quickshell-network PRIVATE Qt::Qml Qt::DBus) +qs_add_link_dependencies(quickshell-network quickshell-dbus) +target_link_libraries(quickshell PRIVATE quickshell-networkplugin) + +qs_module_pch(quickshell-network SET dbus) diff --git a/src/network/frontend.cpp b/src/network/frontend.cpp new file mode 100644 index 00000000..5968eb76 --- /dev/null +++ b/src/network/frontend.cpp @@ -0,0 +1,161 @@ +#include "frontend.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "nm/backend.hpp" + +namespace qs::network { + +namespace { +Q_LOGGING_CATEGORY(logNetworkDevice, "quickshell.network.device", QtWarningMsg); +Q_LOGGING_CATEGORY(logNetwork, "quickshell.network", QtWarningMsg); +} // namespace + +NetworkDevice::NetworkDevice(QObject* parent): QObject(parent) {}; + +QString NetworkDeviceState::toString(NetworkDeviceState::Enum state) { + switch (state) { + case NetworkDeviceState::Unknown: return QStringLiteral("Unknown"); + case NetworkDeviceState::Disconnected: return QStringLiteral("Disconnected"); + case NetworkDeviceState::Connecting: return QStringLiteral("Connecting"); + case NetworkDeviceState::Connected: return QStringLiteral("Connected"); + case NetworkDeviceState::Disconnecting: return QStringLiteral("Disconnecting"); + default: return QStringLiteral("Unknown"); + } +} + +QString NetworkDeviceType::toString(NetworkDeviceType::Enum type) { + switch (type) { + case NetworkDeviceType::Other: return QStringLiteral("Other"); + case NetworkDeviceType::Wireless: return QStringLiteral("Wireless"); + default: return QStringLiteral("Unknown"); + } +} + +void NetworkDevice::setName(const QString& name) { + if (name != this->bName) { + this->bName = name; + } +} + +void NetworkDevice::setAddress(const QString& address) { + if (address != this->bAddress) { + this->bAddress = address; + } +} + +void NetworkDevice::setState(NetworkDeviceState::Enum state) { + if (state != this->bState) { + this->bState = state; + } +} + +void NetworkDevice::disconnect() { + if (this->bState == NetworkDeviceState::Disconnected) { + qCCritical(logNetworkDevice) << "Device" << this << "is already disconnected"; + return; + } + + if (this->bState == NetworkDeviceState::Disconnecting) { + qCCritical(logNetworkDevice) << "Device" << this << "is already disconnecting"; + return; + } + + qCDebug(logNetworkDevice) << "Disconnecting from device" << this; + + this->requestDisconnect(); +} + +NetworkWifiDevice::NetworkWifiDevice(QObject* parent): NetworkDevice(parent) {}; + +void NetworkWifiDevice::scanComplete() { + if (this->bScanning) { + this->bScanning = false; + emit this->scanningChanged(); + } +} + +void NetworkWifiDevice::scan() { + if (this->bScanning) { + qCCritical(logNetworkDevice) << "Wireless device" << this << "is already scanning"; + return; + } + + qCDebug(logNetworkDevice) << "Requesting scan on wireless device" << this; + this->bScanning = true; + this->requestScan(); +} + +void NetworkWifiDevice::wifiNetworkAdded(WifiNetwork* network) { + this->mNetworks.insertObject(network); +} + +void NetworkWifiDevice::wifiNetworkRemoved(WifiNetwork* network) { + this->mNetworks.removeObject(network); +} + +WifiNetwork::WifiNetwork(QObject* parent): QObject(parent) {}; + +void WifiNetwork::setSsid(const QString& ssid) { + if (this->bSsid != ssid) { + this->bSsid = ssid; + emit this->ssidChanged(); + } +} + +void WifiNetwork::setSignalStrength(quint8 signal) { + if (this->bSignalStrength != signal) { + this->bSignalStrength = signal; + emit this->signalStrengthChanged(); + } +} + +void WifiNetwork::setConnected(bool connected) { + if (this->bConnected != connected) { + this->bConnected = connected; + emit this->connectedChanged(); + } +} + +void WifiNetwork::setNmSecurity(NMWirelessSecurityType::Enum security) { + if (this->bNmSecurity != security) { + this->bNmSecurity = security; + emit this->nmSecurityChanged(); + } +} + +void WifiNetwork::setKnown(bool known) { + if (this->bKnown != known) { + this->bKnown = known; + emit this->knownChanged(); + } +} + +Network::Network(QObject* parent): QObject(parent) { + // Try each backend + + // NetworkManager + auto* nm = new NetworkManager(this); + if (nm->isAvailable()) { + QObject::connect(nm, &NetworkManager::deviceAdded, this, &Network::addDevice); + QObject::connect(nm, &NetworkManager::deviceRemoved, this, &Network::removeDevice); + this->backend = nm; + return; + } else { + delete nm; + } + + qCCritical(logNetwork) << "Network will not work. Could not find an available backend."; +} + +void Network::addDevice(NetworkDevice* device) { this->mDevices.insertObject(device); } + +void Network::removeDevice(NetworkDevice* device) { this->mDevices.removeObject(device); } + +} // namespace qs::network diff --git a/src/network/frontend.hpp b/src/network/frontend.hpp new file mode 100644 index 00000000..11072200 --- /dev/null +++ b/src/network/frontend.hpp @@ -0,0 +1,239 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../core/model.hpp" +#include "nm/enums.hpp" + +namespace qs::network { + +///! A wifi network represents an available connection or access point on a wireless device. +class WifiNetwork: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_UNCREATABLE("Wifi items can only be acquired through Network"); + // clang-format off + /// The SSID (service set identifier) of the wifi network + Q_PROPERTY(QString ssid READ default NOTIFY ssidChanged BINDABLE bindableSsid); + // The current signal strength of the best access point on the network, in percent. + Q_PROPERTY(quint8 signalStrength READ default NOTIFY signalStrengthChanged BINDABLE bindableSignalStrength); + /// True if the wireless device is currently connected to this wifi network. + Q_PROPERTY(bool connected READ default NOTIFY connectedChanged BINDABLE bindableConnected); + /// The security type of the wifi network when the backend is NetworkManager. Otherwise NMWirelessSecurityType::Unknown. + Q_PROPERTY(NMWirelessSecurityType::Enum nmSecurity READ default NOTIFY nmSecurityChanged BINDABLE bindableNmSecurity); + /// True if the wifi network has a known connection profile saved. + Q_PROPERTY(bool known READ default NOTIFY knownChanged BINDABLE bindableKnown); + // clang-format on + +signals: + void ssidChanged(); + void signalStrengthChanged(); + void connectedChanged(); + void nmSecurityChanged(); + void knownChanged(); + void requestConnect(); + +public slots: + void setSsid(const QString& ssid); + void setSignalStrength(quint8 signalStrength); + void setConnected(bool connected); + void setNmSecurity(NMWirelessSecurityType::Enum security); + void setKnown(bool known); + +public: + explicit WifiNetwork(QObject* parent = nullptr); + + [[nodiscard]] QBindable bindableSsid() const { return &this->bSsid; }; + [[nodiscard]] QBindable bindableSignalStrength() const { return &this->bSignalStrength; }; + [[nodiscard]] QBindable bindableConnected() const { return &this->bConnected; }; + [[nodiscard]] QBindable bindableNmSecurity() const { + return &this->bNmSecurity; + }; + [[nodiscard]] QBindable bindableKnown() const { return &this->bKnown; }; + + // Q_INVOKABLE void connect(); + +private: + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(WifiNetwork, QString, bSsid, &WifiNetwork::ssidChanged); + Q_OBJECT_BINDABLE_PROPERTY(WifiNetwork, quint8, bSignalStrength, &WifiNetwork::signalStrengthChanged); + Q_OBJECT_BINDABLE_PROPERTY(WifiNetwork, bool, bConnected, &WifiNetwork::connectedChanged); + Q_OBJECT_BINDABLE_PROPERTY(WifiNetwork, NMWirelessSecurityType::Enum, bNmSecurity, &WifiNetwork::nmSecurityChanged); + Q_OBJECT_BINDABLE_PROPERTY(WifiNetwork, bool, bKnown, &WifiNetwork::knownChanged); + // clang-format on +}; + +///! Type of Network device. +class NetworkDeviceType: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + ///! A generic device. + Other = 0, + ///! An 802.11 Wi-Fi device. + Wireless = 1, + }; + Q_ENUM(Enum); + Q_INVOKABLE static QString toString(NetworkDeviceType::Enum type); +}; + +///! Connection state of a Network device. +class NetworkDeviceState: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + /// The device state is unknown. + Unknown = 0, + /// The device is not connected. + Disconnected = 1, + /// The device is connected. + Connected = 2, + /// The device is disconnecting. + Disconnecting = 3, + /// The device is connecting. + Connecting = 4, + }; + Q_ENUM(Enum); + Q_INVOKABLE static QString toString(NetworkDeviceState::Enum state); +}; + +///! A tracked Network device. +class NetworkDevice: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_UNCREATABLE("Devices can only be acquired through Network"); + + // clang-format off + /// The name of the device's interface. + Q_PROPERTY(QString name READ default NOTIFY nameChanged BINDABLE bindableName); + /// The hardware address of the device's interface in the XX:XX:XX:XX:XX:XX format. + Q_PROPERTY(QString address READ default NOTIFY addressChanged BINDABLE bindableAddress); + /// Connection state of the device. + Q_PROPERTY(NetworkDeviceState::Enum state READ default NOTIFY stateChanged BINDABLE bindableState); + /// Type of device. + Q_PROPERTY(NetworkDeviceType::Enum type READ type CONSTANT); + // clang-format on + +signals: + void nameChanged(); + void addressChanged(); + void stateChanged(); + void requestDisconnect(); + +public slots: + void setName(const QString& name); + void setAddress(const QString& address); + void setState(NetworkDeviceState::Enum state); + +public: + explicit NetworkDevice(QObject* parent = nullptr); + + /// Disconnects the device and prevents it from automatically activating further connections. + Q_INVOKABLE void disconnect(); + [[nodiscard]] virtual NetworkDeviceType::Enum type() const { return NetworkDeviceType::Other; }; + + [[nodiscard]] QBindable bindableName() const { return &this->bName; }; + [[nodiscard]] QBindable bindableAddress() const { return &this->bAddress; }; + [[nodiscard]] QBindable bindableState() const { return &this->bState; }; + +private: + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(NetworkDevice, QString, bName, &NetworkDevice::nameChanged); + Q_OBJECT_BINDABLE_PROPERTY(NetworkDevice, QString, bAddress, &NetworkDevice::addressChanged); + Q_OBJECT_BINDABLE_PROPERTY(NetworkDevice, NetworkDeviceState::Enum, bState, &NetworkDevice::stateChanged); + // clang-format on +}; + +///! Wireless variant of a tracked network device. +class NetworkWifiDevice: public NetworkDevice { + Q_OBJECT; + + // clang-format off + /// True if the wifi device is currently scanning for available wifi networks. + Q_PROPERTY(bool scanning READ default NOTIFY scanningChanged BINDABLE bindableScanning); + /// The currently active wifi network + // Q_PROPERTY(WifiNetwork activeNetwork READ networks CONSTANT); + /// A list of all wifi networks available to this wifi device + Q_PROPERTY(UntypedObjectModel* networks READ networks CONSTANT); + QSDOC_TYPE_OVERRIDE(ObjectModel*) + //clang-format on + +signals: + void requestScan(); + void scanningChanged(); + +public slots: + void scanComplete(); + void wifiNetworkAdded(WifiNetwork* network); + void wifiNetworkRemoved(WifiNetwork* network); + +public: + explicit NetworkWifiDevice(QObject* parent = nullptr); + [[nodiscard]] NetworkDeviceType::Enum type() const override { return NetworkDeviceType::Wireless; }; + + /// Request the wireless device to scan for available WiFi networks. + Q_INVOKABLE void scan(); + + [[nodiscard]] QBindable bindableScanning() { return &this->bScanning; }; + + UntypedObjectModel* networks() { return &this->mNetworks; }; + +private: + ObjectModel mNetworks {this}; + + Q_OBJECT_BINDABLE_PROPERTY(NetworkWifiDevice, bool, bScanning, &NetworkWifiDevice::scanningChanged); +}; + +class NetworkBackend: public QObject { + Q_OBJECT; + +public: + [[nodiscard]] virtual bool isAvailable() const = 0; + +protected: + explicit NetworkBackend(QObject* parent = nullptr): QObject(parent) {}; +}; + +///! Network manager +/// Provides access to network devices. +class Network: public QObject { + Q_OBJECT; + QML_NAMED_ELEMENT(Network); + QML_SINGLETON; + + // clang-format off + /// The default wifi device. Usually there is only one. This defaults to the first wifi device registered. + // Q_PROPERTY(WirelessNetworkDevice* defaultWifiDevice READ defaultWifiDevice CONSTANT); + /// A list of all network devices. + Q_PROPERTY(UntypedObjectModel* devices READ devices CONSTANT); + QSDOC_TYPE_OVERRIDE(ObjectModel*); + // clang-format on + +public slots: + void addDevice(NetworkDevice* device); + void removeDevice(NetworkDevice* device); + +public: + explicit Network(QObject* parent = nullptr); + [[nodiscard]] UntypedObjectModel* devices() { return backend ? &this->mDevices : nullptr; }; + +private: + ObjectModel mDevices {this}; + class NetworkBackend* backend = nullptr; +}; + +} // namespace qs::network diff --git a/src/network/nm/accesspoint.cpp b/src/network/nm/accesspoint.cpp new file mode 100644 index 00000000..7467f085 --- /dev/null +++ b/src/network/nm/accesspoint.cpp @@ -0,0 +1,71 @@ +#include "accesspoint.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "../../dbus/properties.hpp" +#include "nm/dbus_nm_accesspoint.h" + +namespace qs::dbus { + +DBusResult +DBusDataTransform::fromWire(quint32 wire) { + return DBusResult(static_cast(wire)); +} + +DBusResult +DBusDataTransform::fromWire(quint32 wire) { + return DBusResult(static_cast(wire)); +} + +DBusResult +DBusDataTransform::fromWire(quint32 wire) { + return DBusResult(static_cast(wire)); +} + +} // namespace qs::dbus + +namespace qs::network { +using namespace qs::dbus; + +namespace { +Q_LOGGING_CATEGORY(logNetworkManager, "quickshell.network.networkmanager", QtWarningMsg); +} + +NMAccessPointAdapter::NMAccessPointAdapter(const QString& path, QObject* parent): QObject(parent) { + this->proxy = new DBusNMAccessPointProxy( + "org.freedesktop.NetworkManager", + path, + QDBusConnection::systemBus(), + this + ); + + if (!this->proxy->isValid()) { + qCWarning(logNetworkManager) << "Cannot create access point proxy for" << path; + return; + } + + QObject::connect( + &this->accessPointProperties, + &DBusPropertyGroup::getAllFinished, + this, + [this]() { emit this->ready(); }, + Qt::SingleShotConnection + ); + + this->accessPointProperties.setInterface(this->proxy); + this->accessPointProperties.updateAllViaGetAll(); +} + +bool NMAccessPointAdapter::isValid() const { return this->proxy && this->proxy->isValid(); } +QString NMAccessPointAdapter::address() const { + return this->proxy ? this->proxy->service() : QString(); +} +QString NMAccessPointAdapter::path() const { return this->proxy ? this->proxy->path() : QString(); } + +} // namespace qs::network diff --git a/src/network/nm/accesspoint.hpp b/src/network/nm/accesspoint.hpp new file mode 100644 index 00000000..15311cc0 --- /dev/null +++ b/src/network/nm/accesspoint.hpp @@ -0,0 +1,92 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../dbus/properties.hpp" +#include "enums.hpp" +#include "nm/dbus_nm_accesspoint.h" + +namespace qs::dbus { + +template <> +struct DBusDataTransform { + using Wire = quint32; + using Data = qs::network::NM80211ApFlags::Enum; + static DBusResult fromWire(Wire wire); +}; + +template <> +struct DBusDataTransform { + using Wire = quint32; + using Data = qs::network::NM80211ApSecurityFlags::Enum; + static DBusResult fromWire(Wire wire); +}; + +template <> +struct DBusDataTransform { + using Wire = quint32; + using Data = qs::network::NM80211Mode::Enum; + static DBusResult fromWire(Wire wire); +}; + +} // namespace qs::dbus + +namespace qs::network { + +// NMAccessPointAdapter wraps the state of a NetworkManager access point +// (org.freedesktop.NetworkManager.AccessPoint) +class NMAccessPointAdapter: public QObject { + Q_OBJECT; + +public: + explicit NMAccessPointAdapter(const QString& path, QObject* parent = nullptr); + + [[nodiscard]] bool isValid() const; + [[nodiscard]] QString path() const; + [[nodiscard]] QString address() const; + [[nodiscard]] QByteArray ssid() const { return this->bSsid; }; + [[nodiscard]] quint8 signalStrength() const { return this->bSignalStrength; }; + [[nodiscard]] NM80211ApFlags::Enum flags() const { return this->bFlags; }; + [[nodiscard]] NM80211ApSecurityFlags::Enum wpaFlags() const { return this->bWpaFlags; }; + [[nodiscard]] NM80211ApSecurityFlags::Enum rsnFlags() const { return this->bRsnFlags; }; + [[nodiscard]] NM80211Mode::Enum mode() const { return this->bMode; }; + +signals: + void ssidChanged(const QByteArray& ssid); + void signalStrengthChanged(quint8 signal); + void wpaFlagsChanged(NM80211ApSecurityFlags::Enum wpaFlags); + void rsnFlagsChanged(NM80211ApSecurityFlags::Enum rsnFlags); + void flagsChanged(NM80211ApFlags::Enum flags); + void modeChanged(NM80211Mode::Enum mode); + void ready(); + +private: + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(NMAccessPointAdapter, QByteArray, bSsid, &NMAccessPointAdapter::ssidChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMAccessPointAdapter, quint8, bSignalStrength, &NMAccessPointAdapter::signalStrengthChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMAccessPointAdapter, NM80211ApFlags::Enum, bFlags, &NMAccessPointAdapter::flagsChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMAccessPointAdapter, NM80211ApSecurityFlags::Enum, bWpaFlags, &NMAccessPointAdapter::wpaFlagsChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMAccessPointAdapter, NM80211ApSecurityFlags::Enum, bRsnFlags, &NMAccessPointAdapter::rsnFlagsChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMAccessPointAdapter, NM80211Mode::Enum, bMode, &NMAccessPointAdapter::modeChanged); + + QS_DBUS_BINDABLE_PROPERTY_GROUP(NMAccessPointAdapter, accessPointProperties); + QS_DBUS_PROPERTY_BINDING(NMAccessPointAdapter, pSsid, bSsid, accessPointProperties, "Ssid"); + QS_DBUS_PROPERTY_BINDING(NMAccessPointAdapter, pSignalStrength, bSignalStrength, accessPointProperties, "Strength"); + QS_DBUS_PROPERTY_BINDING(NMAccessPointAdapter, pFlags, bFlags, accessPointProperties, "Flags"); + QS_DBUS_PROPERTY_BINDING(NMAccessPointAdapter, pWpaFlags, bWpaFlags, accessPointProperties, "WpaFlags"); + QS_DBUS_PROPERTY_BINDING(NMAccessPointAdapter, pRsnFlags, bRsnFlags, accessPointProperties, "RsnFlags"); + QS_DBUS_PROPERTY_BINDING(NMAccessPointAdapter, pMode, bMode, accessPointProperties, "Mode"); + // clang-format on + + DBusNMAccessPointProxy* proxy = nullptr; +}; + +} // namespace qs::network diff --git a/src/network/nm/backend.cpp b/src/network/nm/backend.cpp new file mode 100644 index 00000000..5af89f47 --- /dev/null +++ b/src/network/nm/backend.cpp @@ -0,0 +1,180 @@ +#include "backend.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../dbus/properties.hpp" +#include "../frontend.hpp" +#include "nm/dbus_nm_backend.h" +#include "wireless.hpp" + +namespace qs::network { + +namespace { +Q_LOGGING_CATEGORY(logNetworkManager, "quickshell.network.networkmanager", QtWarningMsg); +} + +const QString NM_SERVICE = "org.freedesktop.NetworkManager"; +const QString NM_PATH = "/org/freedesktop/NetworkManager"; + +NetworkManager::NetworkManager(QObject* parent): NetworkBackend(parent) { + qCDebug(logNetworkManager) << "Starting NetworkManager Network Backend"; + + auto bus = QDBusConnection::systemBus(); + if (!bus.isConnected()) { + qCWarning(logNetworkManager + ) << "Could not connect to DBus. NetworkManager backend will not work."; + return; + } + + this->proxy = new DBusNetworkManagerProxy(NM_SERVICE, NM_PATH, bus, this); + + if (!this->proxy->isValid()) { + qCDebug(logNetworkManager + ) << "NetworkManager service is not currently running. This network backend will not work"; + } else { + this->init(); + } +} + +void NetworkManager::init() { + // clang-format off + QObject::connect(this->proxy, &DBusNetworkManagerProxy::DeviceAdded, this, &NetworkManager::onDeviceAdded); + QObject::connect(this->proxy, &DBusNetworkManagerProxy::DeviceRemoved, this, &NetworkManager::onDeviceRemoved); + // clang-format on + + this->dbusProperties.setInterface(this->proxy); + this->dbusProperties.updateAllViaGetAll(); + + this->registerDevices(); +} + +void NetworkManager::registerDevices() { + auto pending = this->proxy->GetAllDevices(); + auto* call = new QDBusPendingCallWatcher(pending, this); + + auto responseCallback = [this](QDBusPendingCallWatcher* call) { + const QDBusPendingReply> reply = *call; + + if (reply.isError()) { + qCWarning(logNetworkManager) << "Failed to get devices: " << reply.error().message(); + } else { + for (const QDBusObjectPath& devicePath: reply.value()) { + this->queueDeviceRegistration(devicePath.path()); + } + } + + delete call; + }; + + QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); +} + +void NetworkManager::queueDeviceRegistration(const QString& path) { + if (this->mDeviceHash.contains(path)) { + qCDebug(logNetworkManager) << "Skipping duplicate registration of device" << path; + return; + } + + auto* deviceAdapter = new NMDeviceAdapter(path); + + if (!deviceAdapter->isValid()) { + qCWarning(logNetworkManager) << "Ignoring invalid registration of" << path; + delete deviceAdapter; + return; + } + + QObject::connect( + deviceAdapter, + &NMDeviceAdapter::ready, + this, + [this, deviceAdapter, path]() { this->registerDevice(deviceAdapter, path); }, + Qt::SingleShotConnection + ); +} + +NetworkDeviceState::Enum NetworkManager::toNetworkDeviceState(NMDeviceState::Enum state) { + switch (state) { + case 0 ... 20: return NetworkDeviceState::Unknown; + case 30: return NetworkDeviceState::Disconnected; + case 40 ... 90: return NetworkDeviceState::Connecting; + case 100: return NetworkDeviceState::Connected; + case 110 ... 120: return NetworkDeviceState::Disconnecting; + } +} + +void NetworkManager::registerDevice(NMDeviceAdapter* deviceAdapter, const QString& path) { + NetworkDevice* device = nullptr; + switch (deviceAdapter->type()) { + case NMDeviceType::Wifi: device = this->bindWirelessDevice(deviceAdapter, path); break; + default: device = new NetworkDevice(this); break; + } + deviceAdapter->setParent(device); + + // clang-format off + QObject::connect(deviceAdapter, &NMDeviceAdapter::hwAddressChanged, device, &NetworkDevice::setAddress); + QObject::connect(deviceAdapter, &NMDeviceAdapter::interfaceChanged, device, &NetworkDevice::setName); + QObject::connect(deviceAdapter, &NMDeviceAdapter::stateChanged, device, [device](NMDeviceState::Enum state) { device->setState(NetworkManager::toNetworkDeviceState(state)); }); + QObject::connect(device, &NetworkDevice::requestDisconnect, deviceAdapter, &NMDeviceAdapter::disconnect); + // clang-format on + + device->setAddress(deviceAdapter->hwAddress()); + device->setName(deviceAdapter->interface()); + device->setState(NetworkManager::toNetworkDeviceState(deviceAdapter->state())); + + this->mDeviceHash.insert(path, device); + emit deviceAdded(device); + + qCDebug(logNetworkManager) << "Registered device" << path; +} + +// Create a NetworkWifiDevice, NMWirelessManager, and connect the adapters +NetworkWifiDevice* +NetworkManager::bindWirelessDevice(NMDeviceAdapter* deviceAdapter, const QString& path) { + auto* device = new NetworkWifiDevice(this); + auto* wirelessAdapter = new NMWirelessAdapter(path, device); + auto* manager = new NMWirelessManager(device); + + // clang-format off + QObject::connect(wirelessAdapter, &NMWirelessAdapter::lastScanChanged, device, &NetworkWifiDevice::scanComplete); + QObject::connect(device, &NetworkWifiDevice::requestScan, wirelessAdapter, &NMWirelessAdapter::scan); + QObject::connect(wirelessAdapter, &NMWirelessAdapter::networkAdded, manager, &NMWirelessManager::networkAdded); + QObject::connect(wirelessAdapter, &NMWirelessAdapter::networkRemoved, manager, &NMWirelessManager::networkRemoved); + QObject::connect(deviceAdapter, &NMDeviceAdapter::connectionLoaded, manager, &NMWirelessManager::connectionLoaded); + QObject::connect(deviceAdapter, &NMDeviceAdapter::connectionRemoved, manager, &NMWirelessManager::connectionRemoved); + QObject::connect(manager, &NMWirelessManager::wifiNetworkAdded, device, &NetworkWifiDevice::wifiNetworkAdded); + QObject::connect(manager, &NMWirelessManager::wifiNetworkRemoved, device, &NetworkWifiDevice::wifiNetworkRemoved); + // clang-format on + + return device; +} + +void NetworkManager::onDeviceAdded(const QDBusObjectPath& path) { + this->queueDeviceRegistration(path.path()); +} + +void NetworkManager::onDeviceRemoved(const QDBusObjectPath& path) { + auto iter = this->mDeviceHash.find(path.path()); + + if (iter == this->mDeviceHash.end()) { + qCWarning(logNetworkManager) << "NetworkManager backend sent removal signal for" << path.path() + << "which is not registered."; + } else { + auto* device = iter.value(); + this->mDeviceHash.erase(iter); + emit deviceRemoved(device); + delete device; + qCDebug(logNetworkManager) << "Device" << path.path() << "removed."; + } +} + +bool NetworkManager::isAvailable() const { return this->proxy && this->proxy->isValid(); }; + +} // namespace qs::network diff --git a/src/network/nm/backend.hpp b/src/network/nm/backend.hpp new file mode 100644 index 00000000..1f396088 --- /dev/null +++ b/src/network/nm/backend.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../dbus/properties.hpp" +#include "../frontend.hpp" +#include "device.hpp" +#include "nm/dbus_nm_backend.h" + +namespace qs::network { + +class NetworkManager: public NetworkBackend { + Q_OBJECT; + +signals: + void deviceAdded(NetworkDevice* device); + void deviceRemoved(NetworkDevice* device); + +public: + explicit NetworkManager(QObject* parent = nullptr); + [[nodiscard]] bool isAvailable() const override; + +private slots: + void onDeviceAdded(const QDBusObjectPath& path); + void onDeviceRemoved(const QDBusObjectPath& path); + +private: + void init(); + void registerDevice(NMDeviceAdapter* deviceAdapter, const QString& path); + void registerDevices(); + void queueDeviceRegistration(const QString& path); + static NetworkDeviceState::Enum toNetworkDeviceState(NMDeviceState::Enum state); + NetworkWifiDevice* bindWirelessDevice(NMDeviceAdapter* deviceAdapter, const QString& path); + + QHash mDeviceHash; + + QS_DBUS_BINDABLE_PROPERTY_GROUP(NetworkManager, dbusProperties); + DBusNetworkManagerProxy* proxy = nullptr; +}; + +} // namespace qs::network diff --git a/src/network/nm/connection.cpp b/src/network/nm/connection.cpp new file mode 100644 index 00000000..001e5f21 --- /dev/null +++ b/src/network/nm/connection.cpp @@ -0,0 +1,119 @@ +#include "connection.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "../../dbus/properties.hpp" + +namespace qs::network { +using namespace qs::dbus; + +namespace { +Q_LOGGING_CATEGORY(logNetworkManager, "quickshell.network.networkmanager", QtWarningMsg); +} + +// NMConnectionAdapter + +NMConnectionSettingsAdapter::NMConnectionSettingsAdapter(const QString& path, QObject* parent) + : QObject(parent) { + qDBusRegisterMetaType(); + + this->proxy = new DBusNMConnectionSettingsProxy( + "org.freedesktop.NetworkManager", + path, + QDBusConnection::systemBus(), + this + ); + + if (!this->proxy->isValid()) { + qCWarning(logNetworkManager) << "Cannot create DBus interface for connection at" << path; + return; + } + + // clang-format off + QObject::connect(this->proxy, &DBusNMConnectionSettingsProxy::Updated, this, &NMConnectionSettingsAdapter::updateSettings); + // clang-format on + + this->connectionSettingsProperties.setInterface(this->proxy); + this->connectionSettingsProperties.updateAllViaGetAll(); + + this->updateSettings(); +} + +void NMConnectionSettingsAdapter::updateSettings() { + auto pending = this->proxy->GetSettings(); + auto* call = new QDBusPendingCallWatcher(pending, this); + + auto responseCallback = [this](QDBusPendingCallWatcher* call) { + const QDBusPendingReply reply = *call; + + if (reply.isError()) { + qCWarning(logNetworkManager) << "Failed to get settings: " << reply.error().message(); + } else { + this->bSettings = reply.value(); + } + + emit this->ready(); + delete call; + }; + + QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); +} + +bool NMConnectionSettingsAdapter::isValid() const { return this->proxy && this->proxy->isValid(); } +QString NMConnectionSettingsAdapter::address() const { + return this->proxy ? this->proxy->service() : QString(); +} +QString NMConnectionSettingsAdapter::path() const { + return this->proxy ? this->proxy->path() : QString(); +} + +NMActiveConnectionAdapter::NMActiveConnectionAdapter(const QString& path, QObject* parent) + : QObject(parent) { + + this->proxy = new DBusNMActiveConnectionProxy( + "org.freedesktop.NetworkManager", + path, + QDBusConnection::systemBus(), + this + ); + + if (!this->proxy->isValid()) { + qCWarning(logNetworkManager) << "Cannot create DBus interface for connection at" << path; + return; + } + + // clang-format off + QObject::connect(&this->activeConnectionProperties, &DBusPropertyGroup::getAllFinished, this, [this]() { emit this->ready(); }, Qt::SingleShotConnection); + QObject::connect(this->proxy, &DBusNMActiveConnectionProxy::StateChanged, this, &NMActiveConnectionAdapter::onStateChanged); + // clang-format on + + this->activeConnectionProperties.setInterface(this->proxy); + this->activeConnectionProperties.updateAllViaGetAll(); +} + +void NMActiveConnectionAdapter::onStateChanged(quint32 state, quint32 reason) { + auto enumState = static_cast(state); + auto enumReason = static_cast(reason); + + if (enumState != mState) { + this->mState = enumState; + this->mStateReason = enumReason; + emit this->stateChanged(enumState, enumReason); + } +} + +bool NMActiveConnectionAdapter::isValid() const { return this->proxy && this->proxy->isValid(); } +QString NMActiveConnectionAdapter::address() const { + return this->proxy ? this->proxy->service() : QString(); +} +QString NMActiveConnectionAdapter::path() const { + return this->proxy ? this->proxy->path() : QString(); +} + +} // namespace qs::network diff --git a/src/network/nm/connection.hpp b/src/network/nm/connection.hpp new file mode 100644 index 00000000..32ea1fb9 --- /dev/null +++ b/src/network/nm/connection.hpp @@ -0,0 +1,86 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../dbus/properties.hpp" +#include "dbus_types.hpp" +#include "enums.hpp" +#include "nm/dbus_nm_connection_settings.h" +#include "nm/dbus_nm_active_connection.h" + +namespace qs::network { + +class NMConnectionSettingsAdapter: public QObject { + Q_OBJECT; + +public: + explicit NMConnectionSettingsAdapter(const QString& path, QObject* parent = nullptr); + void updateSettings(); + [[nodiscard]] bool isValid() const; + [[nodiscard]] QString path() const; + [[nodiscard]] QString address() const; + [[nodiscard]] ConnectionSettingsMap settings() const { return this->bSettings; }; + +signals: + void settingsChanged(); + void ready(); + +private: + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(NMConnectionSettingsAdapter, ConnectionSettingsMap, bSettings, &NMConnectionSettingsAdapter::settingsChanged); + // clang-format on + + QS_DBUS_BINDABLE_PROPERTY_GROUP(NMConnectionSettingsAdapter, connectionSettingsProperties); + DBusNMConnectionSettingsProxy* proxy = nullptr; +}; + +class NMActiveConnectionAdapter: public QObject { + Q_OBJECT; + +public: + explicit NMActiveConnectionAdapter(const QString& path, QObject* parent = nullptr); + [[nodiscard]] bool isValid() const; + [[nodiscard]] QString path() const; + [[nodiscard]] QString address() const; + [[nodiscard]] QDBusObjectPath connection() const { return this->bConnection; }; + [[nodiscard]] bool isDefault() const { return this->bIsDefault; }; + [[nodiscard]] bool isDefault6() const { return this->bIsDefault6; }; + [[nodiscard]] NMActiveConnectionState::Enum state() const { return this->mState; }; + [[nodiscard]] NMActiveConnectionStateReason::Enum stateReason() const { return this->mStateReason; }; + +signals: + void stateChanged(NMActiveConnectionState::Enum state, NMActiveConnectionStateReason::Enum reason); + void connectionChanged(QDBusObjectPath path); + void isDefaultChanged(bool isDefault); + void isDefault6Changed(bool isDefault6); + void ready(); + +private slots: + void onStateChanged(quint32 state, quint32 reason); + +private: + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(NMActiveConnectionAdapter, QDBusObjectPath, bConnection, &NMActiveConnectionAdapter::connectionChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMActiveConnectionAdapter, bool, bIsDefault, &NMActiveConnectionAdapter::isDefaultChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMActiveConnectionAdapter, bool, bIsDefault6, &NMActiveConnectionAdapter::isDefault6Changed); + // clang-format on + + QS_DBUS_BINDABLE_PROPERTY_GROUP(NMActiveConnectionAdapter, activeConnectionProperties); + QS_DBUS_PROPERTY_BINDING(NMActiveConnectionAdapter, pConnection, bConnection, activeConnectionProperties, "Connection"); + QS_DBUS_PROPERTY_BINDING(NMActiveConnectionAdapter, pIsDefault, bIsDefault, activeConnectionProperties, "Default"); + QS_DBUS_PROPERTY_BINDING(NMActiveConnectionAdapter, pIsDefault6, bIsDefault6, activeConnectionProperties, "Default6"); + DBusNMActiveConnectionProxy* proxy = nullptr; + + NMActiveConnectionState::Enum mState = NMActiveConnectionState::Unknown; + NMActiveConnectionStateReason::Enum mStateReason = NMActiveConnectionStateReason::Unknown; +}; + +} // namespace qs::network diff --git a/src/network/nm/dbus_types.hpp b/src/network/nm/dbus_types.hpp new file mode 100644 index 00000000..dadbcf38 --- /dev/null +++ b/src/network/nm/dbus_types.hpp @@ -0,0 +1,9 @@ +#pragma once + +#include +#include +#include +#include + +using ConnectionSettingsMap = QMap; +Q_DECLARE_METATYPE(ConnectionSettingsMap); diff --git a/src/network/nm/device.cpp b/src/network/nm/device.cpp new file mode 100644 index 00000000..a52dc8f5 --- /dev/null +++ b/src/network/nm/device.cpp @@ -0,0 +1,137 @@ +#include "device.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "../../dbus/properties.hpp" + +namespace qs::dbus { + +DBusResult +DBusDataTransform::fromWire(quint32 wire) { + return DBusResult(static_cast(wire)); +} + +DBusResult +DBusDataTransform::fromWire(quint32 wire) { + return DBusResult(static_cast(wire)); +} + +} // namespace qs::dbus + +namespace qs::network { +using namespace qs::dbus; + +namespace { +Q_LOGGING_CATEGORY(logNetworkManager, "quickshell.network.networkmanager", QtWarningMsg); +} + +NMDeviceAdapter::NMDeviceAdapter(const QString& path, QObject* parent): QObject(parent) { + this->proxy = new DBusNMDeviceProxy( + "org.freedesktop.NetworkManager", + path, + QDBusConnection::systemBus(), + this + ); + + if (!this->proxy->isValid()) { + qCWarning(logNetworkManager) << "Cannot create DBus interface for device at" << path; + return; + } + + // clang-format off + QObject::connect(this, &NMDeviceAdapter::availableConnectionsChanged, this, &NMDeviceAdapter::onAvailableConnectionsChanged); + QObject::connect(this, &NMDeviceAdapter::activeConnectionChanged, this, &NMDeviceAdapter::onActiveConnectionChanged); + QObject::connect(&this->deviceProperties, &DBusPropertyGroup::getAllFinished, this, [this]() { emit this->ready(); }, Qt::SingleShotConnection); + // clang-format on + + this->deviceProperties.setInterface(this->proxy); + this->deviceProperties.updateAllViaGetAll(); +} + +void NMDeviceAdapter::onActiveConnectionChanged(const QDBusObjectPath& path) { + QString stringPath = path.path(); + if (this->mActiveConnection) { + QObject::disconnect(this->mActiveConnection, nullptr, this, nullptr); + emit this->activeConnectionRemoved(this->mActiveConnection); + delete this->mActiveConnection; + this->mActiveConnection = nullptr; + } + + if (stringPath != "/") { + auto* active = new NMActiveConnectionAdapter(stringPath, this); + this->mActiveConnection = active; + qCDebug(logNetworkManager) << "Registered active connection" << stringPath; + + QObject::connect( + active, + &NMActiveConnectionAdapter::ready, + this, + [this, active]() { + QString path = active->connection().path(); + if (!this->mConnectionMap.contains(path)) { + this->registerConnection(path); + } + emit this->activeConnectionLoaded(active); + }, + Qt::SingleShotConnection + ); + } +} + +void NMDeviceAdapter::onAvailableConnectionsChanged(const QList& paths) { + QSet newConnectionPaths; + for (const QDBusObjectPath& path: paths) { + newConnectionPaths.insert(path.path()); + } + + QSet addedConnections = newConnectionPaths - this->mConnectionPaths; + QSet removedConnections = this->mConnectionPaths - newConnectionPaths; + for (const QString& path: addedConnections) { + registerConnection(path); + } + for (const QString& path: removedConnections) { + auto* connection = this->mConnectionMap.take(path); + if (!connection) { + qCDebug(logNetworkManager) << "NetworkManager backend sent removal signal for" << path + << "which is not registered."; + } else { + emit this->connectionRemoved(connection); + delete connection; + } + this->mConnectionPaths.remove(path); + }; +} + +void NMDeviceAdapter::registerConnection(const QString& path) { + auto* connection = new NMConnectionSettingsAdapter(path, this); + if (!connection->isValid()) { + qCWarning(logNetworkManager) << "Ignoring invalid registration of" << path; + delete connection; + } else { + this->mConnectionMap.insert(path, connection); + this->mConnectionPaths.insert(path); + QObject::connect( + connection, + &NMConnectionSettingsAdapter::ready, + this, + [this, connection]() { emit this->connectionLoaded(connection); }, + Qt::SingleShotConnection + ); + qCDebug(logNetworkManager) << "Registered connection" << path; + } +} + +void NMDeviceAdapter::disconnect() { this->proxy->Disconnect(); } +bool NMDeviceAdapter::isValid() const { return this->proxy && this->proxy->isValid(); } +QString NMDeviceAdapter::address() const { + return this->proxy ? this->proxy->service() : QString(); +} +QString NMDeviceAdapter::path() const { return this->proxy ? this->proxy->path() : QString(); } + +} // namespace qs::network diff --git a/src/network/nm/device.hpp b/src/network/nm/device.hpp new file mode 100644 index 00000000..7c95e230 --- /dev/null +++ b/src/network/nm/device.hpp @@ -0,0 +1,102 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../dbus/properties.hpp" +#include "connection.hpp" +#include "enums.hpp" +#include "nm/dbus_nm_device.h" + +namespace qs::dbus { + +template <> +struct DBusDataTransform { + using Wire = quint32; + using Data = qs::network::NMDeviceType::Enum; + static DBusResult fromWire(Wire wire); +}; + +template <> +struct DBusDataTransform { + using Wire = quint32; + using Data = qs::network::NMDeviceState::Enum; + static DBusResult fromWire(Wire wire); +}; + +} // namespace qs::dbus + +namespace qs::network { + +// NMDeviceAdapter wraps the state of a NetworkManager device +// (org.freedesktop.NetworkManager.Device) and provides signals/slots +// that connect to a frontend NetworkDevice. +class NMDeviceAdapter: public QObject { + Q_OBJECT; + +public: + explicit NMDeviceAdapter(const QString& path, QObject* parent = nullptr); + + [[nodiscard]] bool isValid() const; + [[nodiscard]] QString path() const; + [[nodiscard]] QString address() const; + [[nodiscard]] QString interface() { return this->bInterface; }; + [[nodiscard]] QString hwAddress() { return this->bHwAddress; }; + [[nodiscard]] NMDeviceType::Enum type() { return this->bType; }; + [[nodiscard]] NMDeviceState::Enum state() { return this->bState; }; + +public slots: + void disconnect(); + +signals: + void ready(); + void connectionLoaded(NMConnectionSettingsAdapter* connection); + void connectionRemoved(NMConnectionSettingsAdapter* connection); + void activeConnectionLoaded(NMActiveConnectionAdapter* connection); + void activeConnectionRemoved(NMActiveConnectionAdapter* connection); + void interfaceChanged(const QString& interface); + void hwAddressChanged(const QString& hwAddress); + void typeChanged(NMDeviceType::Enum type); + void stateChanged(NMDeviceState::Enum state); + void availableConnectionsChanged(QList paths); + void activeConnectionChanged(const QDBusObjectPath& connection); + +private slots: + void onAvailableConnectionsChanged(const QList& paths); + void onActiveConnectionChanged(const QDBusObjectPath& path); + +private: + void registerConnection(const QString& path); + // Connection lookups + QSet mConnectionPaths; + QHash mConnectionMap; + NMActiveConnectionAdapter* mActiveConnection = nullptr; + + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(NMDeviceAdapter, QString, bInterface, &NMDeviceAdapter::interfaceChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMDeviceAdapter, QString, bHwAddress, &NMDeviceAdapter::hwAddressChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMDeviceAdapter, NMDeviceState::Enum, bState, &NMDeviceAdapter::stateChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMDeviceAdapter, NMDeviceType::Enum, bType, &NMDeviceAdapter::typeChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMDeviceAdapter, QList, bAvailableConnections, &NMDeviceAdapter::availableConnectionsChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMDeviceAdapter, QDBusObjectPath, bActiveConnection, &NMDeviceAdapter::activeConnectionChanged); + + QS_DBUS_BINDABLE_PROPERTY_GROUP(NMDeviceAdapter, deviceProperties); + QS_DBUS_PROPERTY_BINDING(NMDeviceAdapter, pName, bInterface, deviceProperties, "Interface"); + QS_DBUS_PROPERTY_BINDING(NMDeviceAdapter, pAddress, bHwAddress, deviceProperties, "HwAddress"); + QS_DBUS_PROPERTY_BINDING(NMDeviceAdapter, pType, bType, deviceProperties, "DeviceType"); + QS_DBUS_PROPERTY_BINDING(NMDeviceAdapter, pState, bState, deviceProperties, "State"); + QS_DBUS_PROPERTY_BINDING(NMDeviceAdapter, pAvailableConnections, bAvailableConnections, deviceProperties, "AvailableConnections"); + QS_DBUS_PROPERTY_BINDING(NMDeviceAdapter, pActiveConnection, bActiveConnection, deviceProperties, "ActiveConnection"); + // clang-format on + + DBusNMDeviceProxy* proxy = nullptr; +}; + +} // namespace qs::network diff --git a/src/network/nm/enums.hpp b/src/network/nm/enums.hpp new file mode 100644 index 00000000..1b9b7885 --- /dev/null +++ b/src/network/nm/enums.hpp @@ -0,0 +1,273 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace qs::network { + +// 802.11 specific device encryption and authentication capabilities. +// In sync with https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMDeviceWifiCapabilities. +class NMWirelessCapabilities: public QObject { + Q_OBJECT; + +public: + enum Enum : quint16 { + None = 0, + CipherWep40 = 1, + CipherWep104 = 2, + CipherTkip = 4, + CipherCcmp = 8, + Wpa = 16, + Rsn = 32, + Ap = 64, + Adhoc = 128, + FreqValid = 256, + Freq2Ghz = 512, + Freq5Ghz = 1024, + Freq6Ghz = 2048, + Mesh = 4096, + IbssRsn = 8192, + }; + Q_ENUM(Enum); +}; + +class NMWirelessSecurityType: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + Wpa3SuiteB192 = 0, + Sae = 1, + Wpa2Eap = 2, + Wpa2Psk = 3, + WpaEap = 4, + WpaPsk = 5, + StaticWep = 6, + DynamicWep = 7, + Leap = 8, + Owe = 9, + None = 10, + Unknown = 11, + }; + Q_ENUM(Enum); + Q_INVOKABLE static QString toString(NMWirelessSecurityType::Enum type) { + switch (type) { + case Wpa3SuiteB192: return "WPA3 Suite B 192-bit"; + case Sae: return "WPA3"; + case Wpa2Eap: return "WPA2 Enterprise"; + case Wpa2Psk: return "WPA2"; + case WpaEap: return "WPA Enterprise"; + case WpaPsk: return "WPA"; + case StaticWep: return "WEP"; + case DynamicWep: return "Dynamic WEP"; + case Leap: return "LEAP"; + case Owe: return "OWE"; + case None: return "None"; + default: return "Unknown"; + } + } +}; + +// In sync with https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMDeviceState. +class NMDeviceState: public QObject { + Q_OBJECT; + +public: + enum Enum : quint8 { + Unkown = 0, + Unmanaged = 10, + Unavailable = 20, + Disconnected = 30, + Prepare = 40, + Config = 50, + NeedAuth = 60, + IPConfig = 70, + IPCheck = 80, + Secondaries = 90, + Activated = 100, + Deactivating = 110, + Failed = 120, + }; + Q_ENUM(Enum); +}; + +// Indicates the type of hardware represented by a device object. +// In sync with https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMDeviceType. +class NMDeviceType: public QObject { + Q_OBJECT; + +public: + enum Enum : quint8 { + Unknown = 0, + Generic = 14, + Ethernet = 1, + Wifi = 2, + Unused1 = 3, + Unused2 = 4, + Bluetooth = 5, + OlpcMesh = 6, + Wimax = 7, + Modem = 8, + Infiniband = 9, + Bond = 10, + Vlan = 11, + Adsl = 12, + Bridge = 13, + Team = 15, + Tun = 16, + Tunnel = 17, + Macvlan = 18, + Vxlan = 19, + Veth = 20, + Macsec = 21, + Dummy = 22, + Ppp = 23, + OvsInterface = 24, + OvsPort = 25, + OvsBridge = 26, + Wpan = 27, + Lowpan = 28, + WireGuard = 29, + WifiP2P = 30, + Vrf = 31, + Loopback = 32, + Hsr = 33, + Ipvlan = 34, + }; + Q_ENUM(Enum); +}; + +// Indicates the 802.11 mode an access point is currently in. +// In sync with https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NM80211Mode. +class NM80211Mode: public QObject { + Q_OBJECT; + +public: + enum Enum : quint8 { + Unknown = 0, + Adhoc = 1, + Infra = 2, + Ap = 3, + Mesh = 3, + }; + Q_ENUM(Enum); +}; + +// 802.11 access point flags. +// In sync with https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NM80211ApSecurityFlags. +class NM80211ApFlags: public QObject { + Q_OBJECT; + +public: + enum Enum : quint8 { + None = 0, + Privacy = 1, + Wps = 2, + WpsPbc = 4, + WpsPin = 8, + }; + Q_ENUM(Enum); +}; + +// 802.11 access point security and authentication flags. +// These flags describe the current system requirements of an access point as determined from the access point's beacon. +// In sync with https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NM80211ApSecurityFlags. +class NM80211ApSecurityFlags: public QObject { + Q_OBJECT; + +public: + enum Enum : quint16 { + None = 0, + PairWep40 = 1, + PairWep104 = 2, + PairTkip = 4, + PairCcmp = 8, + GroupWep40 = 16, + GroupWep104 = 32, + GroupTkip = 64, + GroupCcmp = 128, + KeyMgmtPsk = 256, + KeyMgmt8021x = 512, + KeyMgmtSae = 1024, + KeyMgmtOwe = 2048, + KeyMgmtOweTm = 4096, + KeyMgmtEapSuiteB192 = 8192, + }; + Q_ENUM(Enum); +}; + +// Indicates the state of a connection to a specific network while it is starting, connected, or disconnected from that network. +// In sync with https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMActiveConnectionState. +class NMActiveConnectionState: public QObject { + Q_OBJECT; + +public: + enum Enum : quint8 { + Unknown = 0, + Activating = 1, + Activated = 2, + Deactivating = 3, + Deactivated = 4 + }; + Q_ENUM(Enum); +}; + +// Active connection state reasons. +// In sync with https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMActiveConnectionStateReason. +class NMActiveConnectionStateReason: public QObject { + Q_OBJECT; + +public: + enum Enum : quint8 { + Unknown = 0, + None = 1, + UserDisconnected = 2, + DeviceDisconnected = 3, + ServiceStopped = 4, + IpConfigInvalid = 5, + ConnectTimeout = 6, + ServiceStartTimeout = 7, + ServiceStartFailed = 8, + NoSecrets = 9, + LoginFailed = 10, + ConnectionRemoved = 11, + DependencyFailed = 12, + DeviceRealizeFailed = 13, + DeviceRemoved = 14 + }; + Q_ENUM(Enum); + Q_INVOKABLE static QString toString(NMActiveConnectionStateReason::Enum reason) { + switch (reason) { + case Unknown: return "The reason for the active connection state change is unknown."; + case None: return "No reason was given for the active connection state change."; + case UserDisconnected: + return "The active connection changed state because the user disconnected it."; + case DeviceDisconnected: + return "The active connection changed state because the device it was using was " + "disconnected."; + case ServiceStopped: return "The service providing the VPN connection was stopped."; + case IpConfigInvalid: return "The IP config of the active connection was invalid."; + case ConnectTimeout: return "The connection attempt to the VPN service timed out."; + case ServiceStartTimeout: + return "A timeout occurred while starting the service providing the VPN connection."; + case ServiceStartFailed: return "Starting the service providing the VPN connection failed."; + case NoSecrets: return "Necessary secrets for the connection were not provided."; + case LoginFailed: return "Authentication to the server failed."; + case ConnectionRemoved: return "Necessary secrets for the connection were not provided."; + case DependencyFailed: return " Master connection of this connection failed to activate."; + case DeviceRealizeFailed: return "Could not create the software device link."; + case DeviceRemoved: return "The device this connection depended on disappeared."; + }; + }; +}; + +} // namespace qs::network diff --git a/src/network/nm/org.freedesktop.NetworkManager.AccessPoint.xml b/src/network/nm/org.freedesktop.NetworkManager.AccessPoint.xml new file mode 100644 index 00000000..c5e7737d --- /dev/null +++ b/src/network/nm/org.freedesktop.NetworkManager.AccessPoint.xml @@ -0,0 +1,4 @@ + + + + diff --git a/src/network/nm/org.freedesktop.NetworkManager.Connection.Active.xml b/src/network/nm/org.freedesktop.NetworkManager.Connection.Active.xml new file mode 100644 index 00000000..fa0e778c --- /dev/null +++ b/src/network/nm/org.freedesktop.NetworkManager.Connection.Active.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/network/nm/org.freedesktop.NetworkManager.Device.Wireless.xml b/src/network/nm/org.freedesktop.NetworkManager.Device.Wireless.xml new file mode 100644 index 00000000..984f43dc --- /dev/null +++ b/src/network/nm/org.freedesktop.NetworkManager.Device.Wireless.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/src/network/nm/org.freedesktop.NetworkManager.Device.xml b/src/network/nm/org.freedesktop.NetworkManager.Device.xml new file mode 100644 index 00000000..322635f3 --- /dev/null +++ b/src/network/nm/org.freedesktop.NetworkManager.Device.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/src/network/nm/org.freedesktop.NetworkManager.Settings.Connection.xml b/src/network/nm/org.freedesktop.NetworkManager.Settings.Connection.xml new file mode 100644 index 00000000..dfe3cae6 --- /dev/null +++ b/src/network/nm/org.freedesktop.NetworkManager.Settings.Connection.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/network/nm/org.freedesktop.NetworkManager.xml b/src/network/nm/org.freedesktop.NetworkManager.xml new file mode 100644 index 00000000..4f9ca03e --- /dev/null +++ b/src/network/nm/org.freedesktop.NetworkManager.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/network/nm/utils.cpp b/src/network/nm/utils.cpp new file mode 100644 index 00000000..1a62540d --- /dev/null +++ b/src/network/nm/utils.cpp @@ -0,0 +1,342 @@ +#include "utils.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "dbus_types.hpp" +#include "enums.hpp" + +namespace qs::network { + +NMWirelessSecurityType::Enum securityFromConnectionSettings(const ConnectionSettingsMap& settings) { + const QVariantMap& security = settings.value("802-11-wireless-security"); + if (security.isEmpty()) { + return NMWirelessSecurityType::Unknown; + }; + + QString keyMgmt = security["key-mgmt"].toString(); + QString authAlg = security["auth-alg"].toString(); + QList proto = security["proto"].toList(); + + if (keyMgmt == "none") { + return NMWirelessSecurityType::StaticWep; + } else if (keyMgmt == "ieee8021x") { + if (authAlg == "leap") { + return NMWirelessSecurityType::Leap; + } else { + return NMWirelessSecurityType::DynamicWep; + } + } else if (keyMgmt == "wpa-psk") { + if (proto.contains("wpa") && proto.contains("rsn")) { + return NMWirelessSecurityType::WpaPsk; + } + return NMWirelessSecurityType::Wpa2Psk; + } else if (keyMgmt == "wpa-eap") { + if (proto.contains("wpa") && proto.contains("rsn")) { + return NMWirelessSecurityType::WpaEap; + } + return NMWirelessSecurityType::Wpa2Eap; + } else if (keyMgmt == "sae") { + return NMWirelessSecurityType::Sae; + } else if (keyMgmt == "wpa-eap-suite-b-192") { + return NMWirelessSecurityType::Wpa3SuiteB192; + } + + return NMWirelessSecurityType::None; +} + +bool deviceSupportsApCiphers( + NMWirelessCapabilities::Enum caps, + NM80211ApSecurityFlags::Enum apFlags, + NMWirelessSecurityType::Enum type +) { + bool havePair = false; + bool haveGroup = false; + // Device needs to support at least one pairwise and one group cipher + + if (type == NMWirelessSecurityType::StaticWep) { + // Static WEP only uses group ciphers + havePair = true; + } else { + if (caps & NMWirelessCapabilities::CipherWep40 && apFlags & NM80211ApSecurityFlags::PairWep40) { + havePair = true; + } + if (caps & NMWirelessCapabilities::CipherWep104 && apFlags & NM80211ApSecurityFlags::PairWep104) + { + havePair = true; + } + if (caps & NMWirelessCapabilities::CipherTkip && apFlags & NM80211ApSecurityFlags::PairTkip) { + havePair = true; + } + if (caps & NMWirelessCapabilities::CipherCcmp && apFlags & NM80211ApSecurityFlags::PairCcmp) { + havePair = true; + } + } + + if (caps & NMWirelessCapabilities::CipherWep40 && apFlags & NM80211ApSecurityFlags::GroupWep40) { + haveGroup = true; + } + if (caps & NMWirelessCapabilities::CipherWep104 && apFlags & NM80211ApSecurityFlags::GroupWep104) + { + haveGroup = true; + } + if (type == NMWirelessSecurityType::StaticWep) { + if (caps & NMWirelessCapabilities::CipherTkip && apFlags & NM80211ApSecurityFlags::GroupTkip) { + haveGroup = true; + } + if (caps & NMWirelessCapabilities::CipherCcmp && apFlags & NM80211ApSecurityFlags::GroupCcmp) { + haveGroup = true; + } + } + + return (havePair && haveGroup); +} + +// In sync with NetworkManager/libnm-core/nm-utils.c:nm_utils_security_valid() +// Given a set of device capabilities, and a desired security type to check +// against, determines whether the combination of device, desired security type, +// and AP capabilities intersect. +bool securityIsValid( + NMWirelessSecurityType::Enum type, + NMWirelessCapabilities::Enum caps, + bool haveAp, + bool adhoc, + NM80211ApFlags::Enum apFlags, + NM80211ApSecurityFlags::Enum apWpa, + NM80211ApSecurityFlags::Enum apRsn +) { + bool good = true; + + if (!haveAp) { + if (type == NMWirelessSecurityType::None) { + return true; + } + if ((type == NMWirelessSecurityType::StaticWep) + || ((type == NMWirelessSecurityType::DynamicWep) && !adhoc) + || ((type == NMWirelessSecurityType::Leap) && !adhoc)) + { + return caps & NMWirelessCapabilities::CipherWep40 + || caps & NMWirelessCapabilities::CipherWep104; + } + } + + switch (type) { + case NMWirelessSecurityType::None: + if (apFlags & NM80211ApFlags::Privacy) { + return false; + } + if (apWpa || apRsn) { + return false; + } + break; + case NMWirelessSecurityType::Leap: + if (adhoc) { + return false; + } + case NMWirelessSecurityType::StaticWep: + if (!(apFlags & NM80211ApFlags::Privacy)) { + return false; + } + if (apWpa || apRsn) { + if (!deviceSupportsApCiphers(caps, apWpa, NMWirelessSecurityType::StaticWep)) { + if (!deviceSupportsApCiphers(caps, apRsn, NMWirelessSecurityType::StaticWep)) { + return false; + } + } + } + break; + case NMWirelessSecurityType::DynamicWep: + if (adhoc) { + return false; + } + if (apRsn || !(apFlags & NM80211ApFlags::Privacy)) { + return false; + } + if (apWpa) { + if (!(apWpa & NM80211ApSecurityFlags::KeyMgmt8021x)) { + return false; + } + if (!deviceSupportsApCiphers(caps, apWpa, NMWirelessSecurityType::DynamicWep)) { + return false; + } + } + break; + case NMWirelessSecurityType::WpaPsk: + if (adhoc) { + return false; + } + + if (!(caps & NMWirelessCapabilities::Wpa)) { + return false; + } + if (haveAp) { + if (apWpa & NM80211ApSecurityFlags::KeyMgmtPsk) { + if (apWpa & NM80211ApSecurityFlags::PairTkip && caps & NMWirelessCapabilities::CipherTkip) { + return true; + } + if (apWpa & NM80211ApSecurityFlags::PairCcmp && caps & NMWirelessCapabilities::CipherCcmp) { + return true; + } + } + return false; + } + break; + case NMWirelessSecurityType::Wpa2Psk: + if (!(caps & NMWirelessCapabilities::Rsn)) { + return false; + } + if (haveAp) { + if (adhoc) { + if (!(caps & NMWirelessCapabilities::IbssRsn)) { + return false; + } + if (apRsn & NM80211ApSecurityFlags::PairCcmp && caps & NMWirelessCapabilities::CipherCcmp) { + return true; + } + } else { + if (apRsn & NM80211ApSecurityFlags::KeyMgmtPsk) { + if (apRsn & NM80211ApSecurityFlags::PairTkip && caps & NMWirelessCapabilities::CipherTkip) + { + return true; + } + if (apRsn & NM80211ApSecurityFlags::PairCcmp && caps & NMWirelessCapabilities::CipherCcmp) + { + return true; + } + } + } + return false; + } + break; + case NMWirelessSecurityType::WpaEap: + if (adhoc) { + return false; + } + if (!(caps & NMWirelessCapabilities::Wpa)) { + return false; + } + if (haveAp) { + if (!(apWpa & NM80211ApSecurityFlags::KeyMgmt8021x)) { + return false; + } + // Ensure at least one WPA cipher is supported + if (!deviceSupportsApCiphers(caps, apWpa, NMWirelessSecurityType::WpaEap)) { + return false; + } + } + break; + case NMWirelessSecurityType::Wpa2Eap: + if (adhoc) { + return false; + } + if (!(caps & NMWirelessCapabilities::Rsn)) { + return false; + } + if (haveAp) { + if (!(apRsn & NM80211ApSecurityFlags::KeyMgmt8021x)) { + return false; + } + // Ensure at least one WPA cipher is supported + if (!deviceSupportsApCiphers(caps, apRsn, NMWirelessSecurityType::Wpa2Eap)) { + return false; + } + } + break; + case NMWirelessSecurityType::Sae: + if (!(caps & NMWirelessCapabilities::Rsn)) { + return false; + } + if (haveAp) { + if (adhoc) { + if (!(caps & NMWirelessCapabilities::IbssRsn)) { + return false; + } + if (apRsn & NM80211ApSecurityFlags::PairCcmp && caps & NMWirelessCapabilities::CipherCcmp) { + return true; + } + } else { + if (apRsn & NM80211ApSecurityFlags::KeyMgmtSae) { + if (apRsn & NM80211ApSecurityFlags::PairTkip && caps & NMWirelessCapabilities::CipherTkip) + { + return true; + } + if (apRsn & NM80211ApSecurityFlags::PairCcmp && caps & NMWirelessCapabilities::CipherCcmp) + { + return true; + } + } + } + return false; + } + break; + case NMWirelessSecurityType::Owe: + if (adhoc) { + return false; + } + if (!(caps & NMWirelessCapabilities::Rsn)) { + return false; + } + if (haveAp) { + if (!(apRsn & NM80211ApSecurityFlags::KeyMgmtOwe) + && !(apRsn & NM80211ApSecurityFlags::KeyMgmtOweTm)) + { + return false; + } + } + break; + case NMWirelessSecurityType::Wpa3SuiteB192: + if (adhoc) { + return false; + } + if (!(caps & NMWirelessCapabilities::Rsn)) { + return false; + } + if (haveAp && !(apRsn & NM80211ApSecurityFlags::KeyMgmtEapSuiteB192)) { + return false; + } + break; + default: good = false; break; + } + + return good; +} + +NMWirelessSecurityType::Enum findBestWirelessSecurity( + NMWirelessCapabilities::Enum caps, + bool haveAp, + bool adHoc, + NM80211ApFlags::Enum apFlags, + NM80211ApSecurityFlags::Enum apWpa, + NM80211ApSecurityFlags::Enum apRsn +) { + // The ordering of this list is a pragmatic combination of security level and popularity + const QList types = { + NMWirelessSecurityType::Wpa3SuiteB192, + NMWirelessSecurityType::Sae, + NMWirelessSecurityType::Wpa2Eap, + NMWirelessSecurityType::Wpa2Psk, + NMWirelessSecurityType::WpaEap, + NMWirelessSecurityType::WpaPsk, + NMWirelessSecurityType::StaticWep, + NMWirelessSecurityType::DynamicWep, + NMWirelessSecurityType::Leap, + NMWirelessSecurityType::Owe, + NMWirelessSecurityType::None + }; + + for (NMWirelessSecurityType::Enum type: types) { + if (securityIsValid(type, caps, haveAp, adHoc, apFlags, apWpa, apRsn)) { + return type; + } + } + return NMWirelessSecurityType::Unknown; +} + +} // namespace qs::network diff --git a/src/network/nm/utils.hpp b/src/network/nm/utils.hpp new file mode 100644 index 00000000..7cdcc0b8 --- /dev/null +++ b/src/network/nm/utils.hpp @@ -0,0 +1,46 @@ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "dbus_types.hpp" +#include "enums.hpp" + +namespace qs::network { + +NMWirelessSecurityType::Enum securityFromConnectionSettings(const ConnectionSettingsMap& settings); + +bool deviceSupportsApCiphers( + NMWirelessCapabilities::Enum caps, + NM80211ApSecurityFlags::Enum apFlags, + NMWirelessSecurityType::Enum type +); + +bool securityIsValid( + NMWirelessSecurityType::Enum type, + NMWirelessCapabilities::Enum caps, + bool haveAp, + bool adhoc, + NM80211ApFlags::Enum apFlags, + NM80211ApSecurityFlags::Enum apWpa, + NM80211ApSecurityFlags::Enum apRsn +); + +NMWirelessSecurityType::Enum findBestWirelessSecurity( + NMWirelessCapabilities::Enum caps, + bool haveAp, + bool adHoc, + NM80211ApFlags::Enum apFlags, + NM80211ApSecurityFlags::Enum apWpa, + NM80211ApSecurityFlags::Enum apRsn +); + +} // namespace qs::network diff --git a/src/network/nm/wireless.cpp b/src/network/nm/wireless.cpp new file mode 100644 index 00000000..e539e70e --- /dev/null +++ b/src/network/nm/wireless.cpp @@ -0,0 +1,422 @@ +#include "wireless.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "dbus_types.hpp" +#include "nm/enums.hpp" +#include "utils.hpp" + +namespace qs::dbus { + +DBusResult +DBusDataTransform::fromWire(quint32 wire) { + return DBusResult(static_cast(wire)); +} + +} // namespace qs::dbus + +namespace qs::network { +using namespace qs::dbus; + +namespace { +Q_LOGGING_CATEGORY(logNetworkManager, "quickshell.network.networkmanager", QtWarningMsg); +} + +NMWirelessNetwork::NMWirelessNetwork( + NMWirelessCapabilities::Enum caps, + QByteArray ssid, + QObject* parent +) + : QObject(parent) + , mDeviceCapabilities(caps) + , mSsid(std::move(ssid)) {} + +void NMWirelessNetwork::updateSignalStrength() { + quint8 max = 0; + NMAccessPointAdapter* maxAP = nullptr; + for (auto* ap: mAccessPoints) { + quint8 signal = ap->signalStrength(); + if (signal > max) { + max = signal; + maxAP = ap; + } + } + if (this->bMaxSignal != max) { + this->bMaxSignal = max; + } + if (maxAP && this->mReferenceAP != maxAP) { + this->mReferenceAP = maxAP; + emit referenceAccessPointChanged(maxAP); + } +} + +void NMWirelessNetwork::addAccessPoint(NMAccessPointAdapter* ap) { + if (this->mAccessPoints.contains(ap)) { + qCWarning(logNetworkManager) << "Access point" << ap->path() << "was already in WifiNetwork" + << this; + return; + } + + this->mAccessPoints.append(ap); + if (this->mReferenceAP == nullptr) { + this->mReferenceAP = ap; + emit referenceAccessPointChanged(ap); + } + QObject::connect( + ap, + &NMAccessPointAdapter::signalStrengthChanged, + this, + &NMWirelessNetwork::updateSignalStrength + ); + this->updateSignalStrength(); +} + +void NMWirelessNetwork::removeAccessPoint(NMAccessPointAdapter* ap) { + if (mAccessPoints.removeOne(ap)) { + QObject::disconnect(ap, nullptr, this, nullptr); + this->updateSignalStrength(); + } +} + +NMWirelessSecurityType::Enum NMWirelessNetwork::findBestSecurity() { + if (this->mReferenceAP) { + auto* ap = this->mReferenceAP; + return findBestWirelessSecurity( + this->mDeviceCapabilities, + true, + ap->mode() == NM80211Mode::Adhoc, + ap->flags(), + ap->wpaFlags(), + ap->rsnFlags() + ); + } + return NMWirelessSecurityType::Unknown; +} + +NMWirelessAdapter::NMWirelessAdapter(const QString& path, QObject* parent): QObject(parent) { + this->proxy = new DBusNMWirelessProxy( + "org.freedesktop.NetworkManager", + path, + QDBusConnection::systemBus(), + this + ); + + if (!this->proxy->isValid()) { + qCWarning(logNetworkManager) << "Cannot create DBus interface for wireless device at" << path; + return; + } + + // We need the properties available to successfully register access points. + // Their security type is dependent on this adapters capabilities. + QObject::connect( + &this->wirelessProperties, + &DBusPropertyGroup::getAllFinished, + this, + [this]() { this->init(); }, + Qt::SingleShotConnection + ); + + this->wirelessProperties.setInterface(this->proxy); + this->wirelessProperties.updateAllViaGetAll(); +} + +void NMWirelessAdapter::init() { + // clang-format off + QObject::connect(this->proxy, &DBusNMWirelessProxy::AccessPointAdded, this, &NMWirelessAdapter::onAccessPointAdded); + QObject::connect(this->proxy, &DBusNMWirelessProxy::AccessPointRemoved, this, &NMWirelessAdapter::onAccessPointRemoved); + // clang-format on + this->registerAccessPoints(); +} + +void NMWirelessAdapter::onAccessPointAdded(const QDBusObjectPath& path) { + this->registerAccessPoint(path.path()); +} + +void NMWirelessAdapter::onAccessPointRemoved(const QDBusObjectPath& path) { + auto* ap = mApMap.take(path.path()); + + if (!ap) { + qCDebug(logNetworkManager) << "NetworkManager backend sent removal signal for" << path.path() + << "which is not registered."; + return; + } + + QObject::disconnect(ap, nullptr, nullptr, nullptr); + removeApFromNetwork(ap); + delete ap; +} + +void NMWirelessAdapter::registerAccessPoints() { + auto pending = this->proxy->GetAllAccessPoints(); + auto* call = new QDBusPendingCallWatcher(pending, this); + + auto responseCallback = [this](QDBusPendingCallWatcher* call) { + const QDBusPendingReply> reply = *call; + + if (reply.isError()) { + qCWarning(logNetworkManager) + << "Failed to get all access points: " << reply.error().message(); + } else { + for (const QDBusObjectPath& devicePath: reply.value()) { + this->registerAccessPoint(devicePath.path()); + } + } + + delete call; + }; + + QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); +} + +void NMWirelessAdapter::registerAccessPoint(const QString& path) { + if (this->mApMap.contains(path)) { + qCDebug(logNetworkManager) << "Skipping duplicate registration of access point" << path; + return; + } + + auto* ap = new NMAccessPointAdapter(path, this); + + if (!ap->isValid()) { + qCWarning(logNetworkManager) << "Could not create DBus interface for access point at" << path; + delete ap; + return; + } + + this->mApMap.insert(path, ap); + qCDebug(logNetworkManager) << "Registered access point" << path; + + // Wait for properties + QObject::connect( + ap, + &NMAccessPointAdapter::ready, + this, + [this, ap]() { this->onAccessPointReady(ap); }, + Qt::SingleShotConnection + ); +} + +void NMWirelessAdapter::onAccessPointReady(NMAccessPointAdapter* ap) { + if (!ap->ssid().isEmpty()) { + this->addApToNetwork(ap, ap->ssid()); + } + + // The access points SSID can change/hide + QObject::connect( + ap, + &NMAccessPointAdapter::ssidChanged, + this, + [this, ap](const QByteArray& ssid) { + if (ssid.isEmpty()) { + this->removeApFromNetwork(ap); + } else { + qCDebug(logNetworkManager) + << "Access point" << ap->path() << "changed to ssid" << ap->ssid(); + this->addApToNetwork(ap, ssid); + } + } + ); +} + +void NMWirelessAdapter::addApToNetwork(NMAccessPointAdapter* ap, const QByteArray& ssid) { + // Remove AP from previous network + removeApFromNetwork(ap); + + auto* network = mNetworkMap.value(ssid); + if (!network) { + network = new NMWirelessNetwork(this->capabilities(), ssid, this); + network->addAccessPoint(ap); + this->mNetworkMap.insert(ssid, network); + this->mSsidMap.insert(ap->path(), ssid); + emit this->networkAdded(network); + } else { + network->addAccessPoint(ap); + this->mSsidMap.insert(ap->path(), ssid); + } +} + +void NMWirelessAdapter::removeApFromNetwork(NMAccessPointAdapter* ap) { + QByteArray ssid = mSsidMap.take(ap->path()); + if (ssid.isEmpty()) return; // AP wasn't previously associated with a network + + auto* network = mNetworkMap.value(ssid); + network->removeAccessPoint(ap); + + // No access points remain for the wifi network + if (network->isEmpty()) { + mNetworkMap.remove(ssid); + emit networkRemoved(network); + delete network; + } +} + +void NMWirelessAdapter::scan() { this->proxy->RequestScan({}); } +bool NMWirelessAdapter::isValid() const { return this->proxy && this->proxy->isValid(); } +QString NMWirelessAdapter::address() const { + return this->proxy ? this->proxy->service() : QString(); +} +QString NMWirelessAdapter::path() const { return this->proxy ? this->proxy->path() : QString(); } + +NMWirelessManager::NMWirelessManager(QObject* parent): QObject(parent) {}; + +void NMWirelessManager::connectionLoaded(NMConnectionSettingsAdapter* connection) { + ConnectionSettingsMap settings = connection->settings(); + if (settings["connection"]["id"].toString().isEmpty() + || settings["connection"]["uuid"].toString().isEmpty() + || !settings.contains("802-11-wireless") + || settings["802-11-wireless"]["mode"].toString() == "ap") // Can't be devices own hotspot + { + return; + } + + // Check if this connection already supplies an item + QString uuid = settings["connection"]["uuid"].toString(); + if (this->mConnectionMap.contains(uuid)) { + return; + qCWarning(logNetworkManager) << "Wireless manager out of sync"; + }; + mConnectionMap.insert(uuid, connection); + + // Is there an item with only a network supplying it? + QString ssid = settings["802-11-wireless"]["ssid"].toString(); + for (WifiNetwork* item: mSsidToItem.values(ssid)) { + if (!item->property("saved").toBool()) { + // Supply existing item + item->setKnown(true); + item->setNmSecurity(securityFromConnectionSettings(settings)); + mUuidToItem.insert(uuid, item); + return; + } + } + + // Create a new item and supply it with only the connection + auto* item = new WifiNetwork(this); + item->setSsid(ssid); + item->setKnown(true); + item->setNmSecurity(securityFromConnectionSettings(settings)); + mUuidToItem.insert(uuid, item); + emit wifiNetworkAdded(item); + + // Is there a network that can supply this item? + auto* network = mNetworkMap.value(ssid); + if (network) { + item->setSignalStrength(network->signalStrength()); + QObject::connect( + network, + &NMWirelessNetwork::signalStrengthChanged, + item, + &WifiNetwork::setSignalStrength + ); + mSsidToItem.insert(ssid, item); + } +}; + +void NMWirelessManager::connectionRemoved(NMConnectionSettingsAdapter* connection) { + ConnectionSettingsMap settings = connection->settings(); + + // Check if this connection supplies an item + QString uuid = settings["connection"]["uuid"].toString(); + if (!this->mConnectionMap.contains(uuid)) { + return; + qCWarning(logNetworkManager) << "Wireless manager out of sync"; + } + mConnectionMap.take(uuid); + + // Does the item this connectoin supplies also have a network supplying it? + auto* item = mUuidToItem.take(uuid); + QString ssid = settings["802-11-wireless"]["ssid"].toString(); + if (mSsidToItem.contains(ssid)) { + // Is the network supplying multiple items? + if (mSsidToItem.count(ssid) != 1) { + mSsidToItem.remove(ssid, item); + emit wifiNetworkRemoved(item); + delete item; + } else { + // Keep the item and supply only from network + item->setKnown(false); + auto* network = mNetworkMap.value(ssid); + item->setNmSecurity(network->findBestSecurity()); + } + return; + } + + // Delete item + emit wifiNetworkRemoved(item); + delete item; +}; + +void NMWirelessManager::networkAdded(NMWirelessNetwork* network) { + QString ssid = network->ssid(); + if (this->mNetworkMap.contains(ssid)) { + return; + }; + mNetworkMap.insert(ssid, network); + + // Find all existing items with a connection supplier + quint8 count = 0; + for (WifiNetwork* item: mUuidToItem.values()) { + if (item->property("ssid").toString() == ssid) { + count += 1; + item->setSignalStrength(network->signalStrength()); + QObject::connect( + network, + &NMWirelessNetwork::signalStrengthChanged, + item, + &WifiNetwork::setSignalStrength + ); + mSsidToItem.insert(ssid, item); + } + } + + // Was there none? + if (count != 0) { + return; + } + + // Create an item and supply it with the network + auto* item = new WifiNetwork(this); + item->setSsid(ssid); + item->setSignalStrength(network->signalStrength()); + QObject::connect( + network, + &NMWirelessNetwork::signalStrengthChanged, + item, + &WifiNetwork::setSignalStrength + ); + item->setKnown(false); + item->setNmSecurity(network->findBestSecurity()); + mSsidToItem.insert(ssid, item); + emit this->wifiNetworkAdded(item); +}; + +void NMWirelessManager::networkRemoved(NMWirelessNetwork* network) { + QString ssid = network->ssid(); + if (!this->mNetworkMap.contains(ssid)) { + return; + } + mNetworkMap.take(ssid); + + // Remove network supplies from all supplied items + for (WifiNetwork* item: mSsidToItem.values(ssid)) { + // Was this item also supplied by a connection? + QString uuid = mUuidToItem.key(item); + if (!uuid.isEmpty()) { + // Supply only with connection + QObject::disconnect(network, nullptr, item, nullptr); + auto* conn = mConnectionMap.value(uuid); + item->setNmSecurity(securityFromConnectionSettings(conn->settings())); + } else { + // Delete item + emit this->wifiNetworkRemoved(item); + delete item; + } + }; + mSsidToItem.remove(ssid); +}; + +} // namespace qs::network diff --git a/src/network/nm/wireless.hpp b/src/network/nm/wireless.hpp new file mode 100644 index 00000000..da556fe8 --- /dev/null +++ b/src/network/nm/wireless.hpp @@ -0,0 +1,151 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../frontend.hpp" +#include "accesspoint.hpp" +#include "connection.hpp" +#include "enums.hpp" +#include "nm/dbus_nm_wireless.h" + +namespace qs::dbus { +template <> +struct DBusDataTransform { + using Wire = quint32; + using Data = qs::network::NMWirelessCapabilities::Enum; + static DBusResult fromWire(Wire wire); +}; + +} // namespace qs::dbus +namespace qs::network { + +// NMWirelessNetwork represents a wireless network, which aggregates all access points with +// the same SSID. It also provides signals and slots for a frontend WifiNetwork. +class NMWirelessNetwork: public QObject { + Q_OBJECT; + +public: + explicit NMWirelessNetwork( + NMWirelessCapabilities::Enum caps, + QByteArray ssid, + QObject* parent = nullptr + ); + void addAccessPoint(NMAccessPointAdapter* ap); + void removeAccessPoint(NMAccessPointAdapter* ap); + void updateSignalStrength(); + + [[nodiscard]] bool isEmpty() const { return this->mAccessPoints.isEmpty(); }; + [[nodiscard]] quint8 signalStrength() const { return this->bMaxSignal; }; + [[nodiscard]] QString ssid() const { return this->mSsid; }; + [[nodiscard]] NMAccessPointAdapter* referenceAccessPoint() const { return this->mReferenceAP; }; + [[nodiscard]] NMWirelessSecurityType::Enum findBestSecurity(); + +signals: + void signalStrengthChanged(quint8 signal); + void referenceAccessPointChanged(NMAccessPointAdapter* ap); + void disappeared(const QString& ssid); + +private: + NMAccessPointAdapter* mReferenceAP = nullptr; + QList mAccessPoints; + NMWirelessCapabilities::Enum mDeviceCapabilities; + QByteArray mSsid; + + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(NMWirelessNetwork, quint8, bMaxSignal, &NMWirelessNetwork::signalStrengthChanged); + // clang-format on +}; + +// NMWirelessAdapter wraps the state of a NetworkManager wireless device +// (org.freedesktop.NetworkManager.Device.Wireless), provides signals/slots that connect to a +// frontend NetworkWifiDevice, and creates/destroys NMAccessPointAdapters and NMAccessPointGroups +class NMWirelessAdapter: public QObject { + Q_OBJECT; + +public: + explicit NMWirelessAdapter(const QString& path, QObject* parent = nullptr); + + void init(); + void registerAccessPoint(const QString& path); + void registerAccessPoints(); + void addApToNetwork(NMAccessPointAdapter* ap, const QByteArray& ssid); + void removeApFromNetwork(NMAccessPointAdapter* ap); + + [[nodiscard]] bool isValid() const; + [[nodiscard]] QString path() const; + [[nodiscard]] QString address() const; + [[nodiscard]] qint64 getLastScan() { return this->bLastScan; }; + [[nodiscard]] const QDBusObjectPath& activeApPath() { return this->bActiveAccessPoint; }; + [[nodiscard]] NMWirelessCapabilities::Enum capabilities() { return this->bCapabilities; }; + +public slots: + void scan(); + +signals: + void lastScanChanged(qint64 lastScan); + void activeApChanged(const QDBusObjectPath& path); + void capabilitiesChanged(NMWirelessCapabilities::Enum caps); + + void networkAdded(NMWirelessNetwork* network); + void networkRemoved(NMWirelessNetwork* network); + +private slots: + void onAccessPointAdded(const QDBusObjectPath& path); + void onAccessPointRemoved(const QDBusObjectPath& path); + void onAccessPointReady(NMAccessPointAdapter* ap); + +private: + // Lookups: NMAccessPointAdapter <-> NMWifiNetwork + QHash mApMap; // AP Path -> NMAccessPointAdapter* + QHash mSsidMap; // AP Path -> Ssid + QHash mNetworkMap; // Ssid -> NMAccessPointGroup* + + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(NMWirelessAdapter, qint64, bLastScan, &NMWirelessAdapter::lastScanChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMWirelessAdapter, QDBusObjectPath, bActiveAccessPoint, &NMWirelessAdapter::activeApChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMWirelessAdapter, NMWirelessCapabilities::Enum, bCapabilities, &NMWirelessAdapter::capabilitiesChanged); + + QS_DBUS_BINDABLE_PROPERTY_GROUP(NMWirelessAdapter, wirelessProperties); + QS_DBUS_PROPERTY_BINDING(NMWirelessAdapter, pLastScan, bLastScan, wirelessProperties, "LastScan"); + QS_DBUS_PROPERTY_BINDING(NMWirelessAdapter, pActiveAccessPoint, bActiveAccessPoint, wirelessProperties, "ActiveAccessPoint"); + QS_DBUS_PROPERTY_BINDING(NMWirelessAdapter, pCapabilities, bCapabilities, wirelessProperties, "WirelessCapabilities"); + // clang-format on + + DBusNMWirelessProxy* proxy = nullptr; +}; + +// NMWirelessManager processes the incoming/outgoing connections (NMDeviceAdapter) and wifi +// networks (NMWirelessAdapter), and merges them into WifiItems for the frontend. +class NMWirelessManager: public QObject { + Q_OBJECT; + +public: + explicit NMWirelessManager(QObject* parent = nullptr); + +public slots: + void connectionLoaded(NMConnectionSettingsAdapter* connection); + void connectionRemoved(NMConnectionSettingsAdapter* connection); + void networkAdded(NMWirelessNetwork* nmNetwork); + void networkRemoved(NMWirelessNetwork* nmNetwork); + +signals: + void wifiNetworkAdded(WifiNetwork* network); + void wifiNetworkRemoved(WifiNetwork* network); + +private: + // Lookups to help merge 1:1 connection:item relation and 1:many network:item relation. + QHash mNetworkMap; // Ssid -> NMWirelessNetwork + QHash mConnectionMap; // Uuid -> NMConnectionAdapter + QHash mUuidToItem; // Uuid -> WifiNetwork + QMultiHash mSsidToItem; // Ssid -> WifiNetwork +}; + +} // namespace qs::network diff --git a/src/network/test/network.qml b/src/network/test/network.qml new file mode 100644 index 00000000..6f4eda87 --- /dev/null +++ b/src/network/test/network.qml @@ -0,0 +1,119 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import Quickshell.Network + +FloatingWindow { + color: contentItem.palette.window + + ColumnLayout { + anchors.fill: parent + anchors.margins: 10 + spacing: 10 + + ListView { + Layout.fillWidth: true + Layout.fillHeight: true + model: Network.devices + + delegate: WrapperRectangle { + width: parent.width + color: "transparent" + border.color: palette.button + border.width: 1 + margin: 10 + + ColumnLayout { + Label { + text: `Device ${index}: ${modelData.name}` + font.bold: true + font.pointSize: 12 + } + Label { text: "Hardware Address: " + modelData.address } + Label { text: "Device type: " + NetworkDeviceType.toString(modelData.type) } + + RowLayout { + Label { text: "State: " + NetworkDeviceState.toString(modelData.state) } + Button { + text: "Disconnect" + onClicked: modelData.disconnect() + visible: modelData.state === NetworkDeviceState.Connected + } + } + ColumnLayout { + RowLayout { + Button { + text: "Scan" + onClicked: modelData.scan() + visible: modelData.scanning === false; + } + } + Label { text: "Available networks: " } + GridLayout { + columns: 4 + columnSpacing: 30 + rowSpacing: 5 + + Label { + Layout.row: 0 + Layout.column: 0 + text: "SSID" + } + Label { + Layout.row: 0 + Layout.column: 1 + text: "SECURITY" + } + Label { + Layout.row: 0 + Layout.column: 2 + text: "KNOWN" + } + Label { + Layout.row: 0 + Layout.column: 3 + text: "SIGNAL STRENGTH" + } + + Repeater { + model: modelData.networks + delegate: Text { + Layout.row: index + 1 + Layout.column: 0 + text: modelData.ssid || "[Hidden]" + } + } + Repeater { + model: modelData.networks + delegate: Text { + Layout.row: index + 1 + Layout.column: 1 + text: NMWirelessSecurityType.toString(modelData.nmSecurity) + } + } + Repeater { + model: modelData.networks + delegate: Text { + Layout.row: index + 1 + Layout.column: 2 + text: modelData.known ? "*" : "" + } + } + Repeater { + model: modelData.networks + delegate: Text { + Layout.row: index + 1 + Layout.column: 3 + text: modelData.signalStrength + "%" + } + } + } + visible: modelData.type === NetworkDeviceType.Wireless + } + } + } + } + } +}