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/handler.cpp

981 lines
45 KiB
C++

/*
Copyright 2013-2014 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 "handler.h"
#include "configuration.h"
#include "iputils.h"
#include "networkclient.h"
#include "secretstore.h"
#include "vpnschema.h"
#include "vpnutils.h"
#include "wifiutils.h"
#include <QDBusConnection>
#include <QDBusMetaType>
#include <QDBusPendingCallWatcher>
#include <QDebug>
#include <QFile>
#include <QFileInfo>
#include <QProcess>
#include <QRegularExpression>
#include <QTimer>
#include <QUuid>
using namespace NetworkDBus;
// The shell's secret agent, which asks for a new network's password up front.
static const QString promptService = QStringLiteral("com.cutefish.SecretAgent");
static const QString promptPath = QStringLiteral("/SecretAgent");
static const QString promptInterface = QStringLiteral("com.cutefish.SecretAgent");
static const QString wirelessSecurity = QStringLiteral("802-11-wireless-security");
// Activations and profile changes may wait on an administrator password.
static constexpr int kAuthorizedCallTimeoutMs = 2 * 60 * 1000;
static constexpr int kForgetWatchTimeoutMs = 2 * 60 * 1000;
static QVariant objectPath(const QString &path)
{
return QVariant::fromValue(QDBusObjectPath(path.isEmpty() ? QStringLiteral("/") : path));
}
// At most 32 bytes, cut on a character boundary.
static QByteArray ssidBytes(const QString &name)
{
QByteArray bytes = name.toUtf8();
if (bytes.size() <= 32)
return bytes;
int size = 32;
while (size > 0 && (static_cast<uchar>(bytes.at(size)) & 0xc0) == 0x80)
--size;
return bytes.left(size);
}
Handler::Handler(QObject *parent)
: QObject(parent)
, m_client(NetworkClient::instance())
{
connect(m_client, &NetworkClient::changed, this, &Handler::update);
connect(m_client, &NetworkClient::activeStateChanged, this,
[this](const QString &active, uint state, uint reason) {
activeStateChanged(active, state);
if (state == ActiveDeactivated)
reportVpnFailure(active, reason);
});
connect(m_client, &NetworkClient::objectRemoved, this, [this](const QString &path) {
// Gone without a final state: the activation never made it.
if (m_forget.contains(path) || path == m_hotspotActivation)
activeStateChanged(path, ActiveDeactivated);
});
connect(m_client, &NetworkClient::readyChanged, this, [this] {
if (!m_client->ready()) {
m_forget.clear();
return;
}
if (m_scanRequested) {
m_scanRequested = false;
requestScan(m_scanInterface);
}
});
update();
}
void Handler::activateConnection(const QString &connection, const QString &device,
const QString &specificObject)
{
m_client->call(managerPath, managerInterface, QStringLiteral("ActivateConnection"),
{objectPath(connection), objectPath(device), objectPath(specificObject)}, this,
{}, kAuthorizedCallTimeoutMs);
}
void Handler::addAndActivateConnection(const QString &device, const QString &specificObject,
const QString &password)
{
addAndActivateWireless(device, specificObject, password, true);
}
void Handler::addAndActivateWireless(const QString &device, const QString &specificObject,
const QString &password, bool askForPassword)
{
if (!m_client->has(specificObject, accessPointInterface))
return;
const QString name = WifiUtils::ssidLabel(
m_client->property(specificObject, accessPointInterface, QStringLiteral("Ssid")).toByteArray());
const Enums::SecurityType security = accessPointSecurity(device, specificObject);
// Enterprise networks need a user name; see addAndActivateEnterprise().
if (WifiUtils::enterprise(security))
return;
// Asked for before anything is added: activating first would save the network
// and show it connecting while the user is still typing, or has cancelled.
if (askForPassword && password.isEmpty() && WifiUtils::needsPassword(security)) {
auto message = QDBusMessage::createMethodCall(promptService, promptPath, promptInterface,
QStringLiteral("ConnectWireless"));
message << device << specificObject << name << (security == Enums::StaticWep);
auto *watcher = new QDBusPendingCallWatcher(QDBusConnection::sessionBus().asyncCall(message), this);
connect(watcher, &QDBusPendingCallWatcher::finished, this,
[this, device, specificObject](QDBusPendingCallWatcher *watcher) {
watcher->deleteLater();
// Without the shell there is no one to ask first; NetworkManager
// asks whichever agent there is once activation needs the secret.
if (watcher->isError())
addAndActivateWireless(device, specificObject, QString(), false);
});
return;
}
// NetworkManager completes the rest from the access point: the SSID's own bytes, the
// mode, and the key management, which for a WPA2/WPA3 network is wpa-psk so drivers
// without SAE still connect.
QVariantMap connection {{QStringLiteral("id"), name}};
// A shared profile keeps its password in NetworkManager for every user; a private
// one keeps it in the user's keyring through the secret agent.
const bool shared = m_client->sharesNewConnections();
if (!shared)
connection.insert(QStringLiteral("permissions"), QStringList {NetworkClient::userPermission()});
NMVariantMapMap settings;
settings.insert(QStringLiteral("connection"), connection);
if (WifiUtils::needsPassword(security)) {
const uint secretFlags = shared ? SecretSystemOwned : SecretAgentOwned;
QVariantMap secrets;
if (security == Enums::StaticWep) {
secrets.insert(QStringLiteral("wep-key-flags"), secretFlags);
if (!password.isEmpty()) {
secrets.insert(QStringLiteral("wep-key0"), password);
// A 5 or 13 character, or 10 or 26 digit key; not a passphrase.
secrets.insert(QStringLiteral("wep-key-type"), 1u);
}
} else {
secrets.insert(QStringLiteral("psk-flags"), secretFlags);
if (!password.isEmpty())
secrets.insert(QStringLiteral("psk"), password);
}
settings.insert(wirelessSecurity, secrets);
}
addAndActivate(settings, device, specificObject);
}
void Handler::addAndActivateEnterprise(const QString &device, const QString &specificObject,
const QString &userName, const QString &password)
{
if (!m_client->has(specificObject, accessPointInterface))
return;
// A 192-bit network signs in with EAP-TLS, which needs certificates.
const Enums::SecurityType security = accessPointSecurity(device, specificObject);
if (security != Enums::WpaEap && security != Enums::Wpa2Eap)
return;
QVariantMap connection {
{QStringLiteral("id"),
WifiUtils::ssidLabel(m_client->property(specificObject, accessPointInterface, QStringLiteral("Ssid")).toByteArray())},
};
const bool shared = m_client->sharesNewConnections();
if (!shared)
connection.insert(QStringLiteral("permissions"), QStringList {NetworkClient::userPermission()});
// NetworkManager completes the SSID and wpa-eap key management from the access point;
// only the 802.1X sign-in has to be spelt out.
NMVariantMapMap settings;
settings.insert(QStringLiteral("connection"), connection);
settings.insert(QStringLiteral("802-1x"),
WifiUtils::passwordEap(userName, password, shared ? SecretSystemOwned : SecretAgentOwned));
addAndActivate(settings, device, specificObject);
}
QVariantMap Handler::wirelessNetwork(const QString &accessPoint) const
{
if (!m_client->has(accessPoint, accessPointInterface))
return {};
for (const QString &device : m_client->devices()) {
if (m_client->deviceType(device) != DeviceWifi || !m_client->accessPoints(device).contains(accessPoint))
continue;
return {{QStringLiteral("device"), device},
{QStringLiteral("name"),
WifiUtils::ssidLabel(m_client->property(accessPoint, accessPointInterface, QStringLiteral("Ssid")).toByteArray())},
{QStringLiteral("security"), int(accessPointSecurity(device, accessPoint))}};
}
return {};
}
Enums::SecurityType Handler::accessPointSecurity(const QString &device, const QString &accessPoint) const
{
const auto ap = [&](const char *name) {
return m_client->property(accessPoint, accessPointInterface, QLatin1String(name)).toUInt();
};
return WifiUtils::security(
m_client->property(device, wirelessInterface, QStringLiteral("WirelessCapabilities")).toUInt(),
ap("Flags"), ap("WpaFlags"), ap("RsnFlags"), ap("Mode"));
}
void Handler::addAndActivate(const NMVariantMapMap &settings, const QString &device,
const QString &specificObject)
{
m_client->call(managerPath, managerInterface, QStringLiteral("AddAndActivateConnection"),
{QVariant::fromValue(settings), objectPath(device), objectPath(specificObject)}, this,
[this](const QDBusMessage &reply) {
if (reply.type() != QDBusMessage::ReplyMessage) {
qWarning() << "Cannot join the network:" << reply.errorMessage();
return;
}
forgetUnlessActivated(NetworkClient::path(reply.arguments().value(0)),
NetworkClient::path(reply.arguments().value(1)));
},
kAuthorizedCallTimeoutMs);
}
void Handler::forgetUnlessActivated(const QString &connection, const QString &active)
{
// A wrong password the user then gave up on would otherwise stay behind as a known
// network. The active connection arrived before this reply did.
if (connection.isEmpty())
return;
if (active.isEmpty() || !m_client->has(active, activeInterface)) {
removeConnection(connection);
return;
}
m_forget.insert(active, connection);
activeStateChanged(active, m_client->property(active, activeInterface, QStringLiteral("State")).toUInt());
// An activation NetworkManager drops without a final state is let go of.
QTimer::singleShot(kForgetWatchTimeoutMs, this, [this, active] { m_forget.remove(active); });
}
void Handler::activeStateChanged(const QString &active, uint state)
{
if (!m_hotspotActivation.isEmpty() && active == m_hotspotActivation) {
if (state == ActiveDeactivated) {
m_hotspotActivation.clear();
Q_EMIT hotspotFailed(tr("The hotspot could not be started. The Wi-Fi adapter may not "
"support it on this channel or with this password."));
} else if (state == ActiveActivated) {
m_hotspotActivation.clear();
}
}
const auto it = m_forget.constFind(active);
if (it == m_forget.cend())
return;
if (state == ActiveDeactivated) {
const QString connection = it.value();
m_forget.erase(it);
removeConnection(connection);
} else if (state == ActiveActivated) {
m_forget.erase(it);
}
}
void Handler::reportVpnFailure(const QString &active, uint reason)
{
if (!m_client->property(active, activeInterface, QStringLiteral("Vpn")).toBool())
return;
// A lost uplink (DEVICE_DISCONNECTED) takes the VPN down without a failure of its own.
if (reason == 3)
return;
const QString message = VpnUtils::failureMessage(reason);
if (message.isEmpty())
return;
Q_EMIT vpnFailed(m_client->property(active, activeInterface, QStringLiteral("Id")).toString(), message);
}
void Handler::deactivateConnection(const QString &connection, const QString &device)
{
for (const QString &active : m_client->activeConnections()) {
const QString profile = NetworkClient::path(
m_client->property(active, activeInterface, QStringLiteral("Connection")));
const QStringList devices = m_client->property(active, activeInterface, QStringLiteral("Devices")).toStringList();
const bool vpn = m_client->property(active, activeInterface, QStringLiteral("Vpn")).toBool();
const bool matches = connection.isEmpty() ? devices.contains(device) : profile == connection;
if (!matches)
continue;
// A VPN, or a profile named without a device (WireGuard brings its own), goes down
// alone; disconnecting a device also keeps it from connecting again by itself.
if (vpn || device.isEmpty()) {
m_client->call(managerPath, managerInterface, QStringLiteral("DeactivateConnection"),
{objectPath(active)}, this, {}, kAuthorizedCallTimeoutMs);
} else if (devices.contains(device)) {
m_client->call(device, deviceInterface, QStringLiteral("Disconnect"), {}, this, {},
kAuthorizedCallTimeoutMs);
}
}
}
void Handler::removeConnection(const QString &connection)
{
const QString uuid = m_client->setting(connection, QStringLiteral("connection"), QStringLiteral("uuid")).toString();
// Port profiles go with the profile they belong to.
if (!uuid.isEmpty()) {
for (const QString &port : m_client->connections()) {
const QString controller =
m_client->setting(port, QStringLiteral("connection"), QStringLiteral("controller")).toString();
if (controller == uuid
|| m_client->setting(port, QStringLiteral("connection"), QStringLiteral("master")).toString() == uuid)
m_client->call(port, connectionInterface, QStringLiteral("Delete"), {}, this, {},
kAuthorizedCallTimeoutMs);
}
}
m_client->call(connection, connectionInterface, QStringLiteral("Delete"), {}, this, {},
kAuthorizedCallTimeoutMs);
}
void Handler::requestScan(const QString &interface)
{
if (!m_client->ready()) {
m_scanRequested = true;
m_scanInterface = interface;
return;
}
if (!m_client->manager(QStringLiteral("WirelessEnabled")).toBool())
return;
// NetworkManager rate-limits and defers scans itself; it only refuses devices that
// cannot scan, and one in AP mode would drop the hotspot's clients.
for (const QString &device : m_client->devices()) {
if (m_client->deviceType(device) != DeviceWifi
|| m_client->deviceState(device) < DeviceDisconnected
|| m_client->property(device, wirelessInterface, QStringLiteral("Mode")).toUInt() == ModeAp)
continue;
if (!interface.isEmpty()
&& m_client->property(device, deviceInterface, QStringLiteral("Interface")).toString() != interface)
continue;
m_client->call(device, wirelessInterface, QStringLiteral("RequestScan"), {QVariantMap()}, this);
}
}
void Handler::enableNetworking(bool enable)
{
m_client->call(managerPath, managerInterface, QStringLiteral("Enable"), {enable}, this, {},
kAuthorizedCallTimeoutMs);
}
void Handler::enableWireless(bool enable)
{
m_client->setProperty(managerPath, managerInterface, QStringLiteral("WirelessEnabled"), enable, this);
}
void Handler::enableWired(bool enable)
{
for (const QString &device : m_client->devices()) {
if (m_client->deviceType(device) != DeviceEthernet
|| !m_client->property(device, deviceInterface, QStringLiteral("Managed")).toBool())
continue;
// Disconnecting clears the device's autoconnect flag and activating sets it again, so the
// flag is written only when neither applies; writing it next to either races the other.
const uint state = m_client->deviceState(device);
if (!enable && state >= DevicePrepare && state <= DeviceActivated) {
m_client->call(device, deviceInterface, QStringLiteral("Disconnect"), {}, this, {},
kAuthorizedCallTimeoutMs);
} else if (enable && state == DeviceDisconnected) {
// "/" lets NetworkManager pick the device's best connection, even one that doesn't autoconnect.
activateConnection(QString(), device, QString());
} else {
m_client->setProperty(device, deviceInterface, QStringLiteral("Autoconnect"), enable, this);
}
}
}
void Handler::enableWwan(bool enable)
{
m_client->setProperty(managerPath, managerInterface, QStringLiteral("WwanEnabled"), enable, this);
}
void Handler::createHotspot()
{
uint capabilities = 0;
const QString device = hotspotDevice(&capabilities);
if (device.isEmpty()) {
Q_EMIT hotspotFailed(tr("No Wi-Fi adapter can run a hotspot."));
return;
}
const bool apMode = capabilities & CapAp;
const Configuration configuration;
const QString name = configuration.hotspotName();
const QString password = configuration.hotspotPassword();
const QString existing = m_client->hotspotConnection();
const QString uuid = existing.isEmpty()
? QUuid::createUuid().toString(QUuid::WithoutBraces)
: m_client->setting(existing, QStringLiteral("connection"), QStringLiteral("uuid")).toString();
NMVariantMapMap settings;
settings.insert(QStringLiteral("connection"),
{{QStringLiteral("id"), name},
{QStringLiteral("uuid"), uuid},
{QStringLiteral("type"), QStringLiteral("802-11-wireless")},
{QStringLiteral("autoconnect"), false}});
settings.insert(QStringLiteral("802-11-wireless"),
{{QStringLiteral("ssid"), ssidBytes(name)},
{QStringLiteral("mode"), apMode ? QStringLiteral("ap") : QStringLiteral("adhoc")}});
// NetworkManager picks a channel that is valid where the machine is.
if (!password.isEmpty()) {
QVariantMap security;
if (apMode) {
security.insert(QStringLiteral("key-mgmt"), QStringLiteral("wpa-psk"));
security.insert(QStringLiteral("psk"), password);
if (capabilities & CapRsn) {
security.insert(QStringLiteral("proto"), QStringList {QStringLiteral("rsn")});
security.insert(QStringLiteral("pairwise"), QStringList {QStringLiteral("ccmp")});
security.insert(QStringLiteral("group"), QStringList {QStringLiteral("ccmp")});
}
} else {
security.insert(QStringLiteral("key-mgmt"), QStringLiteral("none"));
security.insert(QStringLiteral("wep-key-type"), 2u);
security.insert(QStringLiteral("wep-key0"), password);
}
settings.insert(wirelessSecurity, security);
}
settings.insert(QStringLiteral("ipv4"), {{QStringLiteral("method"), QStringLiteral("shared")}});
settings.insert(QStringLiteral("ipv6"), {{QStringLiteral("method"), QStringLiteral("ignore")}});
// One profile, kept and updated, rather than a new one each time.
if (existing.isEmpty()) {
m_client->call(managerPath, managerInterface, QStringLiteral("AddAndActivateConnection"),
{QVariant::fromValue(settings), objectPath(device), objectPath(QString())}, this,
[this](const QDBusMessage &reply) { hotspotRequestFinished(reply, 1); },
kAuthorizedCallTimeoutMs);
return;
}
m_client->call(existing, connectionInterface, QStringLiteral("Update"), {QVariant::fromValue(settings)},
this, [this, existing, device](const QDBusMessage &reply) {
if (reply.type() != QDBusMessage::ReplyMessage) {
hotspotRequestFinished(reply, 0);
return;
}
m_client->call(managerPath, managerInterface, QStringLiteral("ActivateConnection"),
{objectPath(existing), objectPath(device), objectPath(QString())}, this,
[this](const QDBusMessage &reply) { hotspotRequestFinished(reply, 0); },
kAuthorizedCallTimeoutMs);
},
kAuthorizedCallTimeoutMs);
}
void Handler::hotspotRequestFinished(const QDBusMessage &reply, int activePathArgument)
{
if (reply.type() != QDBusMessage::ReplyMessage) {
// Cancelling the administrator password prompt is no failure to report.
if (reply.errorName() != QLatin1String("org.freedesktop.NetworkManager.PermissionDenied"))
Q_EMIT hotspotFailed(reply.errorMessage());
return;
}
const QString active = reply.arguments().value(activePathArgument).value<QDBusObjectPath>().path();
const uint state = m_client->property(active, activeInterface, QStringLiteral("State")).toUInt();
if (state == ActiveActivated)
return;
m_hotspotActivation = active;
// Its state may have changed, or NetworkManager removed it, before this reply arrived.
const bool gone = m_client->ready() && !m_client->has(active, activeInterface);
if (gone || state == ActiveDeactivated || state == ActiveDeactivating)
activeStateChanged(active, ActiveDeactivated);
}
void Handler::stopHotspot()
{
m_hotspotActivation.clear();
const QString active = hotspotActiveConnection();
if (active.isEmpty())
return;
m_client->call(managerPath, managerInterface, QStringLiteral("DeactivateConnection"),
{objectPath(active)}, this, {}, kAuthorizedCallTimeoutMs);
}
void Handler::importVpn(const QUrl &file)
{
const QString path = file.isLocalFile() ? file.toLocalFile() : file.toString();
QFile config(path);
if (config.open(QIODevice::ReadOnly | QIODevice::Text) && config.size() < 1024 * 1024) {
const QByteArray content = config.readAll();
if (VpnUtils::isWireGuardConfig(content)) {
QString error;
const NMVariantMapMap profile = VpnUtils::wireGuardProfile(content, QFileInfo(path).completeBaseName(), &error);
if (profile.isEmpty())
Q_EMIT vpnAddFailed(error);
else
addPrivateVpn(profile);
return;
}
}
// NetworkManager's own readers, the ones nmcli and both big desktops use; whichever
// reads the file first wins.
QStringList plugins = VpnUtils::installedPlugins();
const QString suffix = QFileInfo(path).suffix().toLower();
const QString likely = suffix == QLatin1String("ovpn") || suffix == QLatin1String("conf") ? QStringLiteral("openvpn")
: suffix == QLatin1String("pcf") ? QStringLiteral("vpnc")
: QString();
if (plugins.removeAll(likely))
plugins.prepend(likely);
if (plugins.isEmpty()) {
Q_EMIT vpnAddFailed(tr("No VPN plugin can read this file. Install the one for your VPN, "
"for example network-manager-openvpn for OpenVPN."));
return;
}
importVpnAs(path, plugins, QString());
}
void Handler::importVpnAs(const QString &file, QStringList types, const QString &error)
{
if (types.isEmpty()) {
Q_EMIT vpnAddFailed(error.isEmpty() ? tr("This file isn't a VPN configuration this system can read.")
: error);
return;
}
const QString type = types.takeFirst();
// A plugin's reader runs in nmcli: NetworkManager offers no way to call it over D-Bus.
auto *process = new QProcess(this);
process->setProgram(QStringLiteral("nmcli"));
process->setArguments({QStringLiteral("connection"), QStringLiteral("import"), QStringLiteral("type"), type,
QStringLiteral("file"), file});
connect(process, &QProcess::finished, this,
[this, process, file, types, error](int code, QProcess::ExitStatus status) {
process->deleteLater();
const QString output = QString::fromUtf8(process->readAllStandardOutput());
const QString message = QString::fromUtf8(process->readAllStandardError()).trimmed();
if (status == QProcess::NormalExit && code == 0) {
const auto uuid = QRegularExpression(QStringLiteral("[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}"))
.match(output);
Q_EMIT vpnAdded(uuid.captured());
return;
}
importVpnAs(file, types, message.isEmpty() ? error : message);
});
connect(process, &QProcess::errorOccurred, this, [this, process](QProcess::ProcessError failure) {
if (failure != QProcess::FailedToStart)
return;
process->deleteLater();
Q_EMIT vpnAddFailed(tr("nmcli, which reads VPN files, is not installed."));
});
process->start();
}
QVariantList Handler::vpnKinds() const
{
return VpnUtils::manualKinds();
}
static QMap<QString, QString> vpnData(const NMVariantMapMap &settings)
{
// Straight from D-Bus the a{ss} is still a QDBusArgument.
const QVariantMap current = NetworkDBus::plain(settings.value(QStringLiteral("vpn")).value(QStringLiteral("data"))).toMap();
QMap<QString, QString> data;
for (auto it = current.cbegin(); it != current.cend(); ++it)
data.insert(it.key(), it.value().toString());
return data;
}
QVariantMap Handler::vpnForm(const QString &kind, const QString &connection) const
{
NMVariantMapMap settings;
QString shortName = kind;
if (!connection.isEmpty()) {
settings = m_client->settings(connection);
shortName = VpnUtils::kind(m_client, connection);
}
const bool wireguard = shortName == QLatin1String("wireguard");
const QVariantMap info = settings.value(QStringLiteral("connection"));
const bool ownNetworkOnly = settings.value(QStringLiteral("ipv4")).value(QStringLiteral("never-default")).toBool()
|| settings.value(QStringLiteral("ipv6")).value(QStringLiteral("never-default")).toBool();
QVariantMap form {
{QStringLiteral("kind"), shortName},
{QStringLiteral("label"), VpnUtils::kindLabel(shortName)},
{QStringLiteral("installed"), wireguard || VpnUtils::pluginInstalled(QStringLiteral("org.freedesktop.NetworkManager.") + shortName)},
{QStringLiteral("package"), VpnUtils::package(shortName)},
{QStringLiteral("name"), info.value(QStringLiteral("id")).toString()},
{QStringLiteral("shared"), !connection.isEmpty() && info.value(QStringLiteral("permissions")).toStringList().isEmpty()},
{QStringLiteral("ownNetworkOnly"), ownNetworkOnly},
{QStringLiteral("exportable"), wireguard || VpnSchema::exportable(shortName)},
{QStringLiteral("exportPatterns"), wireguard ? QStringList {QStringLiteral("*.conf")} : VpnSchema::importPatterns(shortName)},
};
if (wireguard)
form.insert(QStringLiteral("wireguard"), VpnUtils::wireGuardForm(settings));
else
form.insert(QStringLiteral("fields"), VpnSchema::formFields(shortName, vpnData(settings)));
return form;
}
void Handler::getSecrets(const QString &connection, const QString &setting,
std::function<void(const NMVariantMapMap &)> done)
{
m_client->call(connection, connectionInterface, QStringLiteral("GetSecrets"), {setting}, this,
[done](const QDBusMessage &reply) {
// A profile without secrets of that kind, or one this user may not
// read them from, gives an error: the form then starts empty.
if (reply.type() == QDBusMessage::ReplyMessage)
done(qdbus_cast<NMVariantMapMap>(reply.arguments().value(0)));
else
done({});
},
kAuthorizedCallTimeoutMs);
}
void Handler::loadVpnSecrets(const QString &connection)
{
const QString uuid = m_client->setting(connection, QStringLiteral("connection"), QStringLiteral("uuid")).toString();
if (VpnUtils::kind(m_client, connection) == QLatin1String("wireguard")) {
getSecrets(connection, QStringLiteral("wireguard"), [this, connection](const NMVariantMapMap &secrets) {
const QVariantMap wireguard = secrets.value(QStringLiteral("wireguard"));
QVariantMap presharedKeys;
// Straight from D-Bus the aa{sv} is still a QDBusArgument.
const QVariantList peers = NetworkDBus::plain(wireguard.value(QStringLiteral("peers"))).toList();
for (const QVariant &entry : peers) {
const QVariantMap peer = entry.toMap();
if (peer.contains(QStringLiteral("preshared-key")))
presharedKeys.insert(peer.value(QStringLiteral("public-key")).toString(), peer.value(QStringLiteral("preshared-key")));
}
Q_EMIT vpnSecretsLoaded(connection, {{QStringLiteral("privateKey"), wireguard.value(QStringLiteral("private-key")).toString()},
{QStringLiteral("presharedKeys"), presharedKeys}});
});
return;
}
// System-owned secrets come from NetworkManager, this user's from the keyring.
getSecrets(connection, QStringLiteral("vpn"), [this, connection, uuid](const NMVariantMapMap &secrets) {
const QVariantMap system = NetworkDBus::plain(secrets.value(QStringLiteral("vpn")).value(QStringLiteral("secrets"))).toMap();
if (!m_secrets)
m_secrets = new SecretStore(this);
m_secrets->load(uuid, QStringLiteral("vpn"), [this, connection, system](const QVariantMap &stored) {
QVariantMap all = system;
all.insert(stored);
Q_EMIT vpnSecretsLoaded(connection, all);
});
});
}
void Handler::saveVpn(const QString &connection, const QVariantMap &form)
{
const bool adding = connection.isEmpty();
const auto fail = [this, adding](const QString &message) {
if (adding)
Q_EMIT vpnAddFailed(message);
else
Q_EMIT vpnSaveFailed(message);
};
NMVariantMapMap profile = adding ? NMVariantMapMap() : m_client->settings(connection);
const QString kind = adding ? form.value(QStringLiteral("kind")).toString() : VpnUtils::kind(m_client, connection);
const bool shared = form.value(QStringLiteral("shared")).toBool();
const QString name = form.value(QStringLiteral("name")).toString().trimmed();
QString error;
if (kind == QLatin1String("wireguard")) {
profile = VpnUtils::wireGuardSettings(form, profile, &error);
if (profile.isEmpty())
return fail(error);
} else {
QMap<QString, QString> data = vpnData(profile);
QMap<QString, QString> secrets;
// A shared profile's passwords are NetworkManager's to keep, a private one's this
// user's: a storage choice from the other arrangement moves over.
QVariantMap storage = form.value(QStringLiteral("storage")).toMap();
for (auto it = storage.begin(); it != storage.end(); ++it) {
const uint flags = it->toUInt();
if (shared && flags == SecretAgentOwned)
*it = uint(SecretSystemOwned);
else if (!shared && flags == SecretSystemOwned)
*it = uint(SecretAgentOwned);
}
if (!VpnSchema::apply(kind, form.value(QStringLiteral("values")).toMap(), storage,
form.value(QStringLiteral("secrets")).toMap(), shared, data, secrets, &error))
return fail(error);
QVariantMap info = profile.value(QStringLiteral("connection"));
if (adding) {
info.insert(QStringLiteral("uuid"), QUuid::createUuid().toString(QUuid::WithoutBraces));
info.insert(QStringLiteral("type"), QStringLiteral("vpn"));
// Named after the server, as the file imports are named after their file.
QString fallback;
for (const VpnSchema::Field &field : VpnSchema::fields(kind)) {
if (field.required && fallback.isEmpty())
fallback = data.value(field.key);
}
info.insert(QStringLiteral("id"), fallback.isEmpty() ? VpnUtils::kindLabel(kind) : fallback);
}
profile[QStringLiteral("connection")] = info;
QVariantMap vpn = profile.value(QStringLiteral("vpn"));
vpn.insert(QStringLiteral("service-type"), QStringLiteral("org.freedesktop.NetworkManager.") + kind);
vpn.insert(QStringLiteral("data"), QVariant::fromValue(data));
// Without secrets in the update, NetworkManager keeps the stored ones.
if (secrets.isEmpty())
vpn.remove(QStringLiteral("secrets"));
else
vpn.insert(QStringLiteral("secrets"), QVariant::fromValue(secrets));
profile[QStringLiteral("vpn")] = vpn;
for (const QString &family : {QStringLiteral("ipv4"), QStringLiteral("ipv6")}) {
if (!profile.value(family).contains(QStringLiteral("method")))
profile[family].insert(QStringLiteral("method"), QStringLiteral("auto"));
}
}
if (!name.isEmpty())
profile[QStringLiteral("connection")].insert(QStringLiteral("id"), name);
profile[QStringLiteral("connection")].insert(QStringLiteral("permissions"),
shared ? QStringList() : QStringList {NetworkClient::userPermission()});
const bool ownNetworkOnly = form.value(QStringLiteral("ownNetworkOnly")).toBool();
for (const QString &family : {QStringLiteral("ipv4"), QStringLiteral("ipv6")}) {
if (ownNetworkOnly)
profile[family].insert(QStringLiteral("never-default"), true);
else
profile[family].remove(QStringLiteral("never-default"));
}
if (adding) {
addPrivateVpn(profile);
return;
}
m_client->call(connection, connectionInterface, QStringLiteral("Update"), {QVariant::fromValue(profile)}, this,
[this, connection](const QDBusMessage &reply) {
if (reply.type() == QDBusMessage::ReplyMessage)
Q_EMIT vpnSaved(connection);
else
Q_EMIT vpnSaveFailed(reply.errorMessage());
},
kAuthorizedCallTimeoutMs);
}
void Handler::exportVpn(const QString &connection, const QUrl &file)
{
const QString path = file.isLocalFile() ? file.toLocalFile() : file.toString();
if (VpnUtils::kind(m_client, connection) == QLatin1String("wireguard")) {
getSecrets(connection, QStringLiteral("wireguard"), [this, connection, path](const NMVariantMapMap &secrets) {
NMVariantMapMap settings = m_client->settings(connection);
const QVariantMap wireguard = secrets.value(QStringLiteral("wireguard"));
if (!wireguard.contains(QStringLiteral("private-key"))) {
Q_EMIT vpnExportFailed(tr("The private key can't be read, so the file would be incomplete."));
return;
}
settings[QStringLiteral("wireguard")].insert(QStringLiteral("private-key"), wireguard.value(QStringLiteral("private-key")));
// Peers come with their preshared keys only here.
if (wireguard.contains(QStringLiteral("peers")))
settings[QStringLiteral("wireguard")].insert(QStringLiteral("peers"), wireguard.value(QStringLiteral("peers")));
QFile out(path);
if (!out.open(QIODevice::WriteOnly | QIODevice::Truncate) || out.write(VpnUtils::wireGuardConfig(settings)) < 0) {
Q_EMIT vpnExportFailed(out.errorString());
return;
}
// The file holds the private key.
out.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner);
Q_EMIT vpnExported(path);
});
return;
}
// The plugin's writer runs in nmcli, like its reader.
const QString uuid = m_client->setting(connection, QStringLiteral("connection"), QStringLiteral("uuid")).toString();
auto *process = new QProcess(this);
process->setProgram(QStringLiteral("nmcli"));
process->setArguments({QStringLiteral("connection"), QStringLiteral("export"), QStringLiteral("uuid"), uuid, path});
connect(process, &QProcess::finished, this, [this, process, path](int code, QProcess::ExitStatus status) {
process->deleteLater();
const QString message = QString::fromUtf8(process->readAllStandardError()).trimmed();
if (status == QProcess::NormalExit && code == 0)
Q_EMIT vpnExported(path);
else
Q_EMIT vpnExportFailed(message.isEmpty() ? tr("The VPN couldn't be written to a file.") : message);
});
connect(process, &QProcess::errorOccurred, this, [this, process](QProcess::ProcessError failure) {
if (failure != QProcess::FailedToStart)
return;
process->deleteLater();
Q_EMIT vpnExportFailed(tr("nmcli, which writes VPN files, is not installed."));
});
process->start();
}
QVariantMap Handler::ipForm(const QString &connection) const
{
QVariantMap form = IpUtils::form(m_client->settings(connection));
form.insert(QStringLiteral("current"), IpUtils::currentAddresses(m_client, connection));
return form;
}
QVariantMap Handler::checkIpForm(const QVariantMap &form) const
{
return IpUtils::check(form);
}
void Handler::saveIp(const QString &connection, const QVariantMap &form)
{
bool changed = false;
QString error;
const NMVariantMapMap profile = IpUtils::settings(form, m_client->settings(connection), &changed, &error);
if (profile.isEmpty()) {
Q_EMIT ipSaveFailed(connection, error);
return;
}
if (!changed) {
Q_EMIT ipSaved(connection);
return;
}
// Without secrets in it, an update keeps the ones NetworkManager and the keyring hold.
m_client->call(connection, connectionInterface, QStringLiteral("Update"), {QVariant::fromValue(profile)}, this,
[this, connection](const QDBusMessage &reply) {
if (reply.type() != QDBusMessage::ReplyMessage) {
Q_EMIT ipSaveFailed(connection, reply.errorMessage());
return;
}
Q_EMIT ipSaved(connection);
reapply(connection);
},
kAuthorizedCallTimeoutMs);
}
// As GNOME's editor does after saving: the device takes the new addressing at once.
void Handler::reapply(const QString &connection)
{
const QString active = VpnUtils::activeConnection(m_client, connection);
const QStringList devices = m_client->property(active, activeInterface, QStringLiteral("Devices")).toStringList();
if (devices.isEmpty())
return;
const QString device = devices.first();
// An empty profile reapplies the saved one; version 0 skips the version check.
m_client->call(device, deviceInterface, QStringLiteral("Reapply"),
{QVariant::fromValue(NMVariantMapMap()), qulonglong(0), 0u}, this,
[this, connection, device](const QDBusMessage &reply) {
if (reply.type() != QDBusMessage::ErrorMessage
|| reply.errorName() == QLatin1String("org.freedesktop.NetworkManager.Device.NotActive"))
return;
// What the device can't take while up, it takes on connecting again.
qWarning() << "Cannot reapply the network settings:" << reply.errorMessage();
activateConnection(connection, device, QString());
},
kAuthorizedCallTimeoutMs);
}
void Handler::addPrivateVpn(NMVariantMapMap profile)
{
// As the big desktops do: the VPN is this user's unless the form says otherwise, which
// needs no administrator password, and comes up only when asked to. A WireGuard profile
// would otherwise connect the moment it is added, its interface being its own.
profile[QStringLiteral("connection")].insert(QStringLiteral("autoconnect"), false);
if (!profile.value(QStringLiteral("connection")).contains(QStringLiteral("permissions")))
profile[QStringLiteral("connection")].insert(QStringLiteral("permissions"),
QStringList {NetworkClient::userPermission()});
const QString uuid = profile.value(QStringLiteral("connection")).value(QStringLiteral("uuid")).toString();
m_client->call(settingsPath, settingsInterface, QStringLiteral("AddConnection"), {QVariant::fromValue(profile)},
this,
[this, uuid](const QDBusMessage &reply) {
if (reply.type() == QDBusMessage::ReplyMessage)
Q_EMIT vpnAdded(uuid);
else
Q_EMIT vpnAddFailed(reply.errorMessage());
},
kAuthorizedCallTimeoutMs);
}
QString Handler::hotspotActiveConnection() const
{
const QString hotspot = m_client->hotspotConnection();
if (hotspot.isEmpty())
return {};
for (const QString &active : m_client->activeConnections()) {
const uint state = m_client->property(active, activeInterface, QStringLiteral("State")).toUInt();
if (NetworkClient::path(m_client->property(active, activeInterface, QStringLiteral("Connection"))) == hotspot
&& (state == ActiveActivating || state == ActiveActivated))
return active;
}
return {};
}
QString Handler::hotspotDevice(uint *capabilities) const
{
// An idle adapter that can run an access point is best; one in use gets disconnected.
QString device;
uint deviceCapabilities = 0;
for (const QString &candidate : m_client->devices()) {
if (m_client->deviceType(candidate) != DeviceWifi
|| !m_client->property(candidate, deviceInterface, QStringLiteral("Managed")).toBool())
continue;
const uint caps = m_client->property(candidate, wirelessInterface,
QStringLiteral("WirelessCapabilities")).toUInt();
if (!(caps & (CapAp | CapAdhoc)))
continue;
const bool idle = m_client->activeConnection(candidate).isEmpty();
const bool better = device.isEmpty() || ((caps & CapAp) && !(deviceCapabilities & CapAp))
|| (bool(caps & CapAp) == bool(deviceCapabilities & CapAp) && idle
&& !m_client->activeConnection(device).isEmpty());
if (better) {
device = candidate;
deviceCapabilities = caps;
}
}
if (capabilities)
*capabilities = deviceCapabilities;
return device;
}
void Handler::update()
{
const QString device = hotspotDevice();
const bool capable = !device.isEmpty();
// Polkit answers "auth" when an administrator password would do; an unread answer
// doesn't block either.
const QString permission = m_client->permission(QStringLiteral("org.freedesktop.NetworkManager.wifi.share.protected"));
const bool allowed = m_client->manager(QStringLiteral("WirelessEnabled")).toBool()
&& permission != QLatin1String("no");
// The network this adapter is on goes when the hotspot takes it over.
QString replaces;
const QString active = capable ? m_client->activeConnection(device) : QString();
if (!active.isEmpty()
&& NetworkClient::path(m_client->property(active, activeInterface, QStringLiteral("Connection")))
!= m_client->hotspotConnection())
replaces = m_client->property(active, activeInterface, QStringLiteral("Id")).toString();
if (capable != m_hotspotCapable || allowed != m_hotspotAllowed || replaces != m_hotspotReplaces) {
m_hotspotCapable = capable;
m_hotspotAllowed = allowed;
m_hotspotReplaces = replaces;
Q_EMIT hotspotSupportedChanged();
}
const bool hotspot = !hotspotActiveConnection().isEmpty();
if (hotspot != m_hotspotActive) {
m_hotspotActive = hotspot;
Q_EMIT hotspotActiveChanged();
}
}