networking: add networking library

This commit is contained in:
Carson Powers 2025-07-03 13:06:21 -05:00 committed by outfoxxed
parent bcc3d4265e
commit db37dc580a
No known key found for this signature in database
GPG key ID: 4C88A185FB89301E
34 changed files with 3177 additions and 1 deletions

View file

@ -77,6 +77,7 @@ boption(SERVICE_GREETD "Greetd" ON)
boption(SERVICE_UPOWER "UPower" ON) boption(SERVICE_UPOWER "UPower" ON)
boption(SERVICE_NOTIFICATIONS "Notifications" ON) boption(SERVICE_NOTIFICATIONS "Notifications" ON)
boption(BLUETOOTH "Bluetooth" ON) boption(BLUETOOTH "Bluetooth" ON)
boption(NETWORK "Network" ON)
include(cmake/install-qml-module.cmake) include(cmake/install-qml-module.cmake)
include(cmake/util.cmake) include(cmake/util.cmake)
@ -125,7 +126,7 @@ if (WAYLAND)
list(APPEND QT_PRIVDEPS WaylandClientPrivate) list(APPEND QT_PRIVDEPS WaylandClientPrivate)
endif() 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) set(DBUS ON)
endif() endif()

View file

@ -20,6 +20,7 @@ set shell id.
- Added the ability to handle move and resize events to FloatingWindow. - Added the ability to handle move and resize events to FloatingWindow.
- Pipewire service now reconnects if pipewire dies or a protocol error occurs. - Pipewire service now reconnects if pipewire dies or a protocol error occurs.
- Added pipewire audio peak detection. - Added pipewire audio peak detection.
- Added initial support for network management.
## Other Changes ## Other Changes

View file

@ -46,6 +46,7 @@
withHyprland ? true, withHyprland ? true,
withI3 ? true, withI3 ? true,
withPolkit ? true, withPolkit ? true,
withNetworkManager ? true,
}: let }: let
unwrapped = stdenv.mkDerivation { unwrapped = stdenv.mkDerivation {
pname = "quickshell${lib.optionalString debug "-debug"}"; pname = "quickshell${lib.optionalString debug "-debug"}";
@ -95,6 +96,7 @@
(lib.cmakeBool "SCREENCOPY" (libgbm != null)) (lib.cmakeBool "SCREENCOPY" (libgbm != null))
(lib.cmakeBool "SERVICE_PIPEWIRE" withPipewire) (lib.cmakeBool "SERVICE_PIPEWIRE" withPipewire)
(lib.cmakeBool "SERVICE_PAM" withPam) (lib.cmakeBool "SERVICE_PAM" withPam)
(lib.cmakeBool "SERVICE_NETWORKMANAGER" withNetworkManager)
(lib.cmakeBool "SERVICE_POLKIT" withPolkit) (lib.cmakeBool "SERVICE_POLKIT" withPolkit)
(lib.cmakeBool "HYPRLAND" withHyprland) (lib.cmakeBool "HYPRLAND" withHyprland)
(lib.cmakeBool "I3" withI3) (lib.cmakeBool "I3" withI3)

View file

@ -33,3 +33,7 @@ add_subdirectory(services)
if (BLUETOOTH) if (BLUETOOTH)
add_subdirectory(bluetooth) add_subdirectory(bluetooth)
endif() endif()
if (NETWORK)
add_subdirectory(network)
endif()

View file

@ -0,0 +1,24 @@
add_subdirectory(nm)
qt_add_library(quickshell-network STATIC
network.cpp
device.cpp
wifi.cpp
)
target_include_directories(quickshell-network PRIVATE
${CMAKE_CURRENT_BINARY_DIR}
)
qt_add_qml_module(quickshell-network
URI Quickshell.Networking
VERSION 0.1
DEPENDENCIES QtQml
)
qs_add_module_deps_light(quickshell-network Quickshell)
install_qml_module(quickshell-network)
target_link_libraries(quickshell-network PRIVATE quickshell-network-nm 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)

82
src/network/device.cpp Normal file
View file

@ -0,0 +1,82 @@
#include "device.hpp"
#include <qdebug.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qobject.h>
#include <qstringliteral.h>
#include <qtmetamacros.h>
#include "../core/logcat.hpp"
namespace qs::network {
namespace {
QS_LOGGING_CATEGORY(logNetworkDevice, "quickshell.network.device", QtWarningMsg);
} // namespace
QString DeviceConnectionState::toString(DeviceConnectionState::Enum state) {
switch (state) {
case Unknown: return QStringLiteral("Unknown");
case Connecting: return QStringLiteral("Connecting");
case Connected: return QStringLiteral("Connected");
case Disconnecting: return QStringLiteral("Disconnecting");
case Disconnected: return QStringLiteral("Disconnected");
default: return QStringLiteral("Unknown");
}
}
QString DeviceType::toString(DeviceType::Enum type) {
switch (type) {
case None: return QStringLiteral("None");
case Wifi: return QStringLiteral("Wifi");
default: return QStringLiteral("Unknown");
}
}
QString NMDeviceState::toString(NMDeviceState::Enum state) {
switch (state) {
case Unknown: return QStringLiteral("Unknown");
case Unmanaged: return QStringLiteral("Not managed by NetworkManager");
case Unavailable: return QStringLiteral("Unavailable");
case Disconnected: return QStringLiteral("Disconnected");
case Prepare: return QStringLiteral("Preparing to connect");
case Config: return QStringLiteral("Connecting to a network");
case NeedAuth: return QStringLiteral("Waiting for authentication");
case IPConfig: return QStringLiteral("Requesting IPv4 and/or IPv6 addresses from the network");
case IPCheck:
return QStringLiteral("Checking if further action is required for the requested connection");
case Secondaries:
return QStringLiteral("Waiting for a required secondary connection to activate");
case Activated: return QStringLiteral("Connected");
case Deactivating: return QStringLiteral("Disconnecting");
case Failed: return QStringLiteral("Failed to connect");
default: return QStringLiteral("Unknown");
};
}
NetworkDevice::NetworkDevice(DeviceType::Enum type, QObject* parent): QObject(parent), mType(type) {
this->bindableConnected().setBinding([this]() {
return this->bState == DeviceConnectionState::Connected;
});
};
void NetworkDevice::setAutoconnect(bool autoconnect) {
if (this->bAutoconnect == autoconnect) return;
emit this->requestSetAutoconnect(autoconnect);
}
void NetworkDevice::disconnect() {
if (this->bState == DeviceConnectionState::Disconnected) {
qCCritical(logNetworkDevice) << "Device" << this << "is already disconnected";
return;
}
if (this->bState == DeviceConnectionState::Disconnecting) {
qCCritical(logNetworkDevice) << "Device" << this << "is already disconnecting";
return;
}
qCDebug(logNetworkDevice) << "Disconnecting from device" << this;
this->requestDisconnect();
}
} // namespace qs::network

133
src/network/device.hpp Normal file
View file

@ -0,0 +1,133 @@
#pragma once
#include <qobject.h>
#include <qproperty.h>
#include <qqmlintegration.h>
#include <qtmetamacros.h>
#include <qtypes.h>
namespace qs::network {
///! Connection state of a NetworkDevice.
class DeviceConnectionState: public QObject {
Q_OBJECT;
QML_ELEMENT;
QML_SINGLETON;
public:
enum Enum : quint8 {
Unknown = 0,
Connecting = 1,
Connected = 2,
Disconnecting = 3,
Disconnected = 4,
};
Q_ENUM(Enum);
Q_INVOKABLE static QString toString(DeviceConnectionState::Enum state);
};
///! Type of network device.
class DeviceType: public QObject {
Q_OBJECT;
QML_ELEMENT;
QML_SINGLETON;
public:
enum Enum : quint8 {
None = 0,
Wifi = 1,
};
Q_ENUM(Enum);
Q_INVOKABLE static QString toString(DeviceType::Enum type);
};
///! NetworkManager-specific device state.
/// In sync with https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMDeviceState.
class NMDeviceState: public QObject {
Q_OBJECT;
QML_ELEMENT;
QML_SINGLETON;
public:
enum Enum : quint8 {
Unknown = 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);
Q_INVOKABLE static QString toString(NMDeviceState::Enum state);
};
///! A network device.
/// When @@type is `Wifi`, the device is a @@WifiDevice, which can be used to scan for and connect to access points.
class NetworkDevice: public QObject {
Q_OBJECT;
QML_ELEMENT;
QML_UNCREATABLE("Devices can only be acquired through Network");
// clang-format off
/// The device type.
Q_PROPERTY(DeviceType::Enum type READ type CONSTANT);
/// The name of the device's control interface.
Q_PROPERTY(QString name READ name NOTIFY nameChanged BINDABLE bindableName);
/// The hardware address of the device in the XX:XX:XX:XX:XX:XX format.
Q_PROPERTY(QString address READ default NOTIFY addressChanged BINDABLE bindableAddress);
/// True if the device is connected.
Q_PROPERTY(bool connected READ default NOTIFY connectedChanged BINDABLE bindableConnected);
/// Connection state of the device.
Q_PROPERTY(qs::network::DeviceConnectionState::Enum state READ default NOTIFY stateChanged BINDABLE bindableState);
/// A more specific device state when the backend is NetworkManager.
Q_PROPERTY(qs::network::NMDeviceState::Enum nmState READ default NOTIFY nmStateChanged BINDABLE bindableNmState);
/// True if the device is allowed to autoconnect.
Q_PROPERTY(bool autoconnect READ autoconnect WRITE setAutoconnect NOTIFY autoconnectChanged);
// clang-format on
public:
explicit NetworkDevice(DeviceType::Enum type, QObject* parent = nullptr);
/// Disconnects the device and prevents it from automatically activating further connections.
Q_INVOKABLE void disconnect();
[[nodiscard]] DeviceType::Enum type() const { return this->mType; };
QBindable<QString> bindableName() { return &this->bName; };
[[nodiscard]] QString name() const { return this->bName; };
QBindable<QString> bindableAddress() { return &this->bAddress; };
QBindable<bool> bindableConnected() { return &this->bConnected; };
QBindable<DeviceConnectionState::Enum> bindableState() { return &this->bState; };
QBindable<NMDeviceState::Enum> bindableNmState() { return &this->bNmState; };
[[nodiscard]] bool autoconnect() const { return this->bAutoconnect; };
QBindable<bool> bindableAutoconnect() { return &this->bAutoconnect; };
void setAutoconnect(bool autoconnect);
signals:
void requestDisconnect();
void requestSetAutoconnect(bool autoconnect);
void nameChanged();
void addressChanged();
void connectedChanged();
void stateChanged();
void nmStateChanged();
void autoconnectChanged();
private:
DeviceType::Enum mType;
// 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, bool, bConnected, &NetworkDevice::connectedChanged);
Q_OBJECT_BINDABLE_PROPERTY(NetworkDevice, DeviceConnectionState::Enum, bState, &NetworkDevice::stateChanged);
Q_OBJECT_BINDABLE_PROPERTY(NetworkDevice, NMDeviceState::Enum, bNmState, &NetworkDevice::nmStateChanged);
Q_OBJECT_BINDABLE_PROPERTY(NetworkDevice, bool, bAutoconnect, &NetworkDevice::autoconnectChanged);
// clang-format on
};
} // namespace qs::network

13
src/network/module.md Normal file
View file

@ -0,0 +1,13 @@
name = "Quickshell.Networking"
description = "Network API"
headers = [
"network.hpp",
"device.hpp",
"wifi.hpp",
]
-----
This module exposes Network management APIs provided by a supported network backend.
For now, the only backend available is the NetworkManager DBus interface.
Both DBus and NetworkManager must be running to use it.
See the @@Quickshell.Networking.Networking singleton.

65
src/network/network.cpp Normal file
View file

@ -0,0 +1,65 @@
#include "network.hpp"
#include <utility>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qobject.h>
#include <qstringliteral.h>
#include <qtmetamacros.h>
#include "../core/logcat.hpp"
#include "device.hpp"
#include "nm/backend.hpp"
namespace qs::network {
namespace {
QS_LOGGING_CATEGORY(logNetwork, "quickshell.network", QtWarningMsg);
} // namespace
QString NetworkState::toString(NetworkState::Enum state) {
switch (state) {
case NetworkState::Connecting: return QStringLiteral("Connecting");
case NetworkState::Connected: return QStringLiteral("Connected");
case NetworkState::Disconnecting: return QStringLiteral("Disconnecting");
case NetworkState::Disconnected: return QStringLiteral("Disconnected");
default: return QStringLiteral("Unknown");
}
}
Networking::Networking(QObject* parent): QObject(parent) {
// Try to create the NetworkManager backend and bind to it.
auto* nm = new NetworkManager(this);
if (nm->isAvailable()) {
QObject::connect(nm, &NetworkManager::deviceAdded, this, &Networking::deviceAdded);
QObject::connect(nm, &NetworkManager::deviceRemoved, this, &Networking::deviceRemoved);
QObject::connect(this, &Networking::requestSetWifiEnabled, nm, &NetworkManager::setWifiEnabled);
this->bindableWifiEnabled().setBinding([nm]() { return nm->wifiEnabled(); });
this->bindableWifiHardwareEnabled().setBinding([nm]() { return nm->wifiHardwareEnabled(); });
this->mBackend = nm;
this->mBackendType = NetworkBackendType::NetworkManager;
return;
} else {
delete nm;
}
qCCritical(logNetwork) << "Network will not work. Could not find an available backend.";
}
void Networking::deviceAdded(NetworkDevice* dev) { this->mDevices.insertObject(dev); }
void Networking::deviceRemoved(NetworkDevice* dev) { this->mDevices.removeObject(dev); }
void Networking::setWifiEnabled(bool enabled) {
if (this->bWifiEnabled == enabled) return;
emit this->requestSetWifiEnabled(enabled);
}
Network::Network(QString name, QObject* parent): QObject(parent), mName(std::move(name)) {
this->bStateChanging.setBinding([this] {
auto state = this->bState.value();
return state == NetworkState::Connecting || state == NetworkState::Disconnecting;
});
};
} // namespace qs::network

142
src/network/network.hpp Normal file
View file

@ -0,0 +1,142 @@
#pragma once
#include <qobject.h>
#include <qproperty.h>
#include <qqmlintegration.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include "../core/model.hpp"
#include "device.hpp"
namespace qs::network {
///! The connection state of a Network.
class NetworkState: public QObject {
Q_OBJECT;
QML_ELEMENT;
QML_SINGLETON;
public:
enum Enum : quint8 {
Unknown = 0,
Connecting = 1,
Connected = 2,
Disconnecting = 3,
Disconnected = 4,
};
Q_ENUM(Enum);
Q_INVOKABLE static QString toString(NetworkState::Enum state);
};
///! The backend supplying the Network service.
class NetworkBackendType: public QObject {
Q_OBJECT;
QML_ELEMENT;
QML_SINGLETON;
public:
enum Enum : quint8 {
None = 0,
NetworkManager = 1,
};
Q_ENUM(Enum);
};
class NetworkBackend: public QObject {
Q_OBJECT;
public:
[[nodiscard]] virtual bool isAvailable() const = 0;
protected:
explicit NetworkBackend(QObject* parent = nullptr): QObject(parent) {};
};
///! The Network service.
/// An interface to a network backend (currently only NetworkManager),
/// which can be used to view, configure, and connect to various networks.
class Networking: public QObject {
Q_OBJECT;
QML_SINGLETON;
QML_ELEMENT;
// clang-format off
/// A list of all network devices.
QSDOC_TYPE_OVERRIDE(ObjectModel<qs::network::NetworkDevice>*);
Q_PROPERTY(UntypedObjectModel* devices READ devices CONSTANT);
/// The backend being used to power the Network service.
Q_PROPERTY(qs::network::NetworkBackendType::Enum backend READ backend CONSTANT);
/// Switch for the rfkill software block of all wireless devices.
Q_PROPERTY(bool wifiEnabled READ wifiEnabled WRITE setWifiEnabled NOTIFY wifiEnabledChanged);
/// State of the rfkill hardware block of all wireless devices.
Q_PROPERTY(bool wifiHardwareEnabled READ default NOTIFY wifiHardwareEnabledChanged BINDABLE bindableWifiHardwareEnabled);
// clang-format on
public:
explicit Networking(QObject* parent = nullptr);
[[nodiscard]] ObjectModel<NetworkDevice>* devices() { return &this->mDevices; };
[[nodiscard]] NetworkBackendType::Enum backend() const { return this->mBackendType; };
QBindable<bool> bindableWifiEnabled() { return &this->bWifiEnabled; };
[[nodiscard]] bool wifiEnabled() const { return this->bWifiEnabled; };
void setWifiEnabled(bool enabled);
QBindable<bool> bindableWifiHardwareEnabled() { return &this->bWifiHardwareEnabled; };
signals:
void requestSetWifiEnabled(bool enabled);
void wifiEnabledChanged();
void wifiHardwareEnabledChanged();
private slots:
void deviceAdded(NetworkDevice* dev);
void deviceRemoved(NetworkDevice* dev);
private:
ObjectModel<NetworkDevice> mDevices {this};
NetworkBackend* mBackend = nullptr;
NetworkBackendType::Enum mBackendType = NetworkBackendType::None;
// clang-format off
Q_OBJECT_BINDABLE_PROPERTY(Networking, bool, bWifiEnabled, &Networking::wifiEnabledChanged);
Q_OBJECT_BINDABLE_PROPERTY(Networking, bool, bWifiHardwareEnabled, &Networking::wifiHardwareEnabledChanged);
// clang-format on
};
///! A network.
class Network: public QObject {
Q_OBJECT;
QML_ELEMENT;
QML_UNCREATABLE("BaseNetwork can only be aqcuired through network devices");
// clang-format off
/// The name of the network.
Q_PROPERTY(QString name READ name CONSTANT);
/// True if the network is connected.
Q_PROPERTY(bool connected READ default NOTIFY connectedChanged BINDABLE bindableConnected);
/// The connectivity state of the network.
Q_PROPERTY(NetworkState::Enum state READ default NOTIFY stateChanged BINDABLE bindableState);
/// If the network is currently connecting or disconnecting. Shorthand for checking @@state.
Q_PROPERTY(bool stateChanging READ default NOTIFY stateChangingChanged BINDABLE bindableStateChanging);
// clang-format on
public:
explicit Network(QString name, QObject* parent = nullptr);
[[nodiscard]] QString name() const { return this->mName; };
QBindable<bool> bindableConnected() { return &this->bConnected; }
QBindable<NetworkState::Enum> bindableState() { return &this->bState; }
QBindable<bool> bindableStateChanging() { return &this->bStateChanging; }
signals:
void connectedChanged();
void stateChanged();
void stateChangingChanged();
protected:
QString mName;
Q_OBJECT_BINDABLE_PROPERTY(Network, bool, bConnected, &Network::connectedChanged);
Q_OBJECT_BINDABLE_PROPERTY(Network, NetworkState::Enum, bState, &Network::stateChanged);
Q_OBJECT_BINDABLE_PROPERTY(Network, bool, bStateChanging, &Network::stateChangingChanged);
};
} // namespace qs::network

View file

@ -0,0 +1,79 @@
set_source_files_properties(org.freedesktop.NetworkManager.xml PROPERTIES
CLASSNAME DBusNetworkManagerProxy
NO_NAMESPACE TRUE
INCLUDE ${CMAKE_CURRENT_SOURCE_DIR}/dbus_types.hpp
)
qt_add_dbus_interface(NM_DBUS_INTERFACES
org.freedesktop.NetworkManager.xml
dbus_nm_backend
)
set_source_files_properties(org.freedesktop.NetworkManager.Device.xml PROPERTIES
CLASSNAME DBusNMDeviceProxy
NO_NAMESPACE TRUE
)
qt_add_dbus_interface(NM_DBUS_INTERFACES
org.freedesktop.NetworkManager.Device.xml
dbus_nm_device
)
set_source_files_properties(org.freedesktop.NetworkManager.Device.Wireless.xml PROPERTIES
CLASSNAME DBusNMWirelessProxy
NO_NAMESPACE TRUE
)
qt_add_dbus_interface(NM_DBUS_INTERFACES
org.freedesktop.NetworkManager.Device.Wireless.xml
dbus_nm_wireless
)
set_source_files_properties(org.freedesktop.NetworkManager.AccessPoint.xml PROPERTIES
CLASSNAME DBusNMAccessPointProxy
NO_NAMESPACE TRUE
)
qt_add_dbus_interface(NM_DBUS_INTERFACES
org.freedesktop.NetworkManager.AccessPoint.xml
dbus_nm_accesspoint
)
set_source_files_properties(org.freedesktop.NetworkManager.Settings.Connection.xml PROPERTIES
CLASSNAME DBusNMConnectionSettingsProxy
NO_NAMESPACE TRUE
INCLUDE ${CMAKE_CURRENT_SOURCE_DIR}/dbus_types.hpp
)
qt_add_dbus_interface(NM_DBUS_INTERFACES
org.freedesktop.NetworkManager.Settings.Connection.xml
dbus_nm_connection_settings
)
set_source_files_properties(org.freedesktop.NetworkManager.Connection.Active.xml PROPERTIES
CLASSNAME DBusNMActiveConnectionProxy
NO_NAMESPACE TRUE
)
qt_add_dbus_interface(NM_DBUS_INTERFACES
org.freedesktop.NetworkManager.Connection.Active.xml
dbus_nm_active_connection
)
qt_add_library(quickshell-network-nm STATIC
backend.cpp
device.cpp
connection.cpp
accesspoint.cpp
wireless.cpp
utils.cpp
enums.hpp
${NM_DBUS_INTERFACES}
)
target_include_directories(quickshell-network-nm PUBLIC
${CMAKE_CURRENT_BINARY_DIR}
)
target_link_libraries(quickshell-network-nm PRIVATE Qt::Qml Qt::DBus)
qs_add_link_dependencies(quickshell-network-nm quickshell-dbus)

View file

@ -0,0 +1,71 @@
#include "accesspoint.hpp"
#include <qdbusconnection.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qnamespace.h>
#include <qobject.h>
#include <qstring.h>
#include <qtypes.h>
#include "../../core/logcat.hpp"
#include "../../dbus/properties.hpp"
#include "dbus_nm_accesspoint.h"
#include "enums.hpp"
namespace qs::network {
using namespace qs::dbus;
namespace {
QS_LOGGING_CATEGORY(logNetworkManager, "quickshell.network.networkmanager", QtWarningMsg);
}
NMAccessPoint::NMAccessPoint(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 DBus interface for access point at" << path;
return;
}
QObject::connect(
&this->accessPointProperties,
&DBusPropertyGroup::getAllFinished,
this,
&NMAccessPoint::loaded,
Qt::SingleShotConnection
);
this->accessPointProperties.setInterface(this->proxy);
this->accessPointProperties.updateAllViaGetAll();
}
bool NMAccessPoint::isValid() const { return this->proxy && this->proxy->isValid(); }
QString NMAccessPoint::address() const { return this->proxy ? this->proxy->service() : QString(); }
QString NMAccessPoint::path() const { return this->proxy ? this->proxy->path() : QString(); }
} // namespace qs::network
namespace qs::dbus {
DBusResult<qs::network::NM80211ApFlags::Enum>
DBusDataTransform<qs::network::NM80211ApFlags::Enum>::fromWire(quint32 wire) {
return DBusResult(static_cast<qs::network::NM80211ApFlags::Enum>(wire));
}
DBusResult<qs::network::NM80211ApSecurityFlags::Enum>
DBusDataTransform<qs::network::NM80211ApSecurityFlags::Enum>::fromWire(quint32 wire) {
return DBusResult(static_cast<qs::network::NM80211ApSecurityFlags::Enum>(wire));
}
DBusResult<qs::network::NM80211Mode::Enum>
DBusDataTransform<qs::network::NM80211Mode::Enum>::fromWire(quint32 wire) {
return DBusResult(static_cast<qs::network::NM80211Mode::Enum>(wire));
}
} // namespace qs::dbus

View file

@ -0,0 +1,92 @@
#pragma once
#include <qdbusextratypes.h>
#include <qobject.h>
#include <qproperty.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include "../../dbus/properties.hpp"
#include "../wifi.hpp"
#include "dbus_nm_accesspoint.h"
#include "enums.hpp"
namespace qs::dbus {
template <>
struct DBusDataTransform<qs::network::NM80211ApFlags::Enum> {
using Wire = quint32;
using Data = qs::network::NM80211ApFlags::Enum;
static DBusResult<Data> fromWire(Wire wire);
};
template <>
struct DBusDataTransform<qs::network::NM80211ApSecurityFlags::Enum> {
using Wire = quint32;
using Data = qs::network::NM80211ApSecurityFlags::Enum;
static DBusResult<Data> fromWire(Wire wire);
};
template <>
struct DBusDataTransform<qs::network::NM80211Mode::Enum> {
using Wire = quint32;
using Data = qs::network::NM80211Mode::Enum;
static DBusResult<Data> fromWire(Wire wire);
};
} // namespace qs::dbus
namespace qs::network {
/// Proxy of a /org/freedesktop/NetworkManager/AccessPoint/* object.
class NMAccessPoint: public QObject {
Q_OBJECT;
public:
explicit NMAccessPoint(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; };
[[nodiscard]] QBindable<WifiSecurityType::Enum> bindableSecurity() { return &this->bSecurity; };
[[nodiscard]] WifiSecurityType::Enum security() const { return this->bSecurity; };
signals:
void loaded();
void ssidChanged(const QByteArray& ssid);
void signalStrengthChanged(quint8 signal);
void flagsChanged(NM80211ApFlags::Enum flags);
void wpaFlagsChanged(NM80211ApSecurityFlags::Enum wpaFlags);
void rsnFlagsChanged(NM80211ApSecurityFlags::Enum rsnFlags);
void modeChanged(NM80211Mode::Enum mode);
void securityChanged(WifiSecurityType::Enum security);
private:
// clang-format off
Q_OBJECT_BINDABLE_PROPERTY(NMAccessPoint, QByteArray, bSsid, &NMAccessPoint::ssidChanged);
Q_OBJECT_BINDABLE_PROPERTY(NMAccessPoint, quint8, bSignalStrength, &NMAccessPoint::signalStrengthChanged);
Q_OBJECT_BINDABLE_PROPERTY(NMAccessPoint, NM80211ApFlags::Enum, bFlags, &NMAccessPoint::flagsChanged);
Q_OBJECT_BINDABLE_PROPERTY(NMAccessPoint, NM80211ApSecurityFlags::Enum, bWpaFlags, &NMAccessPoint::wpaFlagsChanged);
Q_OBJECT_BINDABLE_PROPERTY(NMAccessPoint, NM80211ApSecurityFlags::Enum, bRsnFlags, &NMAccessPoint::rsnFlagsChanged);
Q_OBJECT_BINDABLE_PROPERTY(NMAccessPoint, NM80211Mode::Enum, bMode, &NMAccessPoint::modeChanged);
Q_OBJECT_BINDABLE_PROPERTY(NMAccessPoint, WifiSecurityType::Enum, bSecurity, &NMAccessPoint::securityChanged);
QS_DBUS_BINDABLE_PROPERTY_GROUP(NMAccessPointAdapter, accessPointProperties);
QS_DBUS_PROPERTY_BINDING(NMAccessPoint, pSsid, bSsid, accessPointProperties, "Ssid");
QS_DBUS_PROPERTY_BINDING(NMAccessPoint, pSignalStrength, bSignalStrength, accessPointProperties, "Strength");
QS_DBUS_PROPERTY_BINDING(NMAccessPoint, pFlags, bFlags, accessPointProperties, "Flags");
QS_DBUS_PROPERTY_BINDING(NMAccessPoint, pWpaFlags, bWpaFlags, accessPointProperties, "WpaFlags");
QS_DBUS_PROPERTY_BINDING(NMAccessPoint, pRsnFlags, bRsnFlags, accessPointProperties, "RsnFlags");
QS_DBUS_PROPERTY_BINDING(NMAccessPoint, pMode, bMode, accessPointProperties, "Mode");
// clang-format on
DBusNMAccessPointProxy* proxy = nullptr;
};
} // namespace qs::network

270
src/network/nm/backend.cpp Normal file
View file

@ -0,0 +1,270 @@
#include "backend.hpp"
#include <qdbusconnection.h>
#include <qdbusextratypes.h>
#include <qdbusmetatype.h>
#include <qdbuspendingcall.h>
#include <qdbuspendingreply.h>
#include <qlist.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qobject.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include "../../core/logcat.hpp"
#include "../../dbus/properties.hpp"
#include "../device.hpp"
#include "../network.hpp"
#include "../wifi.hpp"
#include "dbus_nm_backend.h"
#include "dbus_nm_device.h"
#include "dbus_types.hpp"
#include "device.hpp"
#include "enums.hpp"
#include "wireless.hpp"
namespace qs::network {
namespace {
QS_LOGGING_CATEGORY(logNetworkManager, "quickshell.network.networkmanager", QtWarningMsg);
}
NetworkManager::NetworkManager(QObject* parent): NetworkBackend(parent) {
qDBusRegisterMetaType<ConnectionSettingsMap>();
auto bus = QDBusConnection::systemBus();
if (!bus.isConnected()) {
qCWarning(
logNetworkManager
) << "Could not connect to DBus. NetworkManager backend will not work.";
return;
}
this->proxy = new DBusNetworkManagerProxy(
"org.freedesktop.NetworkManager",
"/org/freedesktop/NetworkManager",
bus,
this
);
if (!this->proxy->isValid()) {
qCDebug(
logNetworkManager
) << "NetworkManager 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::onDevicePathAdded);
QObject::connect(this->proxy, &DBusNetworkManagerProxy::DeviceRemoved, this, &NetworkManager::onDevicePathRemoved);
// 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<QList<QDBusObjectPath>> reply = *call;
if (reply.isError()) {
qCWarning(logNetworkManager) << "Failed to get devices: " << reply.error().message();
} else {
for (const QDBusObjectPath& devicePath: reply.value()) {
this->registerDevice(devicePath.path());
}
}
delete call;
};
QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback);
}
void NetworkManager::registerDevice(const QString& path) {
if (this->mDevices.contains(path)) {
qCDebug(logNetworkManager) << "Skipping duplicate registration of device" << path;
return;
}
auto* temp = new DBusNMDeviceProxy(
"org.freedesktop.NetworkManager",
path,
QDBusConnection::systemBus(),
this
);
auto callback = [this, path, temp](uint value, const QDBusError& error) {
if (error.isValid()) {
qCWarning(logNetworkManager) << "Failed to get device type:" << error;
} else {
auto type = static_cast<qs::network::NMDeviceType::Enum>(value);
NMDevice* dev = nullptr;
this->mDevices.insert(path, nullptr);
switch (type) {
case NMDeviceType::Wifi: dev = new NMWirelessDevice(path); break;
default: break;
}
if (dev) {
if (!dev->isValid()) {
qCWarning(logNetworkManager) << "Ignoring invalid registration of" << path;
delete dev;
} else {
this->mDevices[path] = dev;
// Only register a frontend device while it's managed by NM.
auto onManagedChanged = [this, dev, type](bool managed) {
managed ? this->registerFrontendDevice(type, dev) : this->removeFrontendDevice(dev);
};
// clang-format off
QObject::connect(dev, &NMDevice::addAndActivateConnection, this, &NetworkManager::addAndActivateConnection);
QObject::connect(dev, &NMDevice::activateConnection, this, &NetworkManager::activateConnection);
QObject::connect(dev, &NMDevice::managedChanged, this, onManagedChanged);
// clang-format on
if (dev->managed()) this->registerFrontendDevice(type, dev);
}
}
temp->deleteLater();
}
};
qs::dbus::asyncReadProperty<uint>(*temp, "DeviceType", callback);
}
void NetworkManager::registerFrontendDevice(NMDeviceType::Enum type, NMDevice* dev) {
NetworkDevice* frontendDev = nullptr;
switch (type) {
case NMDeviceType::Wifi: {
auto* frontendWifiDev = new WifiDevice(dev);
auto* wifiDev = qobject_cast<NMWirelessDevice*>(dev);
// Bind WifiDevice-specific properties
auto translateMode = [wifiDev]() {
switch (wifiDev->mode()) {
case NM80211Mode::Unknown: return WifiDeviceMode::Unknown;
case NM80211Mode::Adhoc: return WifiDeviceMode::AdHoc;
case NM80211Mode::Infra: return WifiDeviceMode::Station;
case NM80211Mode::Ap: return WifiDeviceMode::AccessPoint;
case NM80211Mode::Mesh: return WifiDeviceMode::Mesh;
}
};
// clang-format off
frontendWifiDev->bindableMode().setBinding(translateMode);
wifiDev->bindableScanning().setBinding([frontendWifiDev]() { return frontendWifiDev->scannerEnabled(); });
QObject::connect(wifiDev, &NMWirelessDevice::networkAdded, frontendWifiDev, &WifiDevice::networkAdded);
QObject::connect(wifiDev, &NMWirelessDevice::networkRemoved, frontendWifiDev, &WifiDevice::networkRemoved);
// clang-format on
frontendDev = frontendWifiDev;
break;
}
default: return;
}
// Bind generic NetworkDevice properties
auto translateState = [dev]() {
switch (dev->state()) {
case 0 ... 20: return DeviceConnectionState::Unknown;
case 30: return DeviceConnectionState::Disconnected;
case 40 ... 90: return DeviceConnectionState::Connecting;
case 100: return DeviceConnectionState::Connected;
case 110 ... 120: return DeviceConnectionState::Disconnecting;
}
};
// clang-format off
frontendDev->bindableName().setBinding([dev]() { return dev->interface(); });
frontendDev->bindableAddress().setBinding([dev]() { return dev->hwAddress(); });
frontendDev->bindableNmState().setBinding([dev]() { return dev->state(); });
frontendDev->bindableState().setBinding(translateState);
frontendDev->bindableAutoconnect().setBinding([dev]() { return dev->autoconnect(); });
QObject::connect(frontendDev, &WifiDevice::requestDisconnect, dev, &NMDevice::disconnect);
QObject::connect(frontendDev, &NetworkDevice::requestSetAutoconnect, dev, &NMDevice::setAutoconnect);
// clang-format on
this->mFrontendDevices.insert(dev->path(), frontendDev);
emit this->deviceAdded(frontendDev);
}
void NetworkManager::removeFrontendDevice(NMDevice* dev) {
auto* frontendDev = this->mFrontendDevices.take(dev->path());
if (frontendDev) {
emit this->deviceRemoved(frontendDev);
frontendDev->deleteLater();
}
}
void NetworkManager::onDevicePathAdded(const QDBusObjectPath& path) {
this->registerDevice(path.path());
}
void NetworkManager::onDevicePathRemoved(const QDBusObjectPath& path) {
auto iter = this->mDevices.find(path.path());
if (iter == this->mDevices.end()) {
qCWarning(logNetworkManager) << "Sent removal signal for" << path.path()
<< "which is not registered.";
} else {
auto* dev = iter.value();
this->mDevices.erase(iter);
if (dev) {
this->removeFrontendDevice(dev);
delete dev;
}
}
}
void NetworkManager::activateConnection(
const QDBusObjectPath& connPath,
const QDBusObjectPath& devPath
) {
auto pending = this->proxy->ActivateConnection(connPath, devPath, QDBusObjectPath("/"));
auto* call = new QDBusPendingCallWatcher(pending, this);
auto responseCallback = [](QDBusPendingCallWatcher* call) {
const QDBusPendingReply<QDBusObjectPath> reply = *call;
if (reply.isError()) {
qCWarning(logNetworkManager) << "Failed to activate connection:" << reply.error().message();
}
delete call;
};
QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback);
}
void NetworkManager::addAndActivateConnection(
const ConnectionSettingsMap& settings,
const QDBusObjectPath& devPath,
const QDBusObjectPath& specificObjectPath
) {
auto pending = this->proxy->AddAndActivateConnection(settings, devPath, specificObjectPath);
auto* call = new QDBusPendingCallWatcher(pending, this);
auto responseCallback = [](QDBusPendingCallWatcher* call) {
const QDBusPendingReply<QDBusObjectPath, QDBusObjectPath> reply = *call;
if (reply.isError()) {
qCWarning(logNetworkManager)
<< "Failed to add and activate connection:" << reply.error().message();
}
delete call;
};
QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback);
}
void NetworkManager::setWifiEnabled(bool enabled) {
if (enabled == this->bWifiEnabled) return;
this->bWifiEnabled = enabled;
this->pWifiEnabled.write();
}
bool NetworkManager::isAvailable() const { return this->proxy && this->proxy->isValid(); };
} // namespace qs::network

View file

@ -0,0 +1,67 @@
#pragma once
#include <qdbusextratypes.h>
#include <qhash.h>
#include <qobject.h>
#include <qproperty.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include "../../dbus/properties.hpp"
#include "../network.hpp"
#include "dbus_nm_backend.h"
#include "device.hpp"
namespace qs::network {
class NetworkManager: public NetworkBackend {
Q_OBJECT;
public:
explicit NetworkManager(QObject* parent = nullptr);
[[nodiscard]] bool isAvailable() const override;
[[nodiscard]] bool wifiEnabled() const { return this->bWifiEnabled; };
[[nodiscard]] bool wifiHardwareEnabled() const { return this->bWifiHardwareEnabled; };
signals:
void deviceAdded(NetworkDevice* device);
void deviceRemoved(NetworkDevice* device);
void wifiEnabledChanged(bool enabled);
void wifiHardwareEnabledChanged(bool enabled);
public slots:
void setWifiEnabled(bool enabled);
private slots:
void onDevicePathAdded(const QDBusObjectPath& path);
void onDevicePathRemoved(const QDBusObjectPath& path);
void activateConnection(const QDBusObjectPath& connPath, const QDBusObjectPath& devPath);
void addAndActivateConnection(
const ConnectionSettingsMap& settings,
const QDBusObjectPath& devPath,
const QDBusObjectPath& specificObjectPath
);
private:
void init();
void registerDevices();
void registerDevice(const QString& path);
void registerFrontendDevice(NMDeviceType::Enum type, NMDevice* dev);
void removeFrontendDevice(NMDevice* dev);
QHash<QString, NMDevice*> mDevices;
QHash<QString, NetworkDevice*> mFrontendDevices;
// clang-format off
Q_OBJECT_BINDABLE_PROPERTY(NetworkManager, bool, bWifiEnabled, &NetworkManager::wifiEnabledChanged);
Q_OBJECT_BINDABLE_PROPERTY(NetworkManager, bool, bWifiHardwareEnabled, &NetworkManager::wifiHardwareEnabledChanged);
QS_DBUS_BINDABLE_PROPERTY_GROUP(NetworkManager, dbusProperties);
QS_DBUS_PROPERTY_BINDING(NetworkManager, pWifiEnabled, bWifiEnabled, dbusProperties, "WirelessEnabled");
QS_DBUS_PROPERTY_BINDING(NetworkManager, pWifiHardwareEnabled, bWifiHardwareEnabled, dbusProperties, "WirelessHardwareEnabled");
// clang-format on
DBusNetworkManagerProxy* proxy = nullptr;
};
} // namespace qs::network

View file

@ -0,0 +1,151 @@
#include "connection.hpp"
#include <qdbusconnection.h>
#include <qdbusmetatype.h>
#include <qdbuspendingcall.h>
#include <qdbuspendingreply.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qnamespace.h>
#include <qobject.h>
#include <qstring.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include "../../core/logcat.hpp"
#include "../../dbus/properties.hpp"
#include "../wifi.hpp"
#include "dbus_nm_active_connection.h"
#include "dbus_nm_connection_settings.h"
#include "dbus_types.hpp"
#include "enums.hpp"
#include "utils.hpp"
namespace qs::network {
using namespace qs::dbus;
namespace {
QS_LOGGING_CATEGORY(logNetworkManager, "quickshell.network.networkmanager", QtWarningMsg);
}
NMConnectionSettings::NMConnectionSettings(const QString& path, QObject* parent): QObject(parent) {
qDBusRegisterMetaType<ConnectionSettingsMap>();
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;
}
QObject::connect(
this->proxy,
&DBusNMConnectionSettingsProxy::Updated,
this,
&NMConnectionSettings::updateSettings
);
this->bSecurity.setBinding([this]() { return securityFromConnectionSettings(this->bSettings); });
this->connectionSettingsProperties.setInterface(this->proxy);
this->connectionSettingsProperties.updateAllViaGetAll();
this->updateSettings();
}
void NMConnectionSettings::updateSettings() {
auto pending = this->proxy->GetSettings();
auto* call = new QDBusPendingCallWatcher(pending, this);
auto responseCallback = [this](QDBusPendingCallWatcher* call) {
const QDBusPendingReply<ConnectionSettingsMap> reply = *call;
if (reply.isError()) {
qCWarning(logNetworkManager)
<< "Failed to get" << this->path() << "settings:" << reply.error().message();
} else {
this->bSettings = reply.value();
}
if (!this->mLoaded) {
emit this->loaded();
this->mLoaded = true;
}
delete call;
};
QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback);
}
void NMConnectionSettings::forget() {
auto pending = this->proxy->Delete();
auto* call = new QDBusPendingCallWatcher(pending, this);
auto responseCallback = [this](QDBusPendingCallWatcher* call) {
const QDBusPendingReply<> reply = *call;
if (reply.isError()) {
qCWarning(logNetworkManager)
<< "Failed to forget" << this->path() << ":" << reply.error().message();
}
delete call;
};
QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback);
}
bool NMConnectionSettings::isValid() const { return this->proxy && this->proxy->isValid(); }
QString NMConnectionSettings::address() const {
return this->proxy ? this->proxy->service() : QString();
}
QString NMConnectionSettings::path() const { return this->proxy ? this->proxy->path() : QString(); }
NMActiveConnection::NMActiveConnection(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, &NMActiveConnection::loaded, Qt::SingleShotConnection);
QObject::connect(this->proxy, &DBusNMActiveConnectionProxy::StateChanged, this, &NMActiveConnection::onStateChanged);
// clang-format on
this->activeConnectionProperties.setInterface(this->proxy);
this->activeConnectionProperties.updateAllViaGetAll();
}
void NMActiveConnection::onStateChanged(quint32 /*state*/, quint32 reason) {
auto enumReason = static_cast<NMConnectionStateReason::Enum>(reason);
if (this->mStateReason == enumReason) return;
this->mStateReason = enumReason;
emit this->stateReasonChanged(enumReason);
}
bool NMActiveConnection::isValid() const { return this->proxy && this->proxy->isValid(); }
QString NMActiveConnection::address() const {
return this->proxy ? this->proxy->service() : QString();
}
QString NMActiveConnection::path() const { return this->proxy ? this->proxy->path() : QString(); }
} // namespace qs::network
namespace qs::dbus {
DBusResult<qs::network::NMConnectionState::Enum>
DBusDataTransform<qs::network::NMConnectionState::Enum>::fromWire(quint32 wire) {
return DBusResult(static_cast<qs::network::NMConnectionState::Enum>(wire));
}
} // namespace qs::dbus

View file

@ -0,0 +1,105 @@
#pragma once
#include <qdbusextratypes.h>
#include <qdbuspendingcall.h>
#include <qdbuspendingreply.h>
#include <qobject.h>
#include <qproperty.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include "../../dbus/properties.hpp"
#include "../wifi.hpp"
#include "dbus_nm_active_connection.h"
#include "dbus_nm_connection_settings.h"
#include "dbus_types.hpp"
#include "enums.hpp"
namespace qs::dbus {
template <>
struct DBusDataTransform<qs::network::NMConnectionState::Enum> {
using Wire = quint32;
using Data = qs::network::NMConnectionState::Enum;
static DBusResult<Data> fromWire(Wire wire);
};
} // namespace qs::dbus
namespace qs::network {
// Proxy of a /org/freedesktop/NetworkManager/Settings/Connection/* object.
class NMConnectionSettings: public QObject {
Q_OBJECT;
public:
explicit NMConnectionSettings(const QString& path, QObject* parent = nullptr);
void forget();
[[nodiscard]] bool isValid() const;
[[nodiscard]] QString path() const;
[[nodiscard]] QString address() const;
[[nodiscard]] ConnectionSettingsMap settings() const { return this->bSettings; };
[[nodiscard]] WifiSecurityType::Enum security() const { return this->bSecurity; };
[[nodiscard]] QBindable<WifiSecurityType::Enum> bindableSecurity() { return &this->bSecurity; };
signals:
void loaded();
void settingsChanged(ConnectionSettingsMap settings);
void securityChanged(WifiSecurityType::Enum security);
void ssidChanged(QString ssid);
private:
bool mLoaded = false;
void updateSettings();
// clang-format off
Q_OBJECT_BINDABLE_PROPERTY(NMConnectionSettings, ConnectionSettingsMap, bSettings, &NMConnectionSettings::settingsChanged);
Q_OBJECT_BINDABLE_PROPERTY(NMConnectionSettings, WifiSecurityType::Enum, bSecurity, &NMConnectionSettings::securityChanged);
QS_DBUS_BINDABLE_PROPERTY_GROUP(NMConnectionSettings, connectionSettingsProperties);
// clang-format on
DBusNMConnectionSettingsProxy* proxy = nullptr;
};
// Proxy of a /org/freedesktop/NetworkManager/ActiveConnection/* object.
class NMActiveConnection: public QObject {
Q_OBJECT;
public:
explicit NMActiveConnection(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]] NMConnectionState::Enum state() const { return this->bState; };
[[nodiscard]] NMConnectionStateReason::Enum stateReason() const { return this->mStateReason; };
signals:
void loaded();
void connectionChanged(QDBusObjectPath path);
void stateChanged(NMConnectionState::Enum state);
void stateReasonChanged(NMConnectionStateReason::Enum reason);
void uuidChanged(const QString& uuid);
private slots:
void onStateChanged(quint32 state, quint32 reason);
private:
NMConnectionStateReason::Enum mStateReason = NMConnectionStateReason::Unknown;
// clang-format off
Q_OBJECT_BINDABLE_PROPERTY(NMActiveConnection, QDBusObjectPath, bConnection, &NMActiveConnection::connectionChanged);
Q_OBJECT_BINDABLE_PROPERTY(NMActiveConnection, QString, bUuid, &NMActiveConnection::uuidChanged);
Q_OBJECT_BINDABLE_PROPERTY(NMActiveConnection, NMConnectionState::Enum, bState, &NMActiveConnection::stateChanged);
QS_DBUS_BINDABLE_PROPERTY_GROUP(NMActiveConnection, activeConnectionProperties);
QS_DBUS_PROPERTY_BINDING(NMActiveConnection, pConnection, bConnection, activeConnectionProperties, "Connection");
QS_DBUS_PROPERTY_BINDING(NMActiveConnection, pUuid, bUuid, activeConnectionProperties, "Uuid");
QS_DBUS_PROPERTY_BINDING(NMActiveConnection, pState, bState, activeConnectionProperties, "State");
// clang-format on
DBusNMActiveConnectionProxy* proxy = nullptr;
};
} // namespace qs::network

View file

@ -0,0 +1,9 @@
#pragma once
#include <qdbusextratypes.h>
#include <qmap.h>
#include <qstring.h>
#include <qvariant.h>
using ConnectionSettingsMap = QMap<QString, QVariantMap>;
Q_DECLARE_METATYPE(ConnectionSettingsMap);

143
src/network/nm/device.cpp Normal file
View file

@ -0,0 +1,143 @@
#include "device.hpp"
#include <qdbusconnection.h>
#include <qdbusextratypes.h>
#include <qlist.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qnamespace.h>
#include <qobject.h>
#include <qset.h>
#include <qstring.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include "../../core/logcat.hpp"
#include "../../dbus/properties.hpp"
#include "../device.hpp"
#include "connection.hpp"
#include "dbus_nm_device.h"
namespace qs::network {
using namespace qs::dbus;
namespace {
QS_LOGGING_CATEGORY(logNetworkManager, "quickshell.network.networkmanager", QtWarningMsg);
}
NMDevice::NMDevice(const QString& path, QObject* parent): QObject(parent) {
this->deviceProxy = new DBusNMDeviceProxy(
"org.freedesktop.NetworkManager",
path,
QDBusConnection::systemBus(),
this
);
if (!this->deviceProxy->isValid()) {
qCWarning(logNetworkManager) << "Cannot create DBus interface for device at" << path;
return;
}
// clang-format off
QObject::connect(this, &NMDevice::availableConnectionPathsChanged, this, &NMDevice::onAvailableConnectionPathsChanged);
QObject::connect(this, &NMDevice::activeConnectionPathChanged, this, &NMDevice::onActiveConnectionPathChanged);
// clang-format on
this->deviceProperties.setInterface(this->deviceProxy);
this->deviceProperties.updateAllViaGetAll();
}
void NMDevice::onActiveConnectionPathChanged(const QDBusObjectPath& path) {
const QString stringPath = path.path();
// Remove old active connection
if (this->mActiveConnection) {
QObject::disconnect(this->mActiveConnection, nullptr, this, nullptr);
delete this->mActiveConnection;
this->mActiveConnection = nullptr;
}
// Create new active connection
if (stringPath != "/") {
auto* active = new NMActiveConnection(stringPath, this);
if (!active->isValid()) {
qCWarning(logNetworkManager) << "Ignoring invalid registration of" << stringPath;
delete active;
} else {
this->mActiveConnection = active;
QObject::connect(
active,
&NMActiveConnection::loaded,
this,
[this, active]() { emit this->activeConnectionLoaded(active); },
Qt::SingleShotConnection
);
}
}
}
void NMDevice::onAvailableConnectionPathsChanged(const QList<QDBusObjectPath>& paths) {
QSet<QString> newPathSet;
for (const QDBusObjectPath& path: paths) {
newPathSet.insert(path.path());
}
const auto existingPaths = this->mConnections.keys();
const QSet<QString> existingPathSet(existingPaths.begin(), existingPaths.end());
const auto addedConnections = newPathSet - existingPathSet;
const auto removedConnections = existingPathSet - newPathSet;
for (const QString& path: addedConnections) {
this->registerConnection(path);
}
for (const QString& path: removedConnections) {
auto* connection = this->mConnections.take(path);
if (!connection) {
qCDebug(logNetworkManager) << "Sent removal signal for" << path << "which is not registered.";
} else {
delete connection;
}
};
}
void NMDevice::registerConnection(const QString& path) {
auto* connection = new NMConnectionSettings(path, this);
if (!connection->isValid()) {
qCWarning(logNetworkManager) << "Ignoring invalid registration of" << path;
delete connection;
} else {
this->mConnections.insert(path, connection);
QObject::connect(
connection,
&NMConnectionSettings::loaded,
this,
[this, connection]() { emit this->connectionLoaded(connection); },
Qt::SingleShotConnection
);
}
}
void NMDevice::disconnect() { this->deviceProxy->Disconnect(); }
void NMDevice::setAutoconnect(bool autoconnect) {
if (autoconnect == this->bAutoconnect) return;
this->bAutoconnect = autoconnect;
this->pAutoconnect.write();
}
bool NMDevice::isValid() const { return this->deviceProxy && this->deviceProxy->isValid(); }
QString NMDevice::address() const {
return this->deviceProxy ? this->deviceProxy->service() : QString();
}
QString NMDevice::path() const { return this->deviceProxy ? this->deviceProxy->path() : QString(); }
} // namespace qs::network
namespace qs::dbus {
DBusResult<qs::network::NMDeviceState::Enum>
DBusDataTransform<qs::network::NMDeviceState::Enum>::fromWire(quint32 wire) {
return DBusResult(static_cast<qs::network::NMDeviceState::Enum>(wire));
}
} // namespace qs::dbus

100
src/network/nm/device.hpp Normal file
View file

@ -0,0 +1,100 @@
#pragma once
#include <qdbusextratypes.h>
#include <qhash.h>
#include <qobject.h>
#include <qproperty.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include "../../dbus/properties.hpp"
#include "connection.hpp"
#include "dbus_nm_device.h"
namespace qs::dbus {
template <>
struct DBusDataTransform<qs::network::NMDeviceState::Enum> {
using Wire = quint32;
using Data = qs::network::NMDeviceState::Enum;
static DBusResult<Data> fromWire(Wire wire);
};
} // namespace qs::dbus
namespace qs::network {
// Proxy of a /org/freedesktop/NetworkManager/Device/* object.
// Only the members from the org.freedesktop.NetworkManager.Device interface.
// Owns the lifetime of NMActiveConnection(s) and NMConnectionSetting(s).
class NMDevice: public QObject {
Q_OBJECT;
public:
explicit NMDevice(const QString& path, QObject* parent = nullptr);
[[nodiscard]] virtual bool isValid() const;
[[nodiscard]] QString path() const;
[[nodiscard]] QString address() const;
[[nodiscard]] QString interface() const { return this->bInterface; };
[[nodiscard]] QString hwAddress() const { return this->bHwAddress; };
[[nodiscard]] bool managed() const { return this->bManaged; };
[[nodiscard]] NMDeviceState::Enum state() const { return this->bState; };
[[nodiscard]] bool autoconnect() const { return this->bAutoconnect; };
[[nodiscard]] NMActiveConnection* activeConnection() const { return this->mActiveConnection; };
signals:
void activateConnection(const QDBusObjectPath& connPath, const QDBusObjectPath& devPath);
void addAndActivateConnection(
const ConnectionSettingsMap& settings,
const QDBusObjectPath& devPath,
const QDBusObjectPath& apPath
);
void connectionLoaded(NMConnectionSettings* connection);
void connectionRemoved(NMConnectionSettings* connection);
void availableConnectionPathsChanged(QList<QDBusObjectPath> paths);
void activeConnectionPathChanged(const QDBusObjectPath& connection);
void activeConnectionLoaded(NMActiveConnection* active);
void interfaceChanged(const QString& interface);
void hwAddressChanged(const QString& hwAddress);
void managedChanged(bool managed);
void stateChanged(NMDeviceState::Enum state);
void autoconnectChanged(bool autoconnect);
public slots:
void disconnect();
void setAutoconnect(bool autoconnect);
private slots:
void onAvailableConnectionPathsChanged(const QList<QDBusObjectPath>& paths);
void onActiveConnectionPathChanged(const QDBusObjectPath& path);
private:
void registerConnection(const QString& path);
QHash<QString, NMConnectionSettings*> mConnections;
NMActiveConnection* mActiveConnection = nullptr;
// clang-format off
Q_OBJECT_BINDABLE_PROPERTY(NMDevice, QString, bInterface, &NMDevice::interfaceChanged);
Q_OBJECT_BINDABLE_PROPERTY(NMDevice, QString, bHwAddress, &NMDevice::hwAddressChanged);
Q_OBJECT_BINDABLE_PROPERTY(NMDevice, bool, bManaged, &NMDevice::managedChanged);
Q_OBJECT_BINDABLE_PROPERTY(NMDevice, NMDeviceState::Enum, bState, &NMDevice::stateChanged);
Q_OBJECT_BINDABLE_PROPERTY(NMDevice, bool, bAutoconnect, &NMDevice::autoconnectChanged);
Q_OBJECT_BINDABLE_PROPERTY(NMDevice, QList<QDBusObjectPath>, bAvailableConnections, &NMDevice::availableConnectionPathsChanged);
Q_OBJECT_BINDABLE_PROPERTY(NMDevice, QDBusObjectPath, bActiveConnection, &NMDevice::activeConnectionPathChanged);
QS_DBUS_BINDABLE_PROPERTY_GROUP(NMDeviceAdapter, deviceProperties);
QS_DBUS_PROPERTY_BINDING(NMDevice, pName, bInterface, deviceProperties, "Interface");
QS_DBUS_PROPERTY_BINDING(NMDevice, pAddress, bHwAddress, deviceProperties, "HwAddress");
QS_DBUS_PROPERTY_BINDING(NMDevice, pManaged, bManaged, deviceProperties, "Managed");
QS_DBUS_PROPERTY_BINDING(NMDevice, pState, bState, deviceProperties, "State");
QS_DBUS_PROPERTY_BINDING(NMDevice, pAutoconnect, bAutoconnect, deviceProperties, "Autoconnect");
QS_DBUS_PROPERTY_BINDING(NMDevice, pAvailableConnections, bAvailableConnections, deviceProperties, "AvailableConnections");
QS_DBUS_PROPERTY_BINDING(NMDevice, pActiveConnection, bActiveConnection, deviceProperties, "ActiveConnection");
// clang-format on
DBusNMDeviceProxy* deviceProxy = nullptr;
};
} // namespace qs::network

156
src/network/nm/enums.hpp Normal file
View file

@ -0,0 +1,156 @@
#pragma once
#include <qobject.h>
#include <qqmlintegration.h>
#include <qtmetamacros.h>
#include <qtypes.h>
namespace qs::network {
// Indicates the type of hardware represented by a device object.
class NMDeviceType: public QObject {
Q_OBJECT;
public:
enum Enum : quint8 {
Unknown = 0,
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,
Generic = 14,
Team = 15,
Tun = 16,
IpTunnel = 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);
};
// 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);
};
// 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 = 4,
};
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 NMConnectionState: public QObject {
Q_OBJECT;
public:
enum Enum : quint8 {
Unknown = 0,
Activating = 1,
Activated = 2,
Deactivating = 3,
Deactivated = 4
};
Q_ENUM(Enum);
};
} // namespace qs::network

View file

@ -0,0 +1,4 @@
<node>
<interface name="org.freedesktop.NetworkManager.AccessPoint">
</interface>
</node>

View file

@ -0,0 +1,8 @@
<node>
<interface name="org.freedesktop.NetworkManager.Connection.Active">
<signal name="StateChanged">
<arg name="state" type="u"/>
<arg name="reason" type="u"/>
</signal>
</interface>
</node>

View file

@ -0,0 +1,15 @@
<node>
<interface name="org.freedesktop.NetworkManager.Device.Wireless">
<method name="GetAllAccessPoints" type="ao"/>
<method name="RequestScan">
<arg name="options" type="a{sv}" direction="in"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.In0" value="QVariantMap"/>
</method>
<signal name="AccessPointAdded">
<arg name="access_point" type="o"/>
</signal>
<signal name="AccessPointRemoved">
<arg name="access_point" type="o"/>
</signal>
</interface>
</node>

View file

@ -0,0 +1,5 @@
<node>
<interface name="org.freedesktop.NetworkManager.Device">
<method name="Disconnect"/>
</interface>
</node>

View file

@ -0,0 +1,11 @@
<node>
<interface name="org.freedesktop.NetworkManager.Settings.Connection">
<method name="GetSettings">
<arg name="settings" type="a{sa{sv}}" direction="out"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="ConnectionSettingsMap"/>
</method>
<method name="Delete"/>
<signal name="Updated"/>
<signal name="Removed"/>
</interface>
</node>

View file

@ -0,0 +1,27 @@
<node>
<interface name="org.freedesktop.NetworkManager">
<method name="GetAllDevices">
<arg direction="out" type="ao" name="devices"/>
</method>
<method name="ActivateConnection">
<arg direction="in" type="o" name="connection"/>
<arg direction="in" type="o" name="device"/>
<arg direction="in" type="o" name="specific_object"/>
<arg direction="out" type="o" name="active_connection"/>
</method>
<method name="AddAndActivateConnection">
<arg direction="in" type="a{sa{sv}}" name="connection"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.In0" value="ConnectionSettingsMap"/>
<arg direction="in" type="o" name="device"/>
<arg direction="in" type="o" name="specific_object"/>
<arg direction="out" type="o" name="path"/>
<arg direction="out" type="o" name="active_connection"/>
</method>
<signal name="DeviceAdded">
<arg type="o" name="device_path"/>
</signal>
<signal name="DeviceRemoved">
<arg type="o" name="device_path"/>
</signal>
</interface>
</node>

248
src/network/nm/utils.cpp Normal file
View file

@ -0,0 +1,248 @@
#include "utils.hpp"
// We depend on non-std Linux extensions that ctime doesn't put in the global namespace
// NOLINTNEXTLINE(modernize-deprecated-headers)
#include <time.h>
#include <qcontainerfwd.h>
#include <qdatetime.h>
#include <qdbusservicewatcher.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qtypes.h>
#include "../wifi.hpp"
#include "dbus_types.hpp"
#include "enums.hpp"
namespace qs::network {
WifiSecurityType::Enum securityFromConnectionSettings(const ConnectionSettingsMap& settings) {
const QVariantMap& security = settings.value("802-11-wireless-security");
if (security.isEmpty()) {
return WifiSecurityType::Open;
};
const QString keyMgmt = security["key-mgmt"].toString();
const QString authAlg = security["auth-alg"].toString();
const QList<QVariant> proto = security["proto"].toList();
if (keyMgmt == "none") {
return WifiSecurityType::StaticWep;
} else if (keyMgmt == "ieee8021x") {
if (authAlg == "leap") {
return WifiSecurityType::Leap;
} else {
return WifiSecurityType::DynamicWep;
}
} else if (keyMgmt == "wpa-psk") {
if (proto.contains("wpa") && proto.contains("rsn")) return WifiSecurityType::WpaPsk;
return WifiSecurityType::Wpa2Psk;
} else if (keyMgmt == "wpa-eap") {
if (proto.contains("wpa") && proto.contains("rsn")) return WifiSecurityType::WpaEap;
return WifiSecurityType::Wpa2Eap;
} else if (keyMgmt == "sae") {
return WifiSecurityType::Sae;
} else if (keyMgmt == "wpa-eap-suite-b-192") {
return WifiSecurityType::Wpa3SuiteB192;
}
return WifiSecurityType::Open;
}
bool deviceSupportsApCiphers(
NMWirelessCapabilities::Enum caps,
NM80211ApSecurityFlags::Enum apFlags,
WifiSecurityType::Enum type
) {
bool havePair = false;
bool haveGroup = false;
// Device needs to support at least one pairwise and one group cipher
if (type == WifiSecurityType::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 != WifiSecurityType::StaticWep) {
if (caps & NMWirelessCapabilities::CipherTkip && apFlags & NM80211ApSecurityFlags::GroupTkip) {
haveGroup = true;
}
if (caps & NMWirelessCapabilities::CipherCcmp && apFlags & NM80211ApSecurityFlags::GroupCcmp) {
haveGroup = true;
}
}
return (havePair && haveGroup);
}
bool securityIsValid(
WifiSecurityType::Enum type,
NMWirelessCapabilities::Enum caps,
bool adhoc,
NM80211ApFlags::Enum apFlags,
NM80211ApSecurityFlags::Enum apWpa,
NM80211ApSecurityFlags::Enum apRsn
) {
switch (type) {
case WifiSecurityType::Open:
if (apFlags & NM80211ApFlags::Privacy) return false;
if (apWpa || apRsn) return false;
break;
case WifiSecurityType::Leap:
if (adhoc) return false;
case WifiSecurityType::StaticWep:
if (!(apFlags & NM80211ApFlags::Privacy)) return false;
if (apWpa || apRsn) {
if (!deviceSupportsApCiphers(caps, apWpa, WifiSecurityType::StaticWep)) {
if (!deviceSupportsApCiphers(caps, apRsn, WifiSecurityType::StaticWep)) return false;
}
}
break;
case WifiSecurityType::DynamicWep:
if (adhoc) return false;
if (apRsn || !(apFlags & NM80211ApFlags::Privacy)) return false;
if (apWpa) {
if (!(apWpa & NM80211ApSecurityFlags::KeyMgmt8021x)) return false;
if (!deviceSupportsApCiphers(caps, apWpa, WifiSecurityType::DynamicWep)) return false;
}
break;
case WifiSecurityType::WpaPsk:
if (adhoc) return false;
if (!(caps & NMWirelessCapabilities::Wpa)) return false;
if (apWpa & NM80211ApSecurityFlags::KeyMgmtPsk) {
if (apWpa & NM80211ApSecurityFlags::PairTkip && caps & NMWirelessCapabilities::CipherTkip) {
return true;
}
if (apWpa & NM80211ApSecurityFlags::PairCcmp && caps & NMWirelessCapabilities::CipherCcmp) {
return true;
}
}
return false;
case WifiSecurityType::Wpa2Psk:
if (!(caps & NMWirelessCapabilities::Rsn)) return false;
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;
case WifiSecurityType::WpaEap:
if (adhoc) return false;
if (!(caps & NMWirelessCapabilities::Wpa)) return false;
if (!(apWpa & NM80211ApSecurityFlags::KeyMgmt8021x)) return false;
if (!deviceSupportsApCiphers(caps, apWpa, WifiSecurityType::WpaEap)) return false;
break;
case WifiSecurityType::Wpa2Eap:
if (adhoc) return false;
if (!(caps & NMWirelessCapabilities::Rsn)) return false;
if (!(apRsn & NM80211ApSecurityFlags::KeyMgmt8021x)) return false;
if (!deviceSupportsApCiphers(caps, apRsn, WifiSecurityType::Wpa2Eap)) return false;
break;
case WifiSecurityType::Sae:
if (!(caps & NMWirelessCapabilities::Rsn)) return false;
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;
case WifiSecurityType::Owe:
if (adhoc) return false;
if (!(caps & NMWirelessCapabilities::Rsn)) return false;
if (!(apRsn & NM80211ApSecurityFlags::KeyMgmtOwe)
&& !(apRsn & NM80211ApSecurityFlags::KeyMgmtOweTm))
{
return false;
}
break;
case WifiSecurityType::Wpa3SuiteB192:
if (adhoc) return false;
if (!(caps & NMWirelessCapabilities::Rsn)) return false;
if (!(apRsn & NM80211ApSecurityFlags::KeyMgmtEapSuiteB192)) return false;
break;
default: return false;
}
return true;
}
WifiSecurityType::Enum findBestWirelessSecurity(
NMWirelessCapabilities::Enum caps,
bool adHoc,
NM80211ApFlags::Enum apFlags,
NM80211ApSecurityFlags::Enum apWpa,
NM80211ApSecurityFlags::Enum apRsn
) {
// Loop through security types from most to least secure since the enum
// values are sequential and in priority order (0-10, excluding Unknown=11)
for (int i = WifiSecurityType::Wpa3SuiteB192; i <= WifiSecurityType::Open; ++i) {
auto type = static_cast<WifiSecurityType::Enum>(i);
if (securityIsValid(type, caps, adHoc, apFlags, apWpa, apRsn)) {
return type;
}
}
return WifiSecurityType::Unknown;
}
// NOLINTBEGIN
QDateTime clockBootTimeToDateTime(qint64 clockBootTime) {
clockid_t clkId = CLOCK_BOOTTIME;
struct timespec tp {};
const QDateTime now = QDateTime::currentDateTime();
int r = clock_gettime(clkId, &tp);
if (r == -1 && errno == EINVAL) {
clkId = CLOCK_MONOTONIC;
r = clock_gettime(clkId, &tp);
}
// Convert to milliseconds
const qint64 nowInMs = tp.tv_sec * 1000 + tp.tv_nsec / 1000000;
// Return a QDateTime of the millisecond diff
const qint64 offset = clockBootTime - nowInMs;
return QDateTime::fromMSecsSinceEpoch(now.toMSecsSinceEpoch() + offset);
}
// NOLINTEND
} // namespace qs::network

45
src/network/nm/utils.hpp Normal file
View file

@ -0,0 +1,45 @@
#pragma once
#include <qcontainerfwd.h>
#include <qdbusservicewatcher.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include "../wifi.hpp"
#include "dbus_types.hpp"
#include "enums.hpp"
namespace qs::network {
WifiSecurityType::Enum securityFromConnectionSettings(const ConnectionSettingsMap& settings);
bool deviceSupportsApCiphers(
NMWirelessCapabilities::Enum caps,
NM80211ApSecurityFlags::Enum apFlags,
WifiSecurityType::Enum type
);
// 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(
WifiSecurityType::Enum type,
NMWirelessCapabilities::Enum caps,
bool adhoc,
NM80211ApFlags::Enum apFlags,
NM80211ApSecurityFlags::Enum apWpa,
NM80211ApSecurityFlags::Enum apRsn
);
WifiSecurityType::Enum findBestWirelessSecurity(
NMWirelessCapabilities::Enum caps,
bool adHoc,
NM80211ApFlags::Enum apFlags,
NM80211ApSecurityFlags::Enum apWpa,
NM80211ApSecurityFlags::Enum apRsn
);
QDateTime clockBootTimeToDateTime(qint64 clockBootTime);
} // namespace qs::network

457
src/network/nm/wireless.cpp Normal file
View file

@ -0,0 +1,457 @@
#include "wireless.hpp"
#include <utility>
#include <qdatetime.h>
#include <qdbusconnection.h>
#include <qdbusextratypes.h>
#include <qdbuspendingcall.h>
#include <qdbuspendingreply.h>
#include <qlist.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qnamespace.h>
#include <qobject.h>
#include <qstring.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include "../../core/logcat.hpp"
#include "../../dbus/properties.hpp"
#include "../network.hpp"
#include "../wifi.hpp"
#include "accesspoint.hpp"
#include "connection.hpp"
#include "dbus_nm_wireless.h"
#include "dbus_types.hpp"
#include "device.hpp"
#include "enums.hpp"
#include "utils.hpp"
namespace qs::network {
using namespace qs::dbus;
namespace {
QS_LOGGING_CATEGORY(logNetworkManager, "quickshell.network.networkmanager", QtWarningMsg);
}
NMWirelessNetwork::NMWirelessNetwork(QString ssid, QObject* parent)
: QObject(parent)
, mSsid(std::move(ssid))
, bKnown(false)
, bSecurity(WifiSecurityType::Unknown)
, bReason(NMConnectionStateReason::None)
, bState(NMConnectionState::Deactivated) {}
void NMWirelessNetwork::updateReferenceConnection() {
// If the network has no connections, the reference is nullptr.
if (this->mConnections.isEmpty()) {
this->mReferenceConn = nullptr;
this->bSecurity = WifiSecurityType::Unknown;
// Set security back to reference AP.
if (this->mReferenceAp) {
this->bSecurity.setBinding([this]() { return this->mReferenceAp->security(); });
}
return;
};
// If the network has an active connection, use it as the reference.
if (this->mActiveConnection) {
auto* conn = this->mConnections.value(this->mActiveConnection->connection().path());
if (conn && conn != this->mReferenceConn) {
this->mReferenceConn = conn;
this->bSecurity.setBinding([conn]() { return conn->security(); });
}
return;
}
// Otherwise, choose the connection with the strongest security settings.
NMConnectionSettings* selectedConn = nullptr;
for (auto* conn: this->mConnections.values()) {
if (!selectedConn || conn->security() > selectedConn->security()) {
selectedConn = conn;
}
}
if (this->mReferenceConn != selectedConn) {
this->mReferenceConn = selectedConn;
this->bSecurity.setBinding([selectedConn]() { return selectedConn->security(); });
}
}
void NMWirelessNetwork::updateReferenceAp() {
// If the network has no APs, the reference is a nullptr.
if (this->mAccessPoints.isEmpty()) {
this->mReferenceAp = nullptr;
this->bSignalStrength = 0;
return;
}
// Otherwise, choose the AP with the strongest signal.
NMAccessPoint* selectedAp = nullptr;
for (auto* ap: this->mAccessPoints.values()) {
// Always prefer the active AP.
if (ap->path() == this->bActiveApPath) {
selectedAp = ap;
break;
}
if (!selectedAp || ap->signalStrength() > selectedAp->signalStrength()) {
selectedAp = ap;
}
}
if (this->mReferenceAp != selectedAp) {
this->mReferenceAp = selectedAp;
this->bSignalStrength.setBinding([selectedAp]() { return selectedAp->signalStrength(); });
// Reference AP is used for security when there's no connection settings.
if (!this->mReferenceConn) {
this->bSecurity.setBinding([selectedAp]() { return selectedAp->security(); });
}
}
}
void NMWirelessNetwork::addAccessPoint(NMAccessPoint* ap) {
if (this->mAccessPoints.contains(ap->path())) return;
this->mAccessPoints.insert(ap->path(), ap);
auto onDestroyed = [this, ap]() {
if (this->mAccessPoints.take(ap->path())) {
this->updateReferenceAp();
if (this->mAccessPoints.isEmpty() && this->mConnections.isEmpty()) emit this->disappeared();
}
};
// clang-format off
QObject::connect(ap, &NMAccessPoint::signalStrengthChanged, this, &NMWirelessNetwork::updateReferenceAp);
QObject::connect(ap, &NMAccessPoint::destroyed, this, onDestroyed);
// clang-format on
this->updateReferenceAp();
};
void NMWirelessNetwork::addConnection(NMConnectionSettings* conn) {
if (this->mConnections.contains(conn->path())) return;
this->mConnections.insert(conn->path(), conn);
auto onDestroyed = [this, conn]() {
if (this->mConnections.take(conn->path())) {
this->updateReferenceConnection();
if (this->mConnections.isEmpty()) this->bKnown = false;
if (this->mAccessPoints.isEmpty() && this->mConnections.isEmpty()) emit this->disappeared();
}
};
// clang-format off
QObject::connect(conn, &NMConnectionSettings::securityChanged, this, &NMWirelessNetwork::updateReferenceConnection);
QObject::connect(conn, &NMConnectionSettings::destroyed, this, onDestroyed);
// clang-format on
this->bKnown = true;
this->updateReferenceConnection();
};
void NMWirelessNetwork::addActiveConnection(NMActiveConnection* active) {
if (this->mActiveConnection) return;
this->mActiveConnection = active;
this->bState.setBinding([active]() { return active->state(); });
this->bReason.setBinding([active]() { return active->stateReason(); });
auto onDestroyed = [this, active]() {
if (this->mActiveConnection && this->mActiveConnection == active) {
this->mActiveConnection = nullptr;
this->updateReferenceConnection();
this->bState = NMConnectionState::Deactivated;
this->bReason = NMConnectionStateReason::None;
}
};
QObject::connect(active, &NMActiveConnection::destroyed, this, onDestroyed);
this->updateReferenceConnection();
};
void NMWirelessNetwork::forget() {
if (this->mConnections.isEmpty()) return;
for (auto* conn: this->mConnections.values()) {
conn->forget();
}
}
NMWirelessDevice::NMWirelessDevice(const QString& path, QObject* parent)
: NMDevice(path, parent)
, mScanTimer(this) {
this->wirelessProxy = new DBusNMWirelessProxy(
"org.freedesktop.NetworkManager",
path,
QDBusConnection::systemBus(),
this
);
if (!this->wirelessProxy->isValid()) {
qCWarning(logNetworkManager) << "Cannot create DBus interface for wireless device at" << path;
return;
}
QObject::connect(
&this->wirelessProperties,
&DBusPropertyGroup::getAllFinished,
this,
&NMWirelessDevice::initWireless,
Qt::SingleShotConnection
);
QObject::connect(&this->mScanTimer, &QTimer::timeout, this, &NMWirelessDevice::onScanTimeout);
this->mScanTimer.setSingleShot(true);
this->wirelessProperties.setInterface(this->wirelessProxy);
this->wirelessProperties.updateAllViaGetAll();
}
void NMWirelessDevice::initWireless() {
// clang-format off
QObject::connect(this->wirelessProxy, &DBusNMWirelessProxy::AccessPointAdded, this, &NMWirelessDevice::onAccessPointAdded);
QObject::connect(this->wirelessProxy, &DBusNMWirelessProxy::AccessPointRemoved, this, &NMWirelessDevice::onAccessPointRemoved);
QObject::connect(this, &NMWirelessDevice::accessPointLoaded, this, &NMWirelessDevice::onAccessPointLoaded);
QObject::connect(this, &NMWirelessDevice::connectionLoaded, this, &NMWirelessDevice::onConnectionLoaded);
QObject::connect(this, &NMWirelessDevice::activeConnectionLoaded, this, &NMWirelessDevice::onActiveConnectionLoaded);
QObject::connect(this, &NMWirelessDevice::scanningChanged, this, &NMWirelessDevice::onScanningChanged);
// clang-format on
this->registerAccessPoints();
}
void NMWirelessDevice::onAccessPointAdded(const QDBusObjectPath& path) {
this->registerAccessPoint(path.path());
}
void NMWirelessDevice::onAccessPointRemoved(const QDBusObjectPath& path) {
auto* ap = this->mAccessPoints.take(path.path());
if (!ap) {
qCDebug(logNetworkManager) << "Sent removal signal for" << path.path()
<< "which is not registered.";
return;
}
delete ap;
}
void NMWirelessDevice::onAccessPointLoaded(NMAccessPoint* ap) {
const QString ssid = ap->ssid();
if (!ssid.isEmpty()) {
auto mode = ap->mode();
if (mode == NM80211Mode::Infra) {
auto* net = this->mNetworks.value(ssid);
if (!net) net = this->registerNetwork(ssid);
net->addAccessPoint(ap);
}
}
}
void NMWirelessDevice::onConnectionLoaded(NMConnectionSettings* conn) {
const ConnectionSettingsMap& settings = conn->settings();
// Filter connections that aren't wireless or have missing settings
if (settings["connection"]["id"].toString().isEmpty()
|| settings["connection"]["uuid"].toString().isEmpty()
|| !settings.contains("802-11-wireless")
|| settings["802-11-wireless"]["ssid"].toString().isEmpty())
{
return;
}
const auto ssid = settings["802-11-wireless"]["ssid"].toString();
const auto mode = settings["802-11-wireless"]["mode"].toString();
if (mode == "infrastructure") {
auto* net = this->mNetworks.value(ssid);
if (!net) net = this->registerNetwork(ssid);
net->addConnection(conn);
// Check for active connections that loaded before their respective connection settings
auto* active = this->activeConnection();
if (active && conn->path() == active->connection().path()) {
net->addActiveConnection(active);
}
}
// TODO: Create hotspots when mode == "ap"
}
void NMWirelessDevice::onActiveConnectionLoaded(NMActiveConnection* active) {
// Find an exisiting network with connection settings that matches the active
const QString activeConnPath = active->connection().path();
for (const auto& net: this->mNetworks.values()) {
for (auto* conn: net->connections()) {
if (activeConnPath == conn->path()) {
net->addActiveConnection(active);
return;
}
}
}
}
void NMWirelessDevice::onScanTimeout() {
const QDateTime now = QDateTime::currentDateTime();
const QDateTime lastScan = this->bLastScan;
const QDateTime lastScanRequest = this->mLastScanRequest;
if (lastScan.isValid() && lastScan.msecsTo(now) < this->mScanIntervalMs) {
// Rate limit if backend last scan property updated within the interval
auto diff = static_cast<int>(this->mScanIntervalMs - lastScan.msecsTo(now));
this->mScanTimer.start(diff);
} else if (lastScanRequest.isValid() && lastScanRequest.msecsTo(now) < this->mScanIntervalMs) {
// Rate limit if frontend changes scanner state within the interval
auto diff = static_cast<int>(this->mScanIntervalMs - lastScanRequest.msecsTo(now));
this->mScanTimer.start(diff);
} else {
this->wirelessProxy->RequestScan({});
this->mLastScanRequest = now;
this->mScanTimer.start(this->mScanIntervalMs);
}
}
void NMWirelessDevice::onScanningChanged(bool scanning) {
scanning ? this->onScanTimeout() : this->mScanTimer.stop();
}
void NMWirelessDevice::registerAccessPoints() {
auto pending = this->wirelessProxy->GetAllAccessPoints();
auto* call = new QDBusPendingCallWatcher(pending, this);
auto responseCallback = [this](QDBusPendingCallWatcher* call) {
const QDBusPendingReply<QList<QDBusObjectPath>> 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 NMWirelessDevice::registerAccessPoint(const QString& path) {
if (this->mAccessPoints.contains(path)) {
qCDebug(logNetworkManager) << "Skipping duplicate registration of access point" << path;
return;
}
auto* ap = new NMAccessPoint(path, this);
if (!ap->isValid()) {
qCWarning(logNetworkManager) << "Ignoring invalid registration of" << path;
delete ap;
return;
}
this->mAccessPoints.insert(path, ap);
QObject::connect(
ap,
&NMAccessPoint::loaded,
this,
[this, ap]() { emit this->accessPointLoaded(ap); },
Qt::SingleShotConnection
);
ap->bindableSecurity().setBinding([this, ap]() {
return findBestWirelessSecurity(
this->bCapabilities,
ap->mode() == NM80211Mode::Adhoc,
ap->flags(),
ap->wpaFlags(),
ap->rsnFlags()
);
});
}
NMWirelessNetwork* NMWirelessDevice::registerNetwork(const QString& ssid) {
auto* net = new NMWirelessNetwork(ssid, this);
// To avoid exposing outdated state to the frontend, filter the backend networks to only show
// the known or currently connected networks when the scanner is off.
auto visible = [this, net]() {
return this->bScanning || net->state() == NMConnectionState::Activated || net->known();
};
auto onVisibilityChanged = [this, net](bool visible) {
visible ? this->registerFrontendNetwork(net) : this->removeFrontendNetwork(net);
};
net->bindableVisible().setBinding(visible);
net->bindableActiveApPath().setBinding([this]() { return this->activeApPath().path(); });
QObject::connect(net, &NMWirelessNetwork::disappeared, this, &NMWirelessDevice::removeNetwork);
QObject::connect(net, &NMWirelessNetwork::visibilityChanged, this, onVisibilityChanged);
this->mNetworks.insert(ssid, net);
if (net->visible()) this->registerFrontendNetwork(net);
return net;
}
void NMWirelessDevice::registerFrontendNetwork(NMWirelessNetwork* net) {
auto ssid = net->ssid();
auto* frontendNet = new WifiNetwork(ssid, net);
// Bind WifiNetwork to NMWirelessNetwork
auto translateSignal = [net]() { return net->signalStrength() / 100.0; };
auto translateState = [net]() { return net->state() == NMConnectionState::Activated; };
frontendNet->bindableSignalStrength().setBinding(translateSignal);
frontendNet->bindableConnected().setBinding(translateState);
frontendNet->bindableKnown().setBinding([net]() { return net->known(); });
frontendNet->bindableNmReason().setBinding([net]() { return net->reason(); });
frontendNet->bindableSecurity().setBinding([net]() { return net->security(); });
frontendNet->bindableState().setBinding([net]() {
return static_cast<NetworkState::Enum>(net->state());
});
QObject::connect(frontendNet, &WifiNetwork::requestConnect, this, [this, net]() {
if (net->referenceConnection()) {
emit this->activateConnection(
QDBusObjectPath(net->referenceConnection()->path()),
QDBusObjectPath(this->path())
);
return;
}
if (net->referenceAp()) {
emit this->addAndActivateConnection(
ConnectionSettingsMap(),
QDBusObjectPath(this->path()),
QDBusObjectPath(net->referenceAp()->path())
);
}
});
QObject::connect(
frontendNet,
&WifiNetwork::requestDisconnect,
this,
&NMWirelessDevice::disconnect
);
QObject::connect(frontendNet, &WifiNetwork::requestForget, net, &NMWirelessNetwork::forget);
this->mFrontendNetworks.insert(ssid, frontendNet);
emit this->networkAdded(frontendNet);
}
void NMWirelessDevice::removeFrontendNetwork(NMWirelessNetwork* net) {
auto* frontendNet = this->mFrontendNetworks.take(net->ssid());
if (frontendNet) {
emit this->networkRemoved(frontendNet);
frontendNet->deleteLater();
}
}
void NMWirelessDevice::removeNetwork() {
auto* net = qobject_cast<NMWirelessNetwork*>(this->sender());
if (this->mNetworks.take(net->ssid())) {
this->removeFrontendNetwork(net);
delete net;
};
}
bool NMWirelessDevice::isValid() const {
return this->NMDevice::isValid() && (this->wirelessProxy && this->wirelessProxy->isValid());
}
} // namespace qs::network
namespace qs::dbus {
DBusResult<qs::network::NMWirelessCapabilities::Enum>
DBusDataTransform<qs::network::NMWirelessCapabilities::Enum>::fromWire(quint32 wire) {
return DBusResult(static_cast<qs::network::NMWirelessCapabilities::Enum>(wire));
}
DBusResult<QDateTime> DBusDataTransform<QDateTime>::fromWire(qint64 wire) {
return DBusResult(qs::network::clockBootTimeToDateTime(wire));
}
} // namespace qs::dbus

166
src/network/nm/wireless.hpp Normal file
View file

@ -0,0 +1,166 @@
#pragma once
#include <qdbusextratypes.h>
#include <qhash.h>
#include <qobject.h>
#include <qproperty.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include "../wifi.hpp"
#include "accesspoint.hpp"
#include "connection.hpp"
#include "dbus_nm_wireless.h"
#include "device.hpp"
#include "enums.hpp"
namespace qs::dbus {
template <>
struct DBusDataTransform<qs::network::NMWirelessCapabilities::Enum> {
using Wire = quint32;
using Data = qs::network::NMWirelessCapabilities::Enum;
static DBusResult<Data> fromWire(Wire wire);
};
template <>
struct DBusDataTransform<QDateTime> {
using Wire = qint64;
using Data = QDateTime;
static DBusResult<Data> fromWire(Wire wire);
};
} // namespace qs::dbus
namespace qs::network {
// NMWirelessNetwork aggregates all related NMActiveConnection, NMAccessPoint, and NMConnectionSetting objects.
class NMWirelessNetwork: public QObject {
Q_OBJECT;
public:
explicit NMWirelessNetwork(QString ssid, QObject* parent = nullptr);
void addAccessPoint(NMAccessPoint* ap);
void addConnection(NMConnectionSettings* conn);
void addActiveConnection(NMActiveConnection* active);
void forget();
[[nodiscard]] QString ssid() const { return this->mSsid; };
[[nodiscard]] quint8 signalStrength() const { return this->bSignalStrength; };
[[nodiscard]] WifiSecurityType::Enum security() const { return this->bSecurity; };
[[nodiscard]] NMConnectionState::Enum state() const { return this->bState; };
[[nodiscard]] bool known() const { return this->bKnown; };
[[nodiscard]] NMConnectionStateReason::Enum reason() const { return this->bReason; };
[[nodiscard]] NMAccessPoint* referenceAp() const { return this->mReferenceAp; };
[[nodiscard]] NMConnectionSettings* referenceConnection() const { return this->mReferenceConn; };
[[nodiscard]] QList<NMAccessPoint*> accessPoints() const { return this->mAccessPoints.values(); };
[[nodiscard]] QList<NMConnectionSettings*> connections() const {
return this->mConnections.values();
}
[[nodiscard]] QBindable<QString> bindableActiveApPath() { return &this->bActiveApPath; };
[[nodiscard]] QBindable<bool> bindableVisible() { return &this->bVisible; };
[[nodiscard]] bool visible() const { return this->bVisible; };
signals:
void disappeared();
void visibilityChanged(bool visible);
void signalStrengthChanged(quint8 signal);
void stateChanged(NMConnectionState::Enum state);
void knownChanged(bool known);
void securityChanged(WifiSecurityType::Enum security);
void reasonChanged(NMConnectionStateReason::Enum reason);
void capabilitiesChanged(NMWirelessCapabilities::Enum caps);
void activeApPathChanged(QString path);
private:
void updateReferenceAp();
void updateReferenceConnection();
QString mSsid;
QHash<QString, NMAccessPoint*> mAccessPoints;
QHash<QString, NMConnectionSettings*> mConnections;
NMAccessPoint* mReferenceAp = nullptr;
NMConnectionSettings* mReferenceConn = nullptr;
NMActiveConnection* mActiveConnection = nullptr;
// clang-format off
Q_OBJECT_BINDABLE_PROPERTY(NMWirelessNetwork, bool, bVisible, &NMWirelessNetwork::visibilityChanged);
Q_OBJECT_BINDABLE_PROPERTY(NMWirelessNetwork, bool, bKnown, &NMWirelessNetwork::knownChanged);
Q_OBJECT_BINDABLE_PROPERTY(NMWirelessNetwork, WifiSecurityType::Enum, bSecurity, &NMWirelessNetwork::securityChanged);
Q_OBJECT_BINDABLE_PROPERTY(NMWirelessNetwork, NMConnectionStateReason::Enum, bReason, &NMWirelessNetwork::reasonChanged);
Q_OBJECT_BINDABLE_PROPERTY(NMWirelessNetwork, NMConnectionState::Enum, bState, &NMWirelessNetwork::stateChanged);
Q_OBJECT_BINDABLE_PROPERTY(NMWirelessNetwork, quint8, bSignalStrength, &NMWirelessNetwork::signalStrengthChanged);
Q_OBJECT_BINDABLE_PROPERTY(NMWirelessNetwork, QString, bActiveApPath, &NMWirelessNetwork::activeApPathChanged);
// clang-format on
};
// Proxy of a /org/freedesktop/NetworkManager/Device/* object.
// Extends NMDevice to also include members from the org.freedesktop.NetworkManager.Device.Wireless interface
// Owns the lifetime of NMAccessPoints(s), NMWirelessNetwork(s), frontend WifiNetwork(s).
class NMWirelessDevice: public NMDevice {
Q_OBJECT;
public:
explicit NMWirelessDevice(const QString& path, QObject* parent = nullptr);
[[nodiscard]] bool isValid() const override;
[[nodiscard]] NMWirelessCapabilities::Enum capabilities() { return this->bCapabilities; };
[[nodiscard]] const QDBusObjectPath& activeApPath() { return this->bActiveAccessPoint; };
[[nodiscard]] NM80211Mode::Enum mode() { return this->bMode; };
[[nodiscard]] QBindable<bool> bindableScanning() { return &this->bScanning; };
signals:
void accessPointLoaded(NMAccessPoint* ap);
void accessPointRemoved(NMAccessPoint* ap);
void networkAdded(WifiNetwork* net);
void networkRemoved(WifiNetwork* net);
void lastScanChanged(QDateTime lastScan);
void scanningChanged(bool scanning);
void capabilitiesChanged(NMWirelessCapabilities::Enum caps);
void activeAccessPointChanged(const QDBusObjectPath& path);
void modeChanged(NM80211Mode::Enum mode);
private slots:
void onAccessPointAdded(const QDBusObjectPath& path);
void onAccessPointRemoved(const QDBusObjectPath& path);
void onAccessPointLoaded(NMAccessPoint* ap);
void onConnectionLoaded(NMConnectionSettings* conn);
void onActiveConnectionLoaded(NMActiveConnection* active);
void onScanTimeout();
void onScanningChanged(bool scanning);
private:
void registerAccessPoint(const QString& path);
void registerFrontendNetwork(NMWirelessNetwork* net);
void removeFrontendNetwork(NMWirelessNetwork* net);
void removeNetwork();
bool checkVisibility(WifiNetwork* net);
void registerAccessPoints();
void initWireless();
NMWirelessNetwork* registerNetwork(const QString& ssid);
QHash<QString, NMAccessPoint*> mAccessPoints;
QHash<QString, NMWirelessNetwork*> mNetworks;
QHash<QString, WifiNetwork*> mFrontendNetworks;
QDateTime mLastScanRequest;
QTimer mScanTimer;
qint32 mScanIntervalMs = 10001;
// clang-format off
Q_OBJECT_BINDABLE_PROPERTY(NMWirelessDevice, bool, bScanning, &NMWirelessDevice::scanningChanged);
Q_OBJECT_BINDABLE_PROPERTY(NMWirelessDevice, QDateTime, bLastScan, &NMWirelessDevice::lastScanChanged);
Q_OBJECT_BINDABLE_PROPERTY(NMWirelessDevice, NMWirelessCapabilities::Enum, bCapabilities, &NMWirelessDevice::capabilitiesChanged);
Q_OBJECT_BINDABLE_PROPERTY(NMWirelessDevice, QDBusObjectPath, bActiveAccessPoint, &NMWirelessDevice::activeAccessPointChanged);
Q_OBJECT_BINDABLE_PROPERTY(NMWirelessDevice, NM80211Mode::Enum, bMode, &NMWirelessDevice::modeChanged);
QS_DBUS_BINDABLE_PROPERTY_GROUP(NMWireless, wirelessProperties);
QS_DBUS_PROPERTY_BINDING(NMWirelessDevice, pLastScan, bLastScan, wirelessProperties, "LastScan");
QS_DBUS_PROPERTY_BINDING(NMWirelessDevice, pCapabilities, bCapabilities, wirelessProperties, "WirelessCapabilities");
QS_DBUS_PROPERTY_BINDING(NMWirelessDevice, pActiveAccessPoint, bActiveAccessPoint, wirelessProperties, "ActiveAccessPoint");
QS_DBUS_PROPERTY_BINDING(NMWirelessDevice, pMode, bMode, wirelessProperties, "Mode");
// clang-format on
DBusNMWirelessProxy* wirelessProxy = nullptr;
};
} // namespace qs::network

View file

@ -0,0 +1,155 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import Quickshell.Networking
FloatingWindow {
color: contentItem.palette.window
ColumnLayout {
anchors.fill: parent
anchors.margins: 5
Column {
Layout.fillWidth: true
RowLayout {
Label {
text: "WiFi"
font.bold: true
font.pointSize: 12
}
CheckBox {
text: "Software"
checked: Networking.wifiEnabled
onClicked: Networking.wifiEnabled = !Networking.wifiEnabled
}
CheckBox {
enabled: false
text: "Hardware"
checked: Networking.wifiHardwareEnabled
}
}
}
ListView {
clip: true
Layout.fillWidth: true
Layout.fillHeight: true
model: Networking.devices
delegate: WrapperRectangle {
width: parent.width
color: "transparent"
border.color: palette.button
border.width: 1
margin: 5
ColumnLayout {
RowLayout {
Label { text: modelData.name; font.bold: true }
Label { text: modelData.address }
Label { text: `(Type: ${DeviceType.toString(modelData.type)})` }
}
RowLayout {
Label {
text: DeviceConnectionState.toString(modelData.state)
color: modelData.connected ? palette.link : palette.placeholderText
}
Label {
visible: Networking.backend == NetworkBackendType.NetworkManager && (modelData.state == DeviceConnectionState.Connecting || modelData.state == DeviceConnectionState.Disconnecting)
text: `(${NMDeviceState.toString(modelData.nmState)})`
}
Button {
visible: modelData.state == DeviceConnectionState.Connected
text: "Disconnect"
onClicked: modelData.disconnect()
}
CheckBox {
text: "Autoconnect"
checked: modelData.autoconnect
onClicked: modelData.autoconnect = !modelData.autoconnect
}
Label {
text: `Mode: ${WifiDeviceMode.toString(modelData.mode)}`
visible: modelData.type == DeviceType.Wifi
}
CheckBox {
text: "Scanner"
checked: modelData.scannerEnabled
onClicked: modelData.scannerEnabled = !modelData.scannerEnabled
visible: modelData.type === DeviceType.Wifi
}
}
Repeater {
Layout.fillWidth: true
model: {
if (modelData.type !== DeviceType.Wifi) return []
return [...modelData.networks.values].sort((a, b) => {
if (a.connected !== b.connected) {
return b.connected - a.connected
}
return b.signalStrength - a.signalStrength
})
}
WrapperRectangle {
Layout.fillWidth: true
color: modelData.connected ? palette.highlight : palette.button
border.color: palette.mid
border.width: 1
margin: 5
RowLayout {
ColumnLayout {
Layout.fillWidth: true
RowLayout {
Label { text: modelData.name; font.bold: true }
Label {
text: modelData.known ? "Known" : ""
color: palette.placeholderText
}
}
RowLayout {
Label {
text: `Security: ${WifiSecurityType.toString(modelData.security)}`
color: palette.placeholderText
}
Label {
text: `| Signal strength: ${Math.round(modelData.signalStrength*100)}%`
color: palette.placeholderText
}
}
Label {
visible: Networking.backend == NetworkBackendType.NetworkManager && (modelData.nmReason != NMConnectionStateReason.Unknown && modelData.nmReason != NMConnectionStateReason.None)
text: `Connection change reason: ${NMConnectionStateReason.toString(modelData.nmReason)}`
}
}
RowLayout {
Layout.alignment: Qt.AlignRight
Button {
text: "Connect"
onClicked: modelData.connect()
visible: !modelData.connected
}
Button {
text: "Disconnect"
onClicked: modelData.disconnect()
visible: modelData.connected
}
Button {
text: "Forget"
onClicked: modelData.forget()
visible: modelData.known
}
}
}
}
}
}
}
}
}
}

139
src/network/wifi.cpp Normal file
View file

@ -0,0 +1,139 @@
#include "wifi.hpp"
#include <utility>
#include <qdebug.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qobject.h>
#include <qstring.h>
#include <qstringliteral.h>
#include "../core/logcat.hpp"
#include "device.hpp"
#include "network.hpp"
namespace qs::network {
namespace {
QS_LOGGING_CATEGORY(logWifi, "quickshell.network.wifi", QtWarningMsg);
} // namespace
QString WifiSecurityType::toString(WifiSecurityType::Enum type) {
switch (type) {
case Unknown: return QStringLiteral("Unknown");
case Wpa3SuiteB192: return QStringLiteral("WPA3 Suite B 192-bit");
case Sae: return QStringLiteral("WPA3");
case Wpa2Eap: return QStringLiteral("WPA2 Enterprise");
case Wpa2Psk: return QStringLiteral("WPA2");
case WpaEap: return QStringLiteral("WPA Enterprise");
case WpaPsk: return QStringLiteral("WPA");
case StaticWep: return QStringLiteral("WEP");
case DynamicWep: return QStringLiteral("Dynamic WEP");
case Leap: return QStringLiteral("LEAP");
case Owe: return QStringLiteral("OWE");
case Open: return QStringLiteral("Open");
default: return QStringLiteral("Unknown");
}
}
QString WifiDeviceMode::toString(WifiDeviceMode::Enum mode) {
switch (mode) {
case Unknown: return QStringLiteral("Unknown");
case AdHoc: return QStringLiteral("Ad-Hoc");
case Station: return QStringLiteral("Station");
case AccessPoint: return QStringLiteral("Access Point");
case Mesh: return QStringLiteral("Mesh");
default: return QStringLiteral("Unknown");
};
}
QString NMConnectionStateReason::toString(NMConnectionStateReason::Enum reason) {
switch (reason) {
case Unknown: return QStringLiteral("Unknown");
case None: return QStringLiteral("No reason");
case UserDisconnected: return QStringLiteral("User disconnection");
case DeviceDisconnected:
return QStringLiteral("The device the connection was using was disconnected.");
case ServiceStopped:
return QStringLiteral("The service providing the VPN connection was stopped.");
case IpConfigInvalid:
return QStringLiteral("The IP config of the active connection was invalid.");
case ConnectTimeout:
return QStringLiteral("The connection attempt to the VPN service timed out.");
case ServiceStartTimeout:
return QStringLiteral(
"A timeout occurred while starting the service providing the VPN connection."
);
case ServiceStartFailed:
return QStringLiteral("Starting the service providing the VPN connection failed.");
case NoSecrets: return QStringLiteral("Necessary secrets for the connection were not provided.");
case LoginFailed: return QStringLiteral("Authentication to the server failed.");
case ConnectionRemoved:
return QStringLiteral("Necessary secrets for the connection were not provided.");
case DependencyFailed:
return QStringLiteral("Master connection of this connection failed to activate.");
case DeviceRealizeFailed: return QStringLiteral("Could not create the software device link.");
case DeviceRemoved: return QStringLiteral("The device this connection depended on disappeared.");
default: return QStringLiteral("Unknown");
};
};
WifiNetwork::WifiNetwork(QString ssid, QObject* parent): Network(std::move(ssid), parent) {};
void WifiNetwork::connect() {
if (this->bConnected) {
qCCritical(logWifi) << this << "is already connected.";
return;
}
this->requestConnect();
}
void WifiNetwork::disconnect() {
if (!this->bConnected) {
qCCritical(logWifi) << this << "is not currently connected";
return;
}
this->requestDisconnect();
}
void WifiNetwork::forget() { this->requestForget(); }
WifiDevice::WifiDevice(QObject* parent): NetworkDevice(DeviceType::Wifi, parent) {};
void WifiDevice::setScannerEnabled(bool enabled) {
if (this->bScannerEnabled == enabled) return;
this->bScannerEnabled = enabled;
}
void WifiDevice::networkAdded(WifiNetwork* net) { this->mNetworks.insertObject(net); }
void WifiDevice::networkRemoved(WifiNetwork* net) { this->mNetworks.removeObject(net); }
} // namespace qs::network
QDebug operator<<(QDebug debug, const qs::network::WifiNetwork* network) {
auto saver = QDebugStateSaver(debug);
if (network) {
debug.nospace() << "WifiNetwork(" << static_cast<const void*>(network)
<< ", name=" << network->name() << ")";
} else {
debug << "WifiNetwork(nullptr)";
}
return debug;
}
QDebug operator<<(QDebug debug, const qs::network::WifiDevice* device) {
auto saver = QDebugStateSaver(debug);
if (device) {
debug.nospace() << "WifiDevice(" << static_cast<const void*>(device)
<< ", name=" << device->name() << ")";
} else {
debug << "WifiDevice(nullptr)";
}
return debug;
}

186
src/network/wifi.hpp Normal file
View file

@ -0,0 +1,186 @@
#pragma once
#include <qobject.h>
#include <qproperty.h>
#include <qqmlintegration.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include "../core/model.hpp"
#include "device.hpp"
#include "network.hpp"
namespace qs::network {
///! The security type of a wifi network.
class WifiSecurityType: 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,
Open = 10,
Unknown = 11,
};
Q_ENUM(Enum);
Q_INVOKABLE static QString toString(WifiSecurityType::Enum type);
};
///! The 802.11 mode of a wifi device.
class WifiDeviceMode: public QObject {
Q_OBJECT;
QML_ELEMENT;
QML_SINGLETON;
public:
enum Enum : quint8 {
/// The device is part of an Ad-Hoc network without a central access point.
AdHoc = 0,
/// The device is a station that can connect to networks.
Station = 1,
/// The device is a local hotspot/access point.
AccessPoint = 2,
/// The device is an 802.11s mesh point.
Mesh = 3,
/// The device mode is unknown.
Unknown = 4,
};
Q_ENUM(Enum);
Q_INVOKABLE static QString toString(WifiDeviceMode::Enum mode);
};
///! NetworkManager-specific reason for a WifiNetworks connection state.
/// In sync with https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMActiveConnectionStateReason.
class NMConnectionStateReason: public QObject {
Q_OBJECT;
QML_ELEMENT;
QML_SINGLETON;
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(NMConnectionStateReason::Enum reason);
};
///! An available wifi network.
class WifiNetwork: public Network {
Q_OBJECT;
QML_ELEMENT;
QML_UNCREATABLE("WifiNetwork can only be acquired through WifiDevice");
// clang-format off
/// The current signal strength of the network, from 0.0 to 1.0.
Q_PROPERTY(qreal signalStrength READ default NOTIFY signalStrengthChanged BINDABLE bindableSignalStrength);
/// True if the wifi network has known connection settings saved.
Q_PROPERTY(bool known READ default NOTIFY knownChanged BINDABLE bindableKnown);
/// The security type of the wifi network.
Q_PROPERTY(WifiSecurityType::Enum security READ default NOTIFY securityChanged BINDABLE bindableSecurity);
/// A specific reason for the connection state when the backend is NetworkManager.
Q_PROPERTY(NMConnectionStateReason::Enum nmReason READ default NOTIFY nmReasonChanged BINDABLE bindableNmReason);
// clang-format on
public:
explicit WifiNetwork(QString ssid, QObject* parent = nullptr);
/// Attempt to connect to the wifi network.
///
/// > [!WARNING] Quickshell does not yet provide a NetworkManager authentication agent,
/// > meaning another agent will need to be active to enter passwords for unsaved networks.
Q_INVOKABLE void connect();
/// Disconnect from the wifi network.
Q_INVOKABLE void disconnect();
/// Forget all connection settings for this wifi network.
Q_INVOKABLE void forget();
QBindable<qreal> bindableSignalStrength() { return &this->bSignalStrength; }
QBindable<bool> bindableKnown() { return &this->bKnown; }
QBindable<NMConnectionStateReason::Enum> bindableNmReason() { return &this->bNmReason; }
QBindable<WifiSecurityType::Enum> bindableSecurity() { return &this->bSecurity; }
signals:
void requestConnect();
void requestDisconnect();
void requestForget();
void signalStrengthChanged();
void knownChanged();
void securityChanged();
void nmReasonChanged();
private:
// clang-format off
Q_OBJECT_BINDABLE_PROPERTY(WifiNetwork, qreal, bSignalStrength, &WifiNetwork::signalStrengthChanged);
Q_OBJECT_BINDABLE_PROPERTY(WifiNetwork, bool, bKnown, &WifiNetwork::knownChanged);
Q_OBJECT_BINDABLE_PROPERTY(WifiNetwork, NMConnectionStateReason::Enum, bNmReason, &WifiNetwork::nmReasonChanged);
Q_OBJECT_BINDABLE_PROPERTY(WifiNetwork, WifiSecurityType::Enum, bSecurity, &WifiNetwork::securityChanged);
// clang-format on
};
///! Wireless variant of a NetworkDevice.
class WifiDevice: public NetworkDevice {
Q_OBJECT;
QML_ELEMENT;
QML_UNCREATABLE("");
// clang-format off
/// A list of this available and connected wifi networks.
QSDOC_TYPE_OVERRIDE(ObjectModel<WifiNetwork>*);
Q_PROPERTY(UntypedObjectModel* networks READ networks CONSTANT);
/// True when currently scanning for networks.
/// When enabled, the scanner populates the device with an active list of available wifi networks.
Q_PROPERTY(bool scannerEnabled READ scannerEnabled WRITE setScannerEnabled NOTIFY scannerEnabledChanged BINDABLE bindableScannerEnabled);
/// The 802.11 mode the device is in.
Q_PROPERTY(WifiDeviceMode::Enum mode READ default NOTIFY modeChanged BINDABLE bindableMode);
// clang-format on
public:
explicit WifiDevice(QObject* parent = nullptr);
void networkAdded(WifiNetwork* net);
void networkRemoved(WifiNetwork* net);
[[nodiscard]] ObjectModel<WifiNetwork>* networks() { return &this->mNetworks; };
QBindable<bool> bindableScannerEnabled() { return &this->bScannerEnabled; };
[[nodiscard]] bool scannerEnabled() const { return this->bScannerEnabled; };
void setScannerEnabled(bool enabled);
QBindable<WifiDeviceMode::Enum> bindableMode() { return &this->bMode; }
signals:
void modeChanged();
void scannerEnabledChanged(bool enabled);
private:
ObjectModel<WifiNetwork> mNetworks {this};
Q_OBJECT_BINDABLE_PROPERTY(WifiDevice, bool, bScannerEnabled, &WifiDevice::scannerEnabledChanged);
Q_OBJECT_BINDABLE_PROPERTY(WifiDevice, WifiDeviceMode::Enum, bMode, &WifiDevice::modeChanged);
};
}; // namespace qs::network
QDebug operator<<(QDebug debug, const qs::network::WifiNetwork* network);
QDebug operator<<(QDebug debug, const qs::network::WifiDevice* device);