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

634 lines
25 KiB
C++

/*
Copyright 2013-2018 Jan Grulich <jgrulich@redhat.com>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) version 3, or any
later version accepted by the membership of KDE e.V. (or its
successor approved by the membership of KDE e.V.), which shall
act as a proxy defined in Section 6 of version 3 of the license.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library. If not, see <http://www.gnu.org/licenses/>.
*/
#include "networkmodel.h"
#include "networkclient.h"
#include "vpnutils.h"
#include "wifiutils.h"
#include <QHostAddress>
#include <QSet>
#include <QtEndian>
#include <cstring>
using namespace NetworkDBus;
namespace {
const QString connectionSetting = QStringLiteral("connection");
const QString wirelessSetting = QStringLiteral("802-11-wireless");
const QString wirelessSecurity = QStringLiteral("802-11-wireless-security");
// How networks are told apart: by what joining one asks for, not by the exact scheme, so
// a router whose bands differ only in WPA2 and WPA3 is still one network.
enum class SecurityClass {
Open,
Wep,
Personal,
Enterprise,
};
SecurityClass classOf(Enums::SecurityType type)
{
switch (type) {
case Enums::StaticWep:
case Enums::DynamicWep:
case Enums::Leap:
return SecurityClass::Wep;
case Enums::WpaPsk:
case Enums::Wpa2Psk:
case Enums::SAE:
return SecurityClass::Personal;
case Enums::WpaEap:
case Enums::Wpa2Eap:
case Enums::Wpa3SuiteB192:
return SecurityClass::Enterprise;
default:
return SecurityClass::Open;
}
}
SecurityClass profileClass(const NetworkClient *client, const QString &profile)
{
if (!client->settings(profile).contains(wirelessSecurity))
return SecurityClass::Open;
const QString keyMgmt = client->setting(profile, wirelessSecurity, QStringLiteral("key-mgmt")).toString();
if (keyMgmt == QLatin1String("none") || keyMgmt == QLatin1String("ieee8021x"))
return SecurityClass::Wep;
if (keyMgmt == QLatin1String("wpa-eap") || keyMgmt == QLatin1String("wpa-eap-suite-b-192"))
return SecurityClass::Enterprise;
if (keyMgmt == QLatin1String("owe"))
return SecurityClass::Open;
return SecurityClass::Personal;
}
bool isPort(const NetworkClient *client, const QString &profile)
{
return !client->setting(profile, connectionSetting, QStringLiteral("controller")).toString().isEmpty()
|| !client->setting(profile, connectionSetting, QStringLiteral("master")).toString().isEmpty();
}
QDateTime lastUsed(const NetworkClient *client, const QString &profile)
{
const qulonglong seconds = client->setting(profile, connectionSetting, QStringLiteral("timestamp")).toULongLong();
return seconds ? QDateTime::fromSecsSinceEpoch(qint64(seconds)) : QDateTime();
}
QString profileOf(const NetworkClient *client, const QString &active)
{
return NetworkClient::path(client->property(active, activeInterface, QStringLiteral("Connection")));
}
// The profiles NetworkManager would activate on the device, and the one it has active.
QStringList profilesFor(const NetworkClient *client, const QString &device, const QString &type)
{
QStringList profiles;
QStringList candidates = client->property(device, deviceInterface, QStringLiteral("AvailableConnections")).toStringList();
const QString active = client->activeConnection(device);
if (!active.isEmpty())
candidates << profileOf(client, active);
for (const QString &profile : std::as_const(candidates)) {
if (profiles.contains(profile) || !client->readable(profile) || isPort(client, profile)
|| client->setting(profile, connectionSetting, QStringLiteral("type")).toString() != type)
continue;
profiles << profile;
}
return profiles;
}
QString firstAddress(const QVariantList &addressData, bool skipLinkLocal)
{
QString fallback;
for (const QVariant &entry : addressData) {
const QString address = entry.toMap().value(QStringLiteral("address")).toString();
if (!skipLinkLocal || !QHostAddress(address).isLinkLocal())
return address;
if (fallback.isEmpty())
fallback = address;
}
return fallback;
}
} // namespace
NetworkModel::NetworkModel(QObject *parent)
: QAbstractListModel(parent)
, m_client(NetworkClient::instance())
{
connect(m_client, &NetworkClient::changed, this, &NetworkModel::rebuild);
connect(m_client, &NetworkClient::activeStateChanged, this,
[this](const QString &active, uint state, uint reason) {
const QString profile = profileOf(m_client, active);
if (!VpnUtils::isVpn(m_client, profile))
return;
if (state == ActiveActivating) {
m_vpnFailures.remove(profile);
} else if (state == ActiveDeactivated) {
const QString message = VpnUtils::failureMessage(reason);
if (!message.isEmpty())
m_vpnFailures.insert(profile, message);
}
});
rebuild();
}
int NetworkModel::rowCount(const QModelIndex &parent) const
{
return parent.isValid() ? 0 : m_items.size();
}
QVariant NetworkModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || index.row() < 0 || index.row() >= m_items.size())
return {};
return itemData(m_items.at(index.row()), role);
}
QVariant NetworkModel::itemData(const Item &item, int role) const
{
switch (role) {
case ConnectionIconRole:
return item.icon;
case ConnectionPathRole:
return item.connectionPath;
case ConnectionStateRole:
return item.connectionState;
case DeviceName:
return item.deviceName;
case DevicePathRole:
return item.devicePath;
case DeviceStateRole:
return item.deviceState;
case DuplicateRole:
case SlaveRole:
return false;
case ItemUniqueNameRole:
return item.uniqueName;
case ItemTypeRole:
return item.itemType;
case NameRole:
return item.name;
case SecurityTypeRole:
return item.securityType;
case SecurityTypeStringRole:
return WifiUtils::securityLabel(Enums::SecurityType(item.securityType));
case SignalRole:
return item.signal;
case SsidRole:
return item.ssid;
case SpecificPathRole:
return item.specificPath;
case TimeStampRole:
return item.timestamp;
case TypeRole:
return item.type;
case UniRole:
return item.connectionPath;
case UuidRole:
return item.uuid;
case IpAddressRole:
return item.ipV4Address;
case IpV6AddressRole:
return item.ipV6Address;
case GateWayRole:
return item.gateway;
case NameServerRole:
return item.nameServer;
case MacAddressRole:
return item.macAddress;
case VpnType:
return item.vpnType;
case StatusMessageRole:
return item.statusMessage;
case CanActivateRole:
return item.canActivate;
case VpnUsesUserNameRole:
return item.vpnUsesUserName;
case VpnUserNameRole:
return item.vpnUserName;
case RxBytesRole:
case TxBytesRole:
case VpnState:
return 0;
case ConnectionDetailsRole:
return QStringList();
case LastUsedRole:
case LastUsedDateOnlyRole:
case SectionRole:
return QString();
default:
return {};
}
}
QHash<int, QByteArray> NetworkModel::roleNames() const
{
QHash<int, QByteArray> roles = QAbstractListModel::roleNames();
roles[ConnectionDetailsRole] = "connectionDetails";
roles[ConnectionIconRole] = "connectionIcon";
roles[ConnectionPathRole] = "connectionPath";
roles[ConnectionStateRole] = "connectionState";
roles[DeviceName] = "deviceName";
roles[DevicePathRole] = "devicePath";
roles[DeviceStateRole] = "deviceState";
roles[DuplicateRole] = "duplicate";
roles[ItemUniqueNameRole] = "itemUniqueName";
roles[ItemTypeRole] = "itemType";
roles[LastUsedRole] = "lastUsed";
roles[LastUsedDateOnlyRole] = "lastUsedDateOnly";
roles[NameRole] = "name";
roles[SectionRole] = "section";
roles[SignalRole] = "signal";
roles[SlaveRole] = "slave";
roles[SsidRole] = "ssid";
roles[SpecificPathRole] = "specificPath";
roles[SecurityTypeRole] = "securityType";
roles[SecurityTypeStringRole] = "securityTypeString";
roles[TimeStampRole] = "timeStamp";
roles[TypeRole] = "type";
roles[UniRole] = "uni";
roles[UuidRole] = "uuid";
roles[VpnState] = "vpnState";
roles[VpnType] = "vpnType";
roles[RxBytesRole] = "rxBytes";
roles[TxBytesRole] = "txBytes";
roles[IpAddressRole] = "ipAddress";
roles[IpV6AddressRole] = "ipV6Address";
roles[GateWayRole] = "gateWay";
roles[NameServerRole] = "nameServer";
roles[MacAddressRole] = "macAddress";
roles[StatusMessageRole] = "statusMessage";
roles[CanActivateRole] = "canActivate";
roles[VpnUsesUserNameRole] = "vpnUsesUserName";
roles[VpnUserNameRole] = "vpnUserName";
return roles;
}
void NetworkModel::rebuild()
{
QList<Item> items;
if (m_client->ready()) {
for (const QString &device : m_client->devices()) {
if (!m_client->property(device, deviceInterface, QStringLiteral("Managed")).toBool())
continue;
if (m_client->deviceType(device) == DeviceEthernet)
addWired(device, items);
else if (m_client->deviceType(device) == DeviceWifi)
addWireless(device, items);
}
addVpns(items);
}
// A name shown twice, say on two adapters, gets its device added.
QHash<QString, int> names;
for (const Item &item : std::as_const(items))
++names[item.name];
for (Item &item : items) {
item.uniqueName = names.value(item.name) > 1 && !item.deviceName.isEmpty()
? QStringLiteral("%1 (%2)").arg(item.name, item.deviceName)
: item.name;
fillDetails(item);
}
QHash<QString, int> wanted;
for (int i = 0; i < items.size(); ++i)
wanted.insert(items.at(i).key, i);
for (int row = m_items.size() - 1; row >= 0; --row) {
if (wanted.contains(m_items.at(row).key))
continue;
beginRemoveRows(QModelIndex(), row, row);
m_items.removeAt(row);
endRemoveRows();
}
const QList<int> roles = roleNames().keys();
QSet<QString> present;
for (int row = 0; row < m_items.size(); ++row) {
const Item &next = items.at(wanted.value(m_items.at(row).key));
present.insert(next.key);
QList<int> changed;
for (const int role : roles) {
if (itemData(m_items.at(row), role) != itemData(next, role))
changed << role;
}
if (changed.isEmpty())
continue;
m_items[row] = next;
const QModelIndex index = this->index(row);
Q_EMIT dataChanged(index, index, changed);
}
for (const Item &item : std::as_const(items)) {
if (present.contains(item.key))
continue;
beginInsertRows(QModelIndex(), m_items.size(), m_items.size());
m_items.append(item);
endInsertRows();
}
}
void NetworkModel::addWired(const QString &device, QList<Item> &items) const
{
const QString active = m_client->activeConnection(device);
const QString activeProfile = profileOf(m_client, active);
for (const QString &profile : profilesFor(m_client, device, QStringLiteral("802-3-ethernet"))) {
Item item;
item.type = Enums::Wired;
item.itemType = AvailableConnection;
item.uuid = m_client->setting(profile, connectionSetting, QStringLiteral("uuid")).toString();
item.key = device + QLatin1Char('|') + item.uuid;
item.name = m_client->setting(profile, connectionSetting, QStringLiteral("id")).toString();
item.connectionPath = profile;
item.devicePath = device;
item.deviceName = m_client->property(device, deviceInterface, QStringLiteral("Interface")).toString();
item.deviceState = m_client->deviceState(device);
item.securityType = Enums::NoneSecurity;
item.timestamp = lastUsed(m_client, profile);
if (profile == activeProfile)
item.connectionState = m_client->property(active, activeInterface, QStringLiteral("State")).toInt();
item.icon = item.connectionState == Activated ? QStringLiteral("network-wired-activated")
: QStringLiteral("network-wired");
items << item;
}
}
void NetworkModel::addWireless(const QString &device, QList<Item> &items) const
{
struct Network {
QByteArray ssid;
SecurityClass securityClass = SecurityClass::Open;
Enums::SecurityType security = Enums::UnknownSecurity;
QString accessPoint;
int strength = -1;
QString profile;
QDateTime timestamp;
};
const auto ap = [this](const QString &path, const char *name) {
return m_client->property(path, accessPointInterface, QLatin1String(name));
};
const QString deviceName = m_client->property(device, deviceInterface, QStringLiteral("Interface")).toString();
const QString hwAddress = m_client->property(device, deviceInterface, QStringLiteral("HwAddress")).toString();
const uint capabilities = m_client->property(device, wirelessInterface, QStringLiteral("WirelessCapabilities")).toUInt();
const QString activeAp = NetworkClient::path(m_client->property(device, wirelessInterface, QStringLiteral("ActiveAccessPoint")));
const QString active = m_client->activeConnection(device);
const QString activeProfile = profileOf(m_client, active);
const int activeState = active.isEmpty() ? int(Deactivated)
: m_client->property(active, activeInterface, QStringLiteral("State")).toInt();
QList<Network> networks;
int activeNetwork = -1;
for (const QString &path : m_client->accessPoints(device)) {
const QByteArray ssid = WifiUtils::ssidKey(ap(path, "Ssid").toByteArray());
const uint rsn = ap(path, "RsnFlags").toUInt();
// Hidden networks have no name to show; our own hotspot shows up as an access point
// with this adapter's address; and an OWE transition network's encrypted half is
// joined through its open one.
if (WifiUtils::ssidHidden(ssid) || ap(path, "Mode").toUInt() != ModeInfrastructure
|| (!hwAddress.isEmpty() && ap(path, "HwAddress").toString().compare(hwAddress, Qt::CaseInsensitive) == 0)
|| ((rsn & KeyMgmtOwe) && (rsn & KeyMgmtOweTm)))
continue;
const Enums::SecurityType security = WifiUtils::security(capabilities, ap(path, "Flags").toUInt(),
ap(path, "WpaFlags").toUInt(), rsn,
ModeInfrastructure);
const SecurityClass securityClass = classOf(security);
const int strength = ap(path, "Strength").toInt();
int index = -1;
for (int i = 0; i < networks.size(); ++i) {
if (networks.at(i).ssid == ssid && networks.at(i).securityClass == securityClass) {
index = i;
break;
}
}
if (index < 0) {
networks.append({ssid, securityClass});
index = networks.size() - 1;
}
// The access point in use stands for its network; otherwise the strongest does.
Network &network = networks[index];
if (path == activeAp) {
activeNetwork = index;
} else if (activeNetwork == index || strength <= network.strength) {
continue;
}
network.accessPoint = path;
network.strength = strength;
network.security = security;
}
bool activeUnlisted = false;
for (const QString &profile : profilesFor(m_client, device, wirelessSetting)) {
const QString mode = m_client->setting(profile, wirelessSetting, QStringLiteral("mode")).toString();
if (!mode.isEmpty() && mode != QLatin1String("infrastructure"))
continue;
const QByteArray ssid = WifiUtils::ssidKey(m_client->setting(profile, wirelessSetting, QStringLiteral("ssid")).toByteArray());
const SecurityClass securityClass = profileClass(m_client, profile);
const bool isActive = profile == activeProfile;
int index = isActive ? activeNetwork : -1;
int sameName = -1;
for (int i = 0; index < 0 && i < networks.size(); ++i) {
if (networks.at(i).ssid != ssid)
continue;
if (networks.at(i).securityClass == securityClass)
index = i;
else
sameName = sameName == -1 ? i : -2;
}
// A lone network of that name is the profile's, whatever its scheme says now.
if (index < 0 && sameName >= 0)
index = sameName;
if (index < 0) {
if (isActive)
activeUnlisted = true;
continue;
}
// Several profiles for one network: the active one, else the one used last.
Network &network = networks[index];
const QDateTime timestamp = lastUsed(m_client, profile);
if (!network.profile.isEmpty() && !isActive
&& (network.profile == activeProfile || network.timestamp >= timestamp))
continue;
network.profile = profile;
network.timestamp = timestamp;
}
const auto addItem = [&](const Network &network, const QString &key) {
Item item;
item.type = Enums::Wireless;
item.key = key;
item.ssid = WifiUtils::ssidLabel(network.ssid);
item.devicePath = device;
item.deviceName = deviceName;
item.deviceState = m_client->deviceState(device);
item.specificPath = network.accessPoint;
item.securityType = network.security;
item.signal = qMax(network.strength, 0);
item.icon = WifiUtils::strengthIcon(item.signal);
if (network.profile.isEmpty()) {
item.itemType = AvailableAccessPoint;
item.name = item.ssid;
} else {
item.itemType = AvailableConnection;
item.connectionPath = network.profile;
item.uuid = m_client->setting(network.profile, connectionSetting, QStringLiteral("uuid")).toString();
item.name = m_client->setting(network.profile, connectionSetting, QStringLiteral("id")).toString();
item.timestamp = network.timestamp;
}
// Another user's private profile is still in use here, just not ours to change.
const bool inUse = !active.isEmpty()
&& (network.profile.isEmpty() ? !network.accessPoint.isEmpty() && network.accessPoint == activeAp
: network.profile == activeProfile);
if (inUse)
item.connectionState = activeState;
items << item;
};
for (const Network &network : std::as_const(networks)) {
const char securityClass = '0' + char(network.securityClass);
addItem(network, device + QLatin1Char('|') + QString::fromLatin1(network.ssid.toHex()) + QLatin1Char('|')
+ QLatin1Char(securityClass));
}
// Connected to a network no scan has listed, a hidden one for instance.
if (activeUnlisted) {
Network network;
network.ssid = WifiUtils::ssidKey(m_client->setting(activeProfile, wirelessSetting, QStringLiteral("ssid")).toByteArray());
network.profile = activeProfile;
network.timestamp = lastUsed(m_client, activeProfile);
network.accessPoint = activeAp;
switch (profileClass(m_client, activeProfile)) {
case SecurityClass::Open:
network.security = Enums::NoneSecurity;
break;
case SecurityClass::Wep:
network.security = Enums::StaticWep;
break;
case SecurityClass::Personal:
network.security = Enums::Wpa2Psk;
break;
case SecurityClass::Enterprise:
network.security = Enums::Wpa2Eap;
break;
}
if (!activeAp.isEmpty())
network.strength = ap(activeAp, "Strength").toInt();
addItem(network, device + QLatin1Char('|') + activeProfile);
}
}
void NetworkModel::addVpns(QList<Item> &items) const
{
for (const QString &profile : m_client->connections()) {
if (!VpnUtils::isVpn(m_client, profile))
continue;
Item item;
item.type = Enums::Vpn;
item.itemType = AvailableConnection;
item.uuid = m_client->setting(profile, connectionSetting, QStringLiteral("uuid")).toString();
item.key = QStringLiteral("vpn|") + item.uuid;
item.name = m_client->setting(profile, connectionSetting, QStringLiteral("id")).toString();
item.connectionPath = profile;
item.securityType = Enums::NoneSecurity;
item.timestamp = lastUsed(m_client, profile);
item.connectionState = VpnUtils::state(m_client, VpnUtils::activeConnection(m_client, profile));
item.icon = QStringLiteral("network-vpn");
const QString kind = VpnUtils::kind(m_client, profile);
item.vpnType = VpnUtils::kindLabel(kind);
const QString service = m_client->setting(profile, QStringLiteral("vpn"), QStringLiteral("service-type")).toString();
const bool installed = kind == QLatin1String("wireguard") || VpnUtils::pluginInstalled(service);
const QVariantMap data = m_client->setting(profile, QStringLiteral("vpn"), QStringLiteral("data")).toMap();
QMap<QString, QString> strings;
for (auto it = data.cbegin(); it != data.cend(); ++it)
strings.insert(it.key(), it.value().toString());
const QString userNameKey = VpnUtils::userNameKey(service, strings);
item.vpnUsesUserName = !userNameKey.isEmpty();
item.vpnUserName = strings.value(userNameKey);
if (!installed) {
item.canActivate = false;
item.statusMessage = tr("Install %1 to use this VPN.").arg(VpnUtils::package(kind));
} else if (kind == QLatin1String("openconnect")) {
item.canActivate = false;
item.statusMessage = tr("Signing in to OpenConnect VPNs isn't supported yet.");
} else {
item.statusMessage = m_vpnFailures.value(profile);
}
items << item;
}
}
void NetworkModel::fillDetails(Item &item) const
{
// Addresses are those of the device the network is up on; a VPN's come from its
// active connection, as its row has no device.
if (item.connectionState != Activated)
return;
const QString device = item.devicePath;
const QString vpn = item.type == Enums::Vpn ? VpnUtils::activeConnection(m_client, item.connectionPath) : QString();
const auto config = [&](const QString &name) {
return NetworkClient::path(vpn.isEmpty() ? m_client->property(device, deviceInterface, name)
: m_client->property(vpn, activeInterface, name));
};
const QString ip4 = config(QStringLiteral("Ip4Config"));
const QString ip6 = config(QStringLiteral("Ip6Config"));
item.ipV4Address = firstAddress(m_client->property(ip4, ip4ConfigInterface, QStringLiteral("AddressData")).toList(), false);
item.ipV6Address = firstAddress(m_client->property(ip6, ip6ConfigInterface, QStringLiteral("AddressData")).toList(), true);
item.gateway = m_client->property(ip4, ip4ConfigInterface, QStringLiteral("Gateway")).toString();
if (item.gateway.isEmpty())
item.gateway = m_client->property(ip6, ip6ConfigInterface, QStringLiteral("Gateway")).toString();
const QVariantList nameservers = m_client->property(ip4, ip4ConfigInterface, QStringLiteral("NameserverData")).toList();
item.nameServer = firstAddress(nameservers, false);
if (item.nameServer.isEmpty()) {
// Network byte order, as NetworkManager before 1.14 only offered it.
const QVariantList legacy = m_client->property(ip4, ip4ConfigInterface, QStringLiteral("Nameservers")).toList();
if (!legacy.isEmpty())
item.nameServer = QHostAddress(qFromBigEndian(legacy.first().toUInt())).toString();
}
if (item.nameServer.isEmpty()) {
const QVariantList servers = m_client->property(ip6, ip6ConfigInterface, QStringLiteral("Nameservers")).toList();
if (!servers.isEmpty() && servers.first().toByteArray().size() == 16) {
Q_IPV6ADDR address;
std::memcpy(address.c, servers.first().toByteArray().constData(), 16);
item.nameServer = QHostAddress(address).toString();
}
}
item.macAddress = m_client->property(device, deviceInterface, QStringLiteral("HwAddress")).toString();
}