diff --git a/client/3rd/QtSsh/src/ssh/sshconnection.cpp b/client/3rd/QtSsh/src/ssh/sshconnection.cpp index 6b2141218..e44034991 100644 --- a/client/3rd/QtSsh/src/ssh/sshconnection.cpp +++ b/client/3rd/QtSsh/src/ssh/sshconnection.cpp @@ -940,8 +940,8 @@ void SshConnectionPrivate::connectToHost() this, &SshConnectionPrivate::handleSocketConnected); connect(m_socket, &QIODevice::readyRead, this, &SshConnectionPrivate::handleIncomingData); - connect(m_socket, &QAbstractSocket::errorOccurred, - this, &SshConnectionPrivate::handleSocketError); + //connect(m_socket, &QAbstractSocket::errorOccurred, + // this, &SshConnectionPrivate::handleSocketError); connect(m_socket, &QAbstractSocket::disconnected, this, &SshConnectionPrivate::handleSocketDisconnected); connect(&m_timeoutTimer, &QTimer::timeout, this, &SshConnectionPrivate::handleTimeout); diff --git a/client/client.pro b/client/client.pro index 28f11c42c..cdabe18ae 100644 --- a/client/client.pro +++ b/client/client.pro @@ -15,7 +15,7 @@ include("3rd/QtSsh/src/ssh/qssh.pri") include("3rd/QtSsh/src/botan/botan.pri") !android:!ios:include("3rd/SingleApplication/singleapplication.pri") include ("3rd/SortFilterProxyModel/SortFilterProxyModel.pri") -include("3rd/QZXing/src/QZXing-components.pri") +include("3rd/qzxing/src/QZXing-components.pri") INCLUDEPATH += $$PWD/3rd/OpenSSL/include DEPENDPATH += $$PWD/3rd/OpenSSL/include @@ -197,6 +197,8 @@ linux:!android { LIBS += /usr/lib/x86_64-linux-gnu/libcrypto.a LIBS += /usr/lib/x86_64-linux-gnu/libssl.a + + INCLUDEPATH += $$PWD/platforms/linux } win32|macx|linux:!android { diff --git a/client/platforms/linux/backendlogsobserver.cpp b/client/platforms/linux/backendlogsobserver.cpp new file mode 100644 index 000000000..b1bb0070c --- /dev/null +++ b/client/platforms/linux/backendlogsobserver.cpp @@ -0,0 +1,36 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "backendlogsobserver.h" +#include "leakdetector.h" +#include "logger.h" + +#include +#include + +namespace { +Logger logger({LOG_LINUX, LOG_CONTROLLER}, "BackendLogsObserver"); +} + +BackendLogsObserver::BackendLogsObserver( + QObject* parent, std::function&& callback) + : QObject(parent), m_callback(std::move(callback)) { + MVPN_COUNT_CTOR(BackendLogsObserver); +} + +BackendLogsObserver::~BackendLogsObserver() { + MVPN_COUNT_DTOR(BackendLogsObserver); +} + +void BackendLogsObserver::completed(QDBusPendingCallWatcher* call) { + QDBusPendingReply reply = *call; + if (reply.isError()) { + logger.error() << "Error received from the DBus service"; + m_callback("Failed to retrieve logs from the mozillavpn linuxdaemon."); + return; + } + + QString status = reply.argumentAt<0>(); + m_callback(status); +} diff --git a/client/platforms/linux/backendlogsobserver.h b/client/platforms/linux/backendlogsobserver.h new file mode 100644 index 000000000..e2cb7f89b --- /dev/null +++ b/client/platforms/linux/backendlogsobserver.h @@ -0,0 +1,29 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef BACKENDLOGSOBSERVER_H +#define BACKENDLOGSOBSERVER_H + +#include +#include + +class QDBusPendingCallWatcher; + +class BackendLogsObserver final : public QObject { + Q_OBJECT + Q_DISABLE_COPY_MOVE(BackendLogsObserver) + + public: + BackendLogsObserver(QObject* parent, + std::function&& callback); + ~BackendLogsObserver(); + + public slots: + void completed(QDBusPendingCallWatcher* call); + + private: + std::function m_callback; +}; + +#endif // BACKENDLOGSOBSERVER_H diff --git a/client/platforms/linux/daemon/apptracker.cpp b/client/platforms/linux/daemon/apptracker.cpp new file mode 100644 index 000000000..437970cdb --- /dev/null +++ b/client/platforms/linux/daemon/apptracker.cpp @@ -0,0 +1,109 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "apptracker.h" +#include "dbustypeslinux.h" +#include "leakdetector.h" +#include "logger.h" + +#include +#include +#include +#include + +#include + +constexpr const char* GTK_DESKTOP_APP_SERVICE = "org.gtk.gio.DesktopAppInfo"; +constexpr const char* GTK_DESKTOP_APP_PATH = "/org/gtk/gio/DesktopAppInfo"; + +constexpr const char* DBUS_LOGIN_SERVICE = "org.freedesktop.login1"; +constexpr const char* DBUS_LOGIN_PATH = "/org/freedesktop/login1"; +constexpr const char* DBUS_LOGIN_MANAGER = "org.freedesktop.login1.Manager"; + +namespace { +Logger logger(LOG_LINUX, "AppTracker"); +} + +AppTracker::AppTracker(QObject* parent) : QObject(parent) { + MVPN_COUNT_CTOR(AppTracker); + logger.debug() << "AppTracker created."; + + QDBusConnection m_conn = QDBusConnection::systemBus(); + m_conn.connect("", DBUS_LOGIN_PATH, DBUS_LOGIN_MANAGER, "UserNew", this, + SLOT(userCreated(uint, const QDBusObjectPath&))); + m_conn.connect("", DBUS_LOGIN_PATH, DBUS_LOGIN_MANAGER, "UserRemoved", this, + SLOT(userRemoved(uint, const QDBusObjectPath&))); + + QDBusInterface n(DBUS_LOGIN_SERVICE, DBUS_LOGIN_PATH, DBUS_LOGIN_MANAGER, + m_conn); + QDBusPendingReply reply = n.asyncCall("ListUsers"); + QDBusPendingCallWatcher* watcher = new QDBusPendingCallWatcher(reply, this); + QObject::connect(watcher, SIGNAL(finished(QDBusPendingCallWatcher*)), this, + SLOT(userListCompleted(QDBusPendingCallWatcher*))); +} + +AppTracker::~AppTracker() { + MVPN_COUNT_DTOR(AppTracker); + logger.debug() << "AppTracker destroyed."; +} + +void AppTracker::userListCompleted(QDBusPendingCallWatcher* watcher) { + QDBusPendingReply reply = *watcher; + if (reply.isValid()) { + UserDataList list = reply.value(); + for (auto user : list) { + userCreated(user.userid, user.path); + } + } + + delete watcher; +} + +void AppTracker::userCreated(uint userid, const QDBusObjectPath& path) { + logger.debug() << "User created uid:" << userid << "at:" << path.path(); + + /* Acquire the effective UID of the user to connect to their session bus. */ + uid_t realuid = getuid(); + if (seteuid(userid) < 0) { + logger.warning() << "Failed to set effective UID"; + } + auto guard = qScopeGuard([&] { + if (seteuid(realuid) < 0) { + logger.warning() << "Failed to restore effective UID"; + } + }); + + /* For correctness we should ask systemd for the user's runtime directory. */ + QString busPath = "unix:path=/run/user/" + QString::number(userid) + "/bus"; + logger.debug() << "Connection to" << busPath; + QDBusConnection connection = + QDBusConnection::connectToBus(busPath, "user-" + QString::number(userid)); + + /* Connect to the user's GTK launch event. */ + bool isConnected = connection.connect( + "", GTK_DESKTOP_APP_PATH, GTK_DESKTOP_APP_SERVICE, "Launched", this, + SLOT(gtkLaunchEvent(const QByteArray&, const QString&, qlonglong, + const QStringList&, const QVariantMap&))); + if (!isConnected) { + logger.warning() << "Failed to connect to GTK Launched signal"; + } +} + +void AppTracker::userRemoved(uint uid, const QDBusObjectPath& path) { + logger.debug() << "User removed uid:" << uid << "at:" << path.path(); + QDBusConnection::disconnectFromBus("user-" + QString::number(uid)); +} + +void AppTracker::gtkLaunchEvent(const QByteArray& appid, const QString& display, + qlonglong pid, const QStringList& uris, + const QVariantMap& extra) { + Q_UNUSED(display); + Q_UNUSED(uris); + Q_UNUSED(extra); + + QString appIdName(appid); + if (!appIdName.isEmpty()) { + emit appLaunched(appIdName, pid); + } +} diff --git a/client/platforms/linux/daemon/apptracker.h b/client/platforms/linux/daemon/apptracker.h new file mode 100644 index 000000000..01d6b9b49 --- /dev/null +++ b/client/platforms/linux/daemon/apptracker.h @@ -0,0 +1,31 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef APPTRACKER_H +#define APPTRACKER_H + +#include +#include + +class AppTracker final : public QObject { + Q_OBJECT + Q_DISABLE_COPY_MOVE(AppTracker) + + public: + explicit AppTracker(QObject* parent); + ~AppTracker(); + + signals: + void appLaunched(const QString& name, int rootpid); + + private slots: + void userListCompleted(QDBusPendingCallWatcher* call); + void userCreated(uint uid, const QDBusObjectPath& path); + void userRemoved(uint uid, const QDBusObjectPath& path); + void gtkLaunchEvent(const QByteArray& appid, const QString& display, + qlonglong pid, const QStringList& uris, + const QVariantMap& extra); +}; + +#endif // APPTRACKER_H diff --git a/client/platforms/linux/daemon/dbusservice.cpp b/client/platforms/linux/daemon/dbusservice.cpp new file mode 100644 index 000000000..3219b5bed --- /dev/null +++ b/client/platforms/linux/daemon/dbusservice.cpp @@ -0,0 +1,249 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "dbusservice.h" +#include "dbus_adaptor.h" +#include "leakdetector.h" +#include "logger.h" +#include "loghandler.h" +#include "polkithelper.h" + +#include +#include +#include + +namespace { +Logger logger(LOG_LINUX, "DBusService"); +} + +constexpr const char* APP_STATE_ACTIVE = "active"; +constexpr const char* APP_STATE_EXCLUDED = "excluded"; +constexpr const char* APP_STATE_BLOCKED = "blocked"; + +DBusService::DBusService(QObject* parent) : Daemon(parent) { + MVPN_COUNT_CTOR(DBusService); + + m_wgutils = new WireguardUtilsLinux(this); + m_apptracker = new AppTracker(this); + m_pidtracker = new PidTracker(this); + + connect(m_apptracker, SIGNAL(appLaunched(const QString&, int)), this, + SLOT(appLaunched(const QString&, int))); + connect(m_pidtracker, SIGNAL(terminated(const QString&, int)), this, + SLOT(appTerminated(const QString&, int))); + + if (!removeInterfaceIfExists()) { + qFatal("Interface `%s` exists and cannot be removed. Cannot proceed!", + WG_INTERFACE); + } +} + +DBusService::~DBusService() { MVPN_COUNT_DTOR(DBusService); } + +IPUtils* DBusService::iputils() { + if (!m_iputils) { + m_iputils = new IPUtilsLinux(this); + } + return m_iputils; +} + +DnsUtils* DBusService::dnsutils() { + if (!m_dnsutils) { + m_dnsutils = new DnsUtilsLinux(this); + } + return m_dnsutils; +} + +void DBusService::setAdaptor(DbusAdaptor* adaptor) { + Q_ASSERT(!m_adaptor); + m_adaptor = adaptor; +} + +bool DBusService::removeInterfaceIfExists() { + if (m_wgutils->interfaceExists()) { + logger.warning() << "Device already exists. Let's remove it."; + if (!m_wgutils->deleteInterface()) { + logger.error() << "Failed to remove the device."; + return false; + } + } + return true; +} + +QString DBusService::version() { + logger.debug() << "Version request"; + return PROTOCOL_VERSION; +} + +bool DBusService::activate(const QString& jsonConfig) { + logger.debug() << "Activate"; + + if (!PolkitHelper::instance()->checkAuthorization( + "org.mozilla.vpn.activate")) { + logger.error() << "Polkit rejected"; + return false; + } + + QJsonDocument json = QJsonDocument::fromJson(jsonConfig.toLocal8Bit()); + if (!json.isObject()) { + logger.error() << "Invalid input"; + return false; + } + + QJsonObject obj = json.object(); + + InterfaceConfig config; + if (!parseConfig(obj, config)) { + logger.error() << "Invalid configuration"; + return false; + } + + if (obj.contains("vpnDisabledApps")) { + QJsonArray disabledApps = obj["vpnDisabledApps"].toArray(); + for (const QJsonValue& app : disabledApps) { + firewallApp(app.toString(), APP_STATE_EXCLUDED); + } + } + + return Daemon::activate(config); +} + +bool DBusService::deactivate(bool emitSignals) { + logger.debug() << "Deactivate"; + firewallClear(); + return Daemon::deactivate(emitSignals); +} + +QString DBusService::status() { return QString(getStatus()); } + +QByteArray DBusService::getStatus() { + logger.debug() << "Status request"; + QJsonObject json; + + if (!m_connections.contains(0)) { + json.insert("status", QJsonValue(false)); + return QJsonDocument(json).toJson(QJsonDocument::Compact); + } + + const InterfaceConfig& config = m_connections.value(0).m_config; + if (!m_wgutils->interfaceExists()) { + logger.error() << "Unable to get device"; + json.insert("status", QJsonValue(false)); + return QJsonDocument(json).toJson(QJsonDocument::Compact); + } + + json.insert("status", QJsonValue(true)); + json.insert("serverIpv4Gateway", QJsonValue(config.m_serverIpv4Gateway)); + json.insert("deviceIpv4Address", QJsonValue(config.m_deviceIpv4Address)); + WireguardUtilsLinux::peerStatus status = + m_wgutils->getPeerStatus(config.m_serverPublicKey); + json.insert("txBytes", QJsonValue(status.txBytes)); + json.insert("rxBytes", QJsonValue(status.rxBytes)); + + return QJsonDocument(json).toJson(QJsonDocument::Compact); +} + +QString DBusService::getLogs() { + logger.debug() << "Log request"; + return Daemon::logs(); +} + +void DBusService::appLaunched(const QString& name, int rootpid) { + logger.debug() << "tracking:" << name << "PID:" << rootpid; + ProcessGroup* group = m_pidtracker->track(name, rootpid); + if (m_firewallApps.contains(name)) { + group->state = m_firewallApps[name]; + group->moveToCgroup(getAppStateCgroup(group->state)); + } +} + +void DBusService::appTerminated(const QString& name, int rootpid) { + logger.debug() << "terminate:" << name << "PID:" << rootpid; +} + +/* Get the list of running applications that the firewall knows about. */ +QString DBusService::runningApps() { + QJsonArray result; + for (auto i = m_pidtracker->begin(); i != m_pidtracker->end(); i++) { + const ProcessGroup* group = *i; + QJsonObject appObject; + QJsonArray pidList; + appObject.insert("name", QJsonValue(group->name)); + appObject.insert("rootpid", QJsonValue(group->rootpid)); + appObject.insert("state", QJsonValue(group->state)); + + for (auto pid : group->kthreads.keys()) { + pidList.append(QJsonValue(pid)); + } + + appObject.insert("pids", pidList); + result.append(appObject); + } + + return QJsonDocument(result).toJson(QJsonDocument::Compact); +} + +/* Update the firewall for running applications matching the application ID. */ +bool DBusService::firewallApp(const QString& appName, const QString& state) { + logger.debug() << "Setting" << appName << "to firewall state" << state; + m_firewallApps[appName] = state; + QString cgroup = getAppStateCgroup(state); + + /* Change matching applications' state to excluded */ + for (auto i = m_pidtracker->begin(); i != m_pidtracker->end(); i++) { + ProcessGroup* group = *i; + if (group->name != appName) { + continue; + } + group->state = state; + group->moveToCgroup(cgroup); + } + + return true; +} + +/* Update the firewall for the application matching the desired PID. */ +bool DBusService::firewallPid(int rootpid, const QString& state) { + ProcessGroup* group = m_pidtracker->group(rootpid); + if (!group) { + return false; + } + + group->state = state; + group->moveToCgroup(getAppStateCgroup(group->state)); + + logger.debug() << "Setting" << group->name << "PID:" << rootpid + << "to firewall state" << state; + return true; +} + +/* Clear the firewall and return all applications to the active state */ +bool DBusService::firewallClear() { + const QString cgroup = getAppStateCgroup(APP_STATE_ACTIVE); + + m_firewallApps.clear(); + for (auto i = m_pidtracker->begin(); i != m_pidtracker->end(); i++) { + ProcessGroup* group = *i; + if (group->state == APP_STATE_ACTIVE) { + continue; + } + + group->state = APP_STATE_ACTIVE; + group->moveToCgroup(cgroup); + + logger.debug() << "Setting" << group->name << "PID:" << group->rootpid + << "to firewall state" << group->state; + } + return true; +} + +QString DBusService::getAppStateCgroup(const QString& state) { + if (state == APP_STATE_EXCLUDED) { + return m_wgutils->getExcludeCgroup(); + } + if (state == APP_STATE_BLOCKED) { + return m_wgutils->getBlockCgroup(); + } + return m_wgutils->getDefaultCgroup(); +} diff --git a/client/platforms/linux/daemon/dbusservice.h b/client/platforms/linux/daemon/dbusservice.h new file mode 100644 index 000000000..c5a80314e --- /dev/null +++ b/client/platforms/linux/daemon/dbusservice.h @@ -0,0 +1,72 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DBUSSERVICE_H +#define DBUSSERVICE_H + +#include "daemon/daemon.h" +#include "apptracker.h" +#include "iputilslinux.h" +#include "dnsutilslinux.h" +#include "pidtracker.h" +#include "wireguardutilslinux.h" + +class DbusAdaptor; + +class DBusService final : public Daemon { + Q_OBJECT + Q_DISABLE_COPY_MOVE(DBusService) + Q_CLASSINFO("D-Bus Interface", "org.mozilla.vpn.dbus") + + public: + DBusService(QObject* parent); + ~DBusService(); + + void setAdaptor(DbusAdaptor* adaptor); + + using Daemon::activate; + + public slots: + bool activate(const QString& jsonConfig); + + bool deactivate(bool emitSignals = true) override; + QString status(); + + QString version(); + QString getLogs(); + + QString runningApps(); + bool firewallApp(const QString& appName, const QString& state); + bool firewallPid(int rootpid, const QString& state); + bool firewallClear(); + + protected: + WireguardUtils* wgutils() const override { return m_wgutils; } + bool supportIPUtils() const override { return true; } + IPUtils* iputils() override; + bool supportDnsUtils() const override { return true; } + DnsUtils* dnsutils() override; + + QByteArray getStatus() override; + + private: + bool removeInterfaceIfExists(); + QString getAppStateCgroup(const QString& state); + + private slots: + void appLaunched(const QString& name, int rootpid); + void appTerminated(const QString& name, int rootpid); + + private: + DbusAdaptor* m_adaptor = nullptr; + WireguardUtilsLinux* m_wgutils = nullptr; + IPUtilsLinux* m_iputils = nullptr; + DnsUtilsLinux* m_dnsutils = nullptr; + + AppTracker* m_apptracker = nullptr; + PidTracker* m_pidtracker = nullptr; + QMap m_firewallApps; +}; + +#endif // DBUSSERVICE_H diff --git a/client/platforms/linux/daemon/dbustypeslinux.h b/client/platforms/linux/daemon/dbustypeslinux.h new file mode 100644 index 000000000..5489149a8 --- /dev/null +++ b/client/platforms/linux/daemon/dbustypeslinux.h @@ -0,0 +1,170 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DBUSTYPESLINUX_H +#define DBUSTYPESLINUX_H + +#include +#include +#include +#include + +#include + +/* D-Bus metatype for marshalling arguments to the SetLinkDNS method */ +class DnsResolver : public QHostAddress { + public: + DnsResolver(const QHostAddress& address = QHostAddress()) + : QHostAddress(address) {} + + friend QDBusArgument& operator<<(QDBusArgument& args, const DnsResolver& ip) { + args.beginStructure(); + if (ip.protocol() == QAbstractSocket::IPv6Protocol) { + Q_IPV6ADDR addrv6 = ip.toIPv6Address(); + args << AF_INET6; + args << QByteArray::fromRawData((const char*)&addrv6, sizeof(addrv6)); + } else { + quint32 addrv4 = ip.toIPv4Address(); + QByteArray data(4, 0); + data[0] = (addrv4 >> 24) & 0xff; + data[1] = (addrv4 >> 16) & 0xff; + data[2] = (addrv4 >> 8) & 0xff; + data[3] = (addrv4 >> 0) & 0xff; + args << AF_INET; + args << data; + } + args.endStructure(); + return args; + } + friend const QDBusArgument& operator>>(const QDBusArgument& args, + DnsResolver& ip) { + int family; + QByteArray data; + args.beginStructure(); + args >> family >> data; + args.endStructure(); + if (family == AF_INET6) { + ip.setAddress(data.constData()); + } else if (data.count() >= 4) { + quint32 addrv4 = 0; + addrv4 |= (data[0] << 24); + addrv4 |= (data[1] << 16); + addrv4 |= (data[2] << 8); + addrv4 |= (data[3] << 0); + ip.setAddress(addrv4); + } + return args; + } +}; +typedef QList DnsResolverList; +Q_DECLARE_METATYPE(DnsResolver); +Q_DECLARE_METATYPE(DnsResolverList); + +/* D-Bus metatype for marshalling arguments to the SetLinkDomains method */ +class DnsLinkDomain { + public: + DnsLinkDomain(const QString d = "", bool s = false) { + domain = d; + search = s; + }; + QString domain; + bool search; + + friend QDBusArgument& operator<<(QDBusArgument& args, + const DnsLinkDomain& data) { + args.beginStructure(); + args << data.domain << data.search; + args.endStructure(); + return args; + } + friend const QDBusArgument& operator>>(const QDBusArgument& args, + DnsLinkDomain& data) { + args.beginStructure(); + args >> data.domain >> data.search; + args.endStructure(); + return args; + } + bool operator==(const DnsLinkDomain& other) const { + return (domain == other.domain) && (search == other.search); + } + bool operator==(const QString& other) const { return (domain == other); } +}; +typedef QList DnsLinkDomainList; +Q_DECLARE_METATYPE(DnsLinkDomain); +Q_DECLARE_METATYPE(DnsLinkDomainList); + +/* D-Bus metatype for marshalling the Domains property */ +class DnsDomain { + public: + DnsDomain() {} + int ifindex = 0; + QString domain = ""; + bool search = false; + + friend QDBusArgument& operator<<(QDBusArgument& args, const DnsDomain& data) { + args.beginStructure(); + args << data.ifindex << data.domain << data.search; + args.endStructure(); + return args; + } + friend const QDBusArgument& operator>>(const QDBusArgument& args, + DnsDomain& data) { + args.beginStructure(); + args >> data.ifindex >> data.domain >> data.search; + args.endStructure(); + return args; + } +}; +typedef QList DnsDomainList; +Q_DECLARE_METATYPE(DnsDomain); +Q_DECLARE_METATYPE(DnsDomainList); + +/* D-Bus metatype for marshalling the freedesktop login manager data. */ +class UserData { + public: + QString name; + uint userid; + QDBusObjectPath path; + + friend QDBusArgument& operator<<(QDBusArgument& args, const UserData& data) { + args.beginStructure(); + args << data.userid << data.name << data.path; + args.endStructure(); + return args; + } + friend const QDBusArgument& operator>>(const QDBusArgument& args, + UserData& data) { + args.beginStructure(); + args >> data.userid >> data.name >> data.path; + args.endStructure(); + return args; + } +}; +typedef QList UserDataList; +Q_DECLARE_METATYPE(UserData); +Q_DECLARE_METATYPE(UserDataList); + +class DnsMetatypeRegistrationProxy { + public: + DnsMetatypeRegistrationProxy() { + qRegisterMetaType(); + qDBusRegisterMetaType(); + qRegisterMetaType(); + qDBusRegisterMetaType(); + qRegisterMetaType(); + qDBusRegisterMetaType(); + qRegisterMetaType(); + qDBusRegisterMetaType(); + qRegisterMetaType(); + qDBusRegisterMetaType(); + qRegisterMetaType(); + qDBusRegisterMetaType(); + qRegisterMetaType(); + qDBusRegisterMetaType(); + qRegisterMetaType(); + qDBusRegisterMetaType(); + } +}; + +#endif // DBUSTYPESLINUX_H diff --git a/client/platforms/linux/daemon/dnsutilslinux.cpp b/client/platforms/linux/daemon/dnsutilslinux.cpp new file mode 100644 index 000000000..214b46110 --- /dev/null +++ b/client/platforms/linux/daemon/dnsutilslinux.cpp @@ -0,0 +1,206 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "dnsutilslinux.h" +#include "leakdetector.h" +#include "logger.h" + +#include +#include + +#include + +constexpr const char* DBUS_RESOLVE_SERVICE = "org.freedesktop.resolve1"; +constexpr const char* DBUS_RESOLVE_PATH = "/org/freedesktop/resolve1"; +constexpr const char* DBUS_RESOLVE_MANAGER = "org.freedesktop.resolve1.Manager"; +constexpr const char* DBUS_PROPERTY_INTERFACE = + "org.freedesktop.DBus.Properties"; + +namespace { +Logger logger(LOG_LINUX, "DnsUtilsLinux"); +} + +DnsUtilsLinux::DnsUtilsLinux(QObject* parent) : DnsUtils(parent) { + MVPN_COUNT_CTOR(DnsUtilsLinux); + logger.debug() << "DnsUtilsLinux created."; + + QDBusConnection conn = QDBusConnection::systemBus(); + m_resolver = new QDBusInterface(DBUS_RESOLVE_SERVICE, DBUS_RESOLVE_PATH, + DBUS_RESOLVE_MANAGER, conn, this); +} + +DnsUtilsLinux::~DnsUtilsLinux() { + MVPN_COUNT_DTOR(DnsUtilsLinux); + + for (int ifindex : m_linkDomains.keys()) { + QList argumentList; + argumentList << QVariant::fromValue(ifindex); + argumentList << QVariant::fromValue(m_linkDomains[ifindex]); + m_resolver->asyncCallWithArgumentList(QStringLiteral("SetLinkDomains"), + argumentList); + } + + if (m_ifindex > 0) { + m_resolver->asyncCall(QStringLiteral("RevertLink"), m_ifindex); + } + + logger.debug() << "DnsUtilsLinux destroyed."; +} + +bool DnsUtilsLinux::updateResolvers(const QString& ifname, + const QList& resolvers) { + m_ifindex = if_nametoindex(qPrintable(ifname)); + if (m_ifindex <= 0) { + logger.error() << "Unable to resolve ifindex for" << ifname; + return false; + } + + setLinkDNS(m_ifindex, resolvers); + setLinkDefaultRoute(m_ifindex, true); + updateLinkDomains(); + return true; +} + +bool DnsUtilsLinux::restoreResolvers() { + for (auto ifindex : m_linkDomains.keys()) { + setLinkDomains(ifindex, m_linkDomains[ifindex]); + } + m_linkDomains.clear(); + + /* Revert the VPN interface's DNS configuration */ + if (m_ifindex > 0) { + QList argumentList = {QVariant::fromValue(m_ifindex)}; + QDBusPendingReply<> reply = m_resolver->asyncCallWithArgumentList( + QStringLiteral("RevertLink"), argumentList); + + QDBusPendingCallWatcher* watcher = new QDBusPendingCallWatcher(reply, this); + QObject::connect(watcher, SIGNAL(finished(QDBusPendingCallWatcher*)), this, + SLOT(dnsCallCompleted(QDBusPendingCallWatcher*))); + + m_ifindex = 0; + } + + return true; +} + +void DnsUtilsLinux::dnsCallCompleted(QDBusPendingCallWatcher* call) { + QDBusPendingReply<> reply = *call; + if (reply.isError()) { + logger.error() << "Error received from the DBus service"; + } + delete call; +} + +void DnsUtilsLinux::setLinkDNS(int ifindex, + const QList& resolvers) { + QList resolverList; + char ifnamebuf[IF_NAMESIZE]; + const char* ifname = if_indextoname(ifindex, ifnamebuf); + for (auto ip : resolvers) { + resolverList.append(ip); + if (ifname) { + logger.debug() << "Adding DNS resolver" << ip.toString() << "via" + << ifname; + } + } + + QList argumentList; + argumentList << QVariant::fromValue(ifindex); + argumentList << QVariant::fromValue(resolverList); + QDBusPendingReply<> reply = m_resolver->asyncCallWithArgumentList( + QStringLiteral("SetLinkDNS"), argumentList); + + QDBusPendingCallWatcher* watcher = new QDBusPendingCallWatcher(reply, this); + QObject::connect(watcher, SIGNAL(finished(QDBusPendingCallWatcher*)), this, + SLOT(dnsCallCompleted(QDBusPendingCallWatcher*))); +} + +void DnsUtilsLinux::setLinkDomains(int ifindex, + const QList& domains) { + char ifnamebuf[IF_NAMESIZE]; + const char* ifname = if_indextoname(ifindex, ifnamebuf); + if (ifname) { + for (auto d : domains) { + logger.debug() << "Setting DNS domain:" << d.domain << "via" << ifname + << (d.search ? "search" : ""); + } + } + + QList argumentList; + argumentList << QVariant::fromValue(ifindex); + argumentList << QVariant::fromValue(domains); + QDBusPendingReply<> reply = m_resolver->asyncCallWithArgumentList( + QStringLiteral("SetLinkDomains"), argumentList); + + QDBusPendingCallWatcher* watcher = new QDBusPendingCallWatcher(reply, this); + QObject::connect(watcher, SIGNAL(finished(QDBusPendingCallWatcher*)), this, + SLOT(dnsCallCompleted(QDBusPendingCallWatcher*))); +} + +void DnsUtilsLinux::setLinkDefaultRoute(int ifindex, bool enable) { + QList argumentList; + argumentList << QVariant::fromValue(ifindex); + argumentList << QVariant::fromValue(enable); + QDBusPendingReply<> reply = m_resolver->asyncCallWithArgumentList( + QStringLiteral("SetLinkDefaultRoute"), argumentList); + + QDBusPendingCallWatcher* watcher = new QDBusPendingCallWatcher(reply, this); + QObject::connect(watcher, SIGNAL(finished(QDBusPendingCallWatcher*)), this, + SLOT(dnsCallCompleted(QDBusPendingCallWatcher*))); +} + +void DnsUtilsLinux::updateLinkDomains() { + /* Get the list of search domains, and remove any others that might conspire + * to satisfy DNS resolution. Unfortunately, this is a pain because Qt doesn't + * seem to be able to demarshall complex property types. + */ + QDBusMessage message = QDBusMessage::createMethodCall( + DBUS_RESOLVE_SERVICE, DBUS_RESOLVE_PATH, DBUS_PROPERTY_INTERFACE, "Get"); + message << QString(DBUS_RESOLVE_MANAGER); + message << QString("Domains"); + QDBusPendingReply reply = + m_resolver->connection().asyncCall(message); + + QDBusPendingCallWatcher* watcher = new QDBusPendingCallWatcher(reply, this); + QObject::connect(watcher, SIGNAL(finished(QDBusPendingCallWatcher*)), this, + SLOT(dnsDomainsReceived(QDBusPendingCallWatcher*))); +} + +void DnsUtilsLinux::dnsDomainsReceived(QDBusPendingCallWatcher* call) { + QDBusPendingReply reply = *call; + if (reply.isError()) { + logger.error() << "Error retrieving the DNS domains from the DBus service"; + delete call; + return; + } + + /* Update the state of the DNS domains */ + m_linkDomains.clear(); + QDBusArgument args = qvariant_cast(reply.value()); + QList list = qdbus_cast>(args); + for (auto d : list) { + if (d.ifindex == 0) { + continue; + } + m_linkDomains[d.ifindex].append(DnsLinkDomain(d.domain, d.search)); + } + + /* Drop any competing root search domains. */ + DnsLinkDomain root = DnsLinkDomain(".", true); + for (auto ifindex : m_linkDomains.keys()) { + if (!m_linkDomains[ifindex].contains(root)) { + continue; + } + QList newlist = m_linkDomains[ifindex]; + newlist.removeAll(root); + setLinkDomains(ifindex, newlist); + } + + /* Add a root search domain for the new interface. */ + QList newlist = {root}; + setLinkDomains(m_ifindex, newlist); + delete call; +} + +static DnsMetatypeRegistrationProxy s_dnsMetatypeProxy; diff --git a/client/platforms/linux/daemon/dnsutilslinux.h b/client/platforms/linux/daemon/dnsutilslinux.h new file mode 100644 index 000000000..67c6f698d --- /dev/null +++ b/client/platforms/linux/daemon/dnsutilslinux.h @@ -0,0 +1,41 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DNSUTILSLINUX_H +#define DNSUTILSLINUX_H + +#include "daemon/dnsutils.h" +#include "dbustypeslinux.h" + +#include +#include + +class DnsUtilsLinux final : public DnsUtils { + Q_OBJECT + Q_DISABLE_COPY_MOVE(DnsUtilsLinux) + + public: + DnsUtilsLinux(QObject* parent); + ~DnsUtilsLinux(); + bool updateResolvers(const QString& ifname, + const QList& resolvers) override; + bool restoreResolvers() override; + + private: + void setLinkDNS(int ifindex, const QList& resolvers); + void setLinkDomains(int ifindex, const QList& domains); + void setLinkDefaultRoute(int ifindex, bool enable); + void updateLinkDomains(); + + private slots: + void dnsCallCompleted(QDBusPendingCallWatcher*); + void dnsDomainsReceived(QDBusPendingCallWatcher*); + + private: + int m_ifindex = 0; + QMap m_linkDomains; + QDBusInterface* m_resolver = nullptr; +}; + +#endif // DNSUTILSLINUX_H diff --git a/client/platforms/linux/daemon/iputilslinux.cpp b/client/platforms/linux/daemon/iputilslinux.cpp new file mode 100644 index 000000000..6d79d35a7 --- /dev/null +++ b/client/platforms/linux/daemon/iputilslinux.cpp @@ -0,0 +1,150 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "iputilslinux.h" + +#include "daemon/wireguardutils.h" +#include "leakdetector.h" +#include "logger.h" + +#include +#include +#include +#include + +#include +#include + +constexpr uint32_t ETH_MTU = 1500; +constexpr uint32_t WG_MTU_OVERHEAD = 80; + +namespace { +Logger logger(LOG_LINUX, "IPUtilsLinux"); +} + +IPUtilsLinux::IPUtilsLinux(QObject* parent) : IPUtils(parent) { + MVPN_COUNT_CTOR(IPUtilsLinux); + logger.debug() << "IPUtilsLinux created."; +} + +IPUtilsLinux::~IPUtilsLinux() { + MVPN_COUNT_DTOR(IPUtilsLinux); + logger.debug() << "IPUtilsLinux destroyed."; +} + +bool IPUtilsLinux::addInterfaceIPs(const InterfaceConfig& config) { + return addIP4AddressToDevice(config) && addIP6AddressToDevice(config); +} + +bool IPUtilsLinux::setMTUAndUp(const InterfaceConfig& config) { + Q_UNUSED(config); + + // Create socket file descriptor to perform the ioctl operations on + int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); + if (sockfd < 0) { + logger.error() << "Failed to create ioctl socket."; + return false; + } + auto guard = qScopeGuard([&] { close(sockfd); }); + + // Setup the interface to interact with + struct ifreq ifr; + strncpy(ifr.ifr_name, WG_INTERFACE, IFNAMSIZ); + + // MTU + // FIXME: We need to know how many layers deep this particular + // interface is into a tunnel to work effectively. Otherwise + // we will run into fragmentation issues. + ifr.ifr_mtu = ETH_MTU - WG_MTU_OVERHEAD; + int ret = ioctl(sockfd, SIOCSIFMTU, &ifr); + if (ret) { + logger.error() << "Failed to set MTU -- Return code: " << ret; + return false; + } + + // Up + ifr.ifr_flags |= (IFF_UP | IFF_RUNNING); + ret = ioctl(sockfd, SIOCSIFFLAGS, &ifr); + if (ret) { + logger.error() << "Failed to set device up -- Return code: " << ret; + return false; + } + + return true; +} + +bool IPUtilsLinux::addIP4AddressToDevice(const InterfaceConfig& config) { + struct ifreq ifr; + struct sockaddr_in* ifrAddr = (struct sockaddr_in*)&ifr.ifr_addr; + + // Name the interface and set family + strncpy(ifr.ifr_name, WG_INTERFACE, IFNAMSIZ); + ifr.ifr_addr.sa_family = AF_INET; + + // Get the device address to add to interface + QPair parsedAddr = + QHostAddress::parseSubnet(config.m_deviceIpv4Address); + QByteArray _deviceAddr = parsedAddr.first.toString().toLocal8Bit(); + char* deviceAddr = _deviceAddr.data(); + inet_pton(AF_INET, deviceAddr, &ifrAddr->sin_addr); + + // Create IPv4 socket to perform the ioctl operations on + int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); + if (sockfd < 0) { + logger.error() << "Failed to create ioctl socket."; + return false; + } + auto guard = qScopeGuard([&] { close(sockfd); }); + + // Set ifr to interface + int ret = ioctl(sockfd, SIOCSIFADDR, &ifr); + if (ret) { + logger.error() << "Failed to set IPv4: " << deviceAddr + << "error:" << strerror(errno); + return false; + } + return true; +} + +bool IPUtilsLinux::addIP6AddressToDevice(const InterfaceConfig& config) { + // Set up the ifr and the companion ifr6 + struct in6_ifreq ifr6; + ifr6.prefixlen = 64; + + // Get the device address to add to ifr6 interface + QPair parsedAddr = + QHostAddress::parseSubnet(config.m_deviceIpv6Address); + QByteArray _deviceAddr = parsedAddr.first.toString().toLocal8Bit(); + char* deviceAddr = _deviceAddr.data(); + inet_pton(AF_INET6, deviceAddr, &ifr6.addr); + + // Create IPv6 socket to perform the ioctl operations on + int sockfd = socket(AF_INET6, SOCK_DGRAM, IPPROTO_IP); + if (sockfd < 0) { + logger.error() << "Failed to create ioctl socket."; + return false; + } + auto guard = qScopeGuard([&] { close(sockfd); }); + + // Get the index of named ifr and link with ifr6 + struct ifreq ifr; + strncpy(ifr.ifr_name, WG_INTERFACE, IFNAMSIZ); + ifr.ifr_addr.sa_family = AF_INET6; + int ret = ioctl(sockfd, SIOGIFINDEX, &ifr); + if (ret) { + logger.error() << "Failed to get ifindex. Return code: " << ret; + return false; + } + ifr6.ifindex = ifr.ifr_ifindex; + + // Set ifr6 to the interface + ret = ioctl(sockfd, SIOCSIFADDR, &ifr6); + if (ret && (errno != EEXIST)) { + logger.error() << "Failed to set IPv6: " << deviceAddr + << "error:" << strerror(errno); + return false; + } + + return true; +} diff --git a/client/platforms/linux/daemon/iputilslinux.h b/client/platforms/linux/daemon/iputilslinux.h new file mode 100644 index 000000000..d10f70d79 --- /dev/null +++ b/client/platforms/linux/daemon/iputilslinux.h @@ -0,0 +1,31 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef IPUTILSLINUX_H +#define IPUTILSLINUX_H + +#include "daemon/iputils.h" + +#include + +class IPUtilsLinux final : public IPUtils { + public: + IPUtilsLinux(QObject* parent); + ~IPUtilsLinux(); + bool addInterfaceIPs(const InterfaceConfig& config) override; + bool setMTUAndUp(const InterfaceConfig& config) override; + + private: + bool addIP4AddressToDevice(const InterfaceConfig& config); + bool addIP6AddressToDevice(const InterfaceConfig& config); + + private: + struct in6_ifreq { + struct in6_addr addr; + uint32_t prefixlen; + unsigned int ifindex; + }; +}; + +#endif // IPUTILSLINUX_H \ No newline at end of file diff --git a/client/platforms/linux/daemon/linuxdaemon.cpp b/client/platforms/linux/daemon/linuxdaemon.cpp new file mode 100644 index 000000000..2a3026b6d --- /dev/null +++ b/client/platforms/linux/daemon/linuxdaemon.cpp @@ -0,0 +1,58 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "command.h" +#include "dbusservice.h" +#include "dbus_adaptor.h" +#include "leakdetector.h" +#include "logger.h" +#include "loghandler.h" +#include "signalhandler.h" + +namespace { +Logger logger(LOG_LINUX, "main"); +} + +class CommandLinuxDaemon final : public Command { + public: + explicit CommandLinuxDaemon(QObject* parent) + : Command(parent, "linuxdaemon", "Starts the linux daemon") { + MVPN_COUNT_CTOR(CommandLinuxDaemon); + } + + ~CommandLinuxDaemon() { MVPN_COUNT_DTOR(CommandLinuxDaemon); } + + int run(QStringList& tokens) override { + Q_ASSERT(!tokens.isEmpty()); + LogHandler::setLocation("/var/log"); + + return runCommandLineApp([&]() { + DBusService* dbus = new DBusService(qApp); + DbusAdaptor* adaptor = new DbusAdaptor(dbus); + dbus->setAdaptor(adaptor); + + QDBusConnection connection = QDBusConnection::systemBus(); + logger.debug() << "Connecting to DBus..."; + + if (!connection.registerService("org.mozilla.vpn.dbus") || + !connection.registerObject("/", dbus)) { + logger.error() << "Connection failed - name:" + << connection.lastError().name() + << "message:" << connection.lastError().message(); + return 1; + } + + SignalHandler sh; + QObject::connect(&sh, &SignalHandler::quitRequested, [&]() { + dbus->deactivate(); + qApp->quit(); + }); + + logger.debug() << "Ready!"; + return qApp->exec(); + }); + } +}; + +static Command::RegistrationProxy s_commandLinuxDaemon; diff --git a/client/platforms/linux/daemon/org.mozilla.vpn.conf b/client/platforms/linux/daemon/org.mozilla.vpn.conf new file mode 100644 index 000000000..83b8ad4d9 --- /dev/null +++ b/client/platforms/linux/daemon/org.mozilla.vpn.conf @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/platforms/linux/daemon/org.mozilla.vpn.dbus.service b/client/platforms/linux/daemon/org.mozilla.vpn.dbus.service new file mode 100644 index 000000000..700de1936 --- /dev/null +++ b/client/platforms/linux/daemon/org.mozilla.vpn.dbus.service @@ -0,0 +1,5 @@ +[D-BUS Service] +User=root +Name=org.mozilla.vpn.dbus +Exec=/usr/bin/mozillavpn linuxdaemon +SystemdService=mozillavpn.service diff --git a/client/platforms/linux/daemon/org.mozilla.vpn.dbus.xml b/client/platforms/linux/daemon/org.mozilla.vpn.dbus.xml new file mode 100644 index 000000000..ad3e6ba68 --- /dev/null +++ b/client/platforms/linux/daemon/org.mozilla.vpn.dbus.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/platforms/linux/daemon/org.mozilla.vpn.policy b/client/platforms/linux/daemon/org.mozilla.vpn.policy new file mode 100644 index 000000000..e29679caa --- /dev/null +++ b/client/platforms/linux/daemon/org.mozilla.vpn.policy @@ -0,0 +1,25 @@ + + + + + + Activate the Mozilla VPN + Activate the Mozilla VPN + + no + auth_admin + + + + + Deactivate the Mozilla VPN + Deactivate the Mozilla VPN + + no + auth_admin + + + + diff --git a/client/platforms/linux/daemon/pidtracker.cpp b/client/platforms/linux/daemon/pidtracker.cpp new file mode 100644 index 000000000..68f5a090d --- /dev/null +++ b/client/platforms/linux/daemon/pidtracker.cpp @@ -0,0 +1,227 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "pidtracker.h" +#include "leakdetector.h" +#include "logger.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +constexpr size_t CN_MCAST_MSG_SIZE = + sizeof(struct cn_msg) + sizeof(enum proc_cn_mcast_op); + +namespace { +Logger logger(LOG_LINUX, "PidTracker"); +} + +PidTracker::PidTracker(QObject* parent) : QObject(parent) { + MVPN_COUNT_CTOR(PidTracker); + logger.debug() << "PidTracker created."; + + m_nlsock = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_CONNECTOR); + if (m_nlsock < 0) { + logger.error() << "Failed to create netlink socket:" << strerror(errno); + return; + } + + struct sockaddr_nl nladdr; + nladdr.nl_family = AF_NETLINK; + nladdr.nl_groups = CN_IDX_PROC; + nladdr.nl_pid = getpid(); + nladdr.nl_pad = 0; + if (bind(m_nlsock, (struct sockaddr*)&nladdr, sizeof(nladdr)) < 0) { + logger.error() << "Failed to bind netlink socket:" << strerror(errno); + close(m_nlsock); + m_nlsock = -1; + return; + } + + char buf[NLMSG_SPACE(CN_MCAST_MSG_SIZE)]; + struct nlmsghdr* nlmsg = (struct nlmsghdr*)buf; + struct cn_msg* cnmsg = (struct cn_msg*)NLMSG_DATA(nlmsg); + enum proc_cn_mcast_op mcast_op = PROC_CN_MCAST_LISTEN; + + memset(buf, 0, sizeof(buf)); + nlmsg->nlmsg_len = NLMSG_LENGTH(CN_MCAST_MSG_SIZE); + nlmsg->nlmsg_type = NLMSG_DONE; + nlmsg->nlmsg_flags = 0; + nlmsg->nlmsg_seq = 0; + nlmsg->nlmsg_pid = getpid(); + + cnmsg->id.idx = CN_IDX_PROC; + cnmsg->id.val = CN_VAL_PROC; + cnmsg->seq = 0; + cnmsg->ack = 0; + cnmsg->len = sizeof(mcast_op); + memcpy(cnmsg->data, &mcast_op, sizeof(mcast_op)); + + if (send(m_nlsock, nlmsg, sizeof(buf), 0) != sizeof(buf)) { + logger.error() << "Failed to send netlink message:" << strerror(errno); + close(m_nlsock); + m_nlsock = -1; + return; + } + + m_socket = new QSocketNotifier(m_nlsock, QSocketNotifier::Read, this); + connect(m_socket, &QSocketNotifier::activated, this, &PidTracker::readData); +} + +PidTracker::~PidTracker() { + MVPN_COUNT_DTOR(PidTracker); + logger.debug() << "PidTracker destroyed."; + + m_processTree.clear(); + while (!m_processGroups.isEmpty()) { + ProcessGroup* group = m_processGroups.takeFirst(); + delete group; + } + + if (m_nlsock > 0) { + close(m_nlsock); + } +} + +ProcessGroup* PidTracker::track(const QString& name, int rootpid) { + ProcessGroup* group = m_processTree.value(rootpid, nullptr); + if (group) { + logger.warning() << "Ignoring attempt to track duplicate PID"; + return group; + } + group = new ProcessGroup(name, rootpid); + group->kthreads[rootpid] = 1; + group->refcount = 1; + + m_processGroups.append(group); + m_processTree[rootpid] = group; + + return group; +} + +void PidTracker::handleProcEvent(struct cn_msg* cnmsg) { + struct proc_event* ev = (struct proc_event*)cnmsg->data; + + if (ev->what == proc_event::PROC_EVENT_FORK) { + auto forkdata = &ev->event_data.fork; + /* If the child process already exists, track a new kernel thread. */ + ProcessGroup* group = m_processTree.value(forkdata->child_tgid, nullptr); + if (group) { + group->kthreads[forkdata->child_tgid]++; + return; + } + + /* Track a new userspace process if was forked from a known parent. */ + group = m_processTree.value(forkdata->parent_tgid, nullptr); + if (!group) { + return; + } + m_processTree[forkdata->child_tgid] = group; + group->kthreads[forkdata->child_tgid] = 1; + group->refcount++; + emit pidForked(group->name, forkdata->parent_tgid, forkdata->child_tgid); + } + + if (ev->what == proc_event::PROC_EVENT_EXIT) { + auto exitdata = &ev->event_data.exit; + ProcessGroup* group = m_processTree.value(exitdata->process_tgid, nullptr); + if (!group) { + return; + } + + /* Decrement the number of kernel threads in this userspace process. */ + uint threadcount = group->kthreads.value(exitdata->process_tgid, 0); + if (threadcount == 0) { + return; + } + if (threadcount > 1) { + group->kthreads[exitdata->process_tgid] = threadcount - 1; + return; + } + group->kthreads.remove(exitdata->process_tgid); + + /* A userspace process exits when all of its kernel threads exit. */ + Q_ASSERT(group->refcount > 0); + group->refcount--; + if (group->refcount == 0) { + emit terminated(group->name, group->rootpid); + m_processGroups.removeAll(group); + delete group; + } + } +} + +void PidTracker::readData() { + struct sockaddr_nl src; + socklen_t srclen = sizeof(src); + ssize_t recvlen; + + recvlen = recvfrom(m_nlsock, m_readBuf, sizeof(m_readBuf), MSG_DONTWAIT, + (struct sockaddr*)&src, &srclen); + if (recvlen == ENOBUFS) { + logger.error() + << "Failed to read netlink socket: buffer full, message dropped"; + return; + } + if (recvlen < 0) { + logger.error() << "Failed to read netlink socket:" << strerror(errno); + return; + } + if (srclen != sizeof(src)) { + logger.error() << "Failed to read netlink socket: invalid address length"; + return; + } + + /* We are only interested in process-control messages from the kernel */ + if ((src.nl_groups != CN_IDX_PROC) || (src.nl_pid != 0)) { + return; + } + + /* Handle the process-control messages. */ + struct nlmsghdr* msg; + for (msg = (struct nlmsghdr*)m_readBuf; NLMSG_OK(msg, recvlen); + msg = NLMSG_NEXT(msg, recvlen)) { + struct cn_msg* cnmsg = (struct cn_msg*)NLMSG_DATA(msg); + if (msg->nlmsg_type == NLMSG_NOOP) { + continue; + } + if ((msg->nlmsg_type == NLMSG_ERROR) || + (msg->nlmsg_type == NLMSG_OVERRUN)) { + break; + } + handleProcEvent(cnmsg); + if (msg->nlmsg_type == NLMSG_DONE) { + break; + } + } +} + +bool ProcessGroup::moveToCgroup(const QString& name) { + /* Do nothing if Cgroups are not supported. */ + if (name.isNull()) { + return true; + } + + QString cgProcsFile = name + "/cgroup.procs"; + FILE* fp = fopen(qPrintable(cgProcsFile), "w"); + if (!fp) { + return false; + } + + for (auto pid : kthreads.keys()) { + fprintf(fp, "%d\n", pid); + fflush(fp); + } + fclose(fp); + return true; +} diff --git a/client/platforms/linux/daemon/pidtracker.h b/client/platforms/linux/daemon/pidtracker.h new file mode 100644 index 000000000..42b2313e9 --- /dev/null +++ b/client/platforms/linux/daemon/pidtracker.h @@ -0,0 +1,72 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef PIDTRACKER_H +#define PIDTRACKER_H + +#include +#include +#include +#include + +#include "leakdetector.h" + +struct cn_msg; + +class ProcessGroup { + public: + ProcessGroup(const QString& groupName, int groupRootPid, + const QString& groupState = "active") { + MVPN_COUNT_CTOR(ProcessGroup); + name = groupName; + rootpid = groupRootPid; + state = groupState; + refcount = 0; + } + ~ProcessGroup() { MVPN_COUNT_DTOR(ProcessGroup); } + + bool moveToCgroup(const QString& name); + + QHash kthreads; + QString name; + QString state; + int rootpid; + int refcount; +}; + +class PidTracker final : public QObject { + Q_OBJECT + Q_DISABLE_COPY_MOVE(PidTracker) + + public: + explicit PidTracker(QObject* parent); + ~PidTracker(); + + ProcessGroup* track(const QString& name, int rootpid); + + QList pids() { return m_processTree.keys(); } + QList::iterator begin() { return m_processGroups.begin(); } + QList::iterator end() { return m_processGroups.end(); } + ProcessGroup* group(int pid) { return m_processTree.value(pid); } + + signals: + void pidForked(const QString& name, int parent, int child); + void pidExited(const QString& name, int pid); + void terminated(const QString& name, int rootpid); + + private: + void handleProcEvent(struct cn_msg*); + + private slots: + void readData(); + + private: + int m_nlsock; + char m_readBuf[2048]; + QSocketNotifier* m_socket = nullptr; + QHash m_processTree; + QList m_processGroups; +}; + +#endif // PIDTRACKER_H diff --git a/client/platforms/linux/daemon/polkithelper.cpp b/client/platforms/linux/daemon/polkithelper.cpp new file mode 100644 index 000000000..34838de0e --- /dev/null +++ b/client/platforms/linux/daemon/polkithelper.cpp @@ -0,0 +1,66 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "polkithelper.h" + +#include + +// No extra QT includes after this line! +#undef Q_SIGNALS +#include "polkit/polkit.h" + +class Helper final { + public: + Helper() = default; + ~Helper() { + if (m_error) { + g_error_free(m_error); + } + + if (m_subject) { + g_object_unref(m_subject); + } + + if (m_result) { + g_object_unref(m_result); + } + } + + public: + GError* m_error = nullptr; + PolkitSubject* m_subject = nullptr; + PolkitAuthorizationResult* m_result = nullptr; +}; + +// static +PolkitHelper* PolkitHelper::instance() { + static PolkitHelper s_instance; + return &s_instance; +} + +bool PolkitHelper::checkAuthorization(const QString& actionId) { + qDebug() << "Check Authorization for" << actionId; + + Helper h; + + PolkitAuthority* authority = polkit_authority_get_sync(NULL, &h.m_error); + if (h.m_error) { + qDebug() << "Fail to generate a polkit authority object:" + << h.m_error->message; + return false; + } + + h.m_subject = polkit_unix_process_new_for_owner(getpid(), 0, -1); + + h.m_result = polkit_authority_check_authorization_sync( + authority, h.m_subject, actionId.toLatin1().data(), nullptr, + POLKIT_CHECK_AUTHORIZATION_FLAGS_ALLOW_USER_INTERACTION, nullptr, + &h.m_error); + if (h.m_error) { + qDebug() << "Authorization sync failed:" << h.m_error->message; + return false; + } + + return polkit_authorization_result_get_is_authorized(h.m_result); +} diff --git a/client/platforms/linux/daemon/polkithelper.h b/client/platforms/linux/daemon/polkithelper.h new file mode 100644 index 000000000..2be4f2b05 --- /dev/null +++ b/client/platforms/linux/daemon/polkithelper.h @@ -0,0 +1,23 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef POLKITHELPER_H +#define POLKITHELPER_H + +#include + +class PolkitHelper final { + public: + static PolkitHelper* instance(); + + bool checkAuthorization(const QString& actionId); + + private: + PolkitHelper() = default; + ~PolkitHelper() = default; + + Q_DISABLE_COPY(PolkitHelper) +}; + +#endif // POLKITHELPER_H diff --git a/client/platforms/linux/daemon/wireguardutilslinux.cpp b/client/platforms/linux/daemon/wireguardutilslinux.cpp new file mode 100644 index 000000000..1d0906209 --- /dev/null +++ b/client/platforms/linux/daemon/wireguardutilslinux.cpp @@ -0,0 +1,648 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "wireguardutilslinux.h" +#include "leakdetector.h" +#include "logger.h" +#include "platforms/linux/linuxdependencies.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Import wireguard C library for Linux +#if defined(__cplusplus) +extern "C" { +#endif +#include "../../3rdparty/wireguard-tools/contrib/embeddable-wg-library/wireguard.h" +#include "../../linux/netfilter/netfilter.h" +#if defined(__cplusplus) +} +#endif +// End import wireguard + +/* Packets sent outside the VPN need to be marked for the routing policy + * to direct them appropriately. The value of the mark and the table ID + * aren't important, so long as they are unique. + */ +constexpr uint32_t WG_FIREWALL_MARK = 0xca6c; +constexpr uint32_t WG_ROUTE_TABLE = 0xca6c; + +/* Traffic classifiers can be used to mark packets which should be either + * excluded from the VPN tunnel, or blocked entirely. The values of these + * classifiers aren't important so long as they are unique. + */ +constexpr const char* VPN_EXCLUDE_CGROUP = "/mozvpn.exclude"; +constexpr const char* VPN_BLOCK_CGROUP = "/mozvpn.block"; +constexpr uint32_t VPN_EXCLUDE_CLASS_ID = 0x00110011; +constexpr uint32_t VPN_BLOCK_CLASS_ID = 0x00220022; + +static void nlmsg_append_attr(char* buf, size_t maxlen, int attrtype, + const void* attrdata, size_t attrlen); +static void nlmsg_append_attr32(char* buf, size_t maxlen, int attrtype, + uint32_t value); + +namespace { +Logger logger(LOG_LINUX, "WireguardUtilsLinux"); + +void NetfilterLogger(int level, const char* msg) { + Q_UNUSED(level); + logger.debug() << "NetfilterGo:" << msg; +} +} // namespace + +WireguardUtilsLinux::WireguardUtilsLinux(QObject* parent) + : WireguardUtils(parent) { + MVPN_COUNT_CTOR(WireguardUtilsLinux); + NetfilterSetLogger((GoUintptr)&NetfilterLogger); + NetfilterCreateTables(); + + m_nlsock = socket(AF_NETLINK, SOCK_DGRAM, NETLINK_ROUTE); + if (m_nlsock < 0) { + logger.warning() << "Failed to create netlink socket:" << strerror(errno); + } + + struct sockaddr_nl nladdr; + memset(&nladdr, 0, sizeof(nladdr)); + nladdr.nl_family = AF_NETLINK; + nladdr.nl_pid = getpid(); + if (bind(m_nlsock, (struct sockaddr*)&nladdr, sizeof(nladdr)) != 0) { + logger.warning() << "Failed to bind netlink socket:" << strerror(errno); + } + + m_notifier = new QSocketNotifier(m_nlsock, QSocketNotifier::Read, this); + connect(m_notifier, &QSocketNotifier::activated, this, + &WireguardUtilsLinux::nlsockReady); + + /* Create control groups for split tunnelling */ + m_cgroups = LinuxDependencies::findCgroupPath("net_cls"); + if (!m_cgroups.isNull()) { + if (!setupCgroupClass(m_cgroups + VPN_EXCLUDE_CGROUP, + VPN_EXCLUDE_CLASS_ID)) { + m_cgroups.clear(); + } else if (!setupCgroupClass(m_cgroups + VPN_BLOCK_CGROUP, + VPN_BLOCK_CLASS_ID)) { + m_cgroups.clear(); + } + } + + logger.debug() << "WireguardUtilsLinux created."; +} + +WireguardUtilsLinux::~WireguardUtilsLinux() { + MVPN_COUNT_DTOR(WireguardUtilsLinux); + NetfilterRemoveTables(); + if (m_nlsock >= 0) { + close(m_nlsock); + } + logger.debug() << "WireguardUtilsLinux destroyed."; +} + +bool WireguardUtilsLinux::interfaceExists() { + // As currentInterfaces only gets wireguard interfaces, this method + // also confirms an interface as being a wireguard interface. + return currentInterfaces().contains(WG_INTERFACE); +}; + +bool WireguardUtilsLinux::addInterface(const InterfaceConfig& config) { + int code = wg_add_device(WG_INTERFACE); + if (code != 0) { + logger.error() << "Adding interface failed:" << strerror(-code); + return false; + } + + wg_device* device = static_cast(calloc(1, sizeof(*device))); + if (!device) { + logger.error() << "Allocation failure"; + return false; + } + auto guard = qScopeGuard([&] { wg_free_device(device); }); + + // Name + strncpy(device->name, WG_INTERFACE, IFNAMSIZ); + // Private Key + wg_key_from_base64(device->private_key, config.m_privateKey.toLocal8Bit()); + + // Set/update device + device->fwmark = WG_FIREWALL_MARK; + device->flags = (wg_device_flags)( + WGDEVICE_HAS_PRIVATE_KEY | WGDEVICE_REPLACE_PEERS | WGDEVICE_HAS_FWMARK); + if (wg_set_device(device) != 0) { + logger.error() << "Failed to setup the device"; + return false; + } + + // Create routing policy rules + if (!rtmSendRule(RTM_NEWRULE, + NLM_F_REQUEST | NLM_F_CREATE | NLM_F_REPLACE | NLM_F_ACK, + AF_INET)) { + return false; + } + if (!rtmSendRule(RTM_NEWRULE, + NLM_F_REQUEST | NLM_F_CREATE | NLM_F_REPLACE | NLM_F_ACK, + AF_INET6)) { + return false; + } + + // Configure firewall rules + GoString goIfname = {.p = device->name, .n = (ptrdiff_t)strlen(device->name)}; + if (NetfilterIfup(goIfname, device->fwmark) != 0) { + return false; + } + if (!m_cgroups.isNull()) { + NetfilterMarkCgroup(VPN_EXCLUDE_CLASS_ID, device->fwmark); + NetfilterBlockCgroup(VPN_BLOCK_CLASS_ID); + } + + int slashPos = config.m_deviceIpv6Address.indexOf('/'); + GoString goIpv6Address = {.p = qPrintable(config.m_deviceIpv6Address), + .n = config.m_deviceIpv6Address.length()}; + if (slashPos != -1) { + goIpv6Address.n = slashPos; + } + NetfilterIsolateIpv6(goIfname, goIpv6Address); + + return true; +} + +bool WireguardUtilsLinux::updatePeer(const InterfaceConfig& config) { + wg_device* device = static_cast(calloc(1, sizeof(*device))); + if (!device) { + logger.error() << "Allocation failure"; + return false; + } + auto guard = qScopeGuard([&] { wg_free_device(device); }); + + wg_peer* peer = static_cast(calloc(1, sizeof(*peer))); + if (!peer) { + logger.error() << "Allocation failure"; + return false; + } + device->first_peer = device->last_peer = peer; + + logger.debug() << "Adding peer" << printablePubkey(config.m_serverPublicKey); + + // Public Key + wg_key_from_base64(peer->public_key, qPrintable(config.m_serverPublicKey)); + // Endpoint + if (!setPeerEndpoint(&peer->endpoint.addr, config.m_serverIpv4AddrIn, + config.m_serverPort)) { + logger.error() << "Failed to set peer endpoint for hop" + << config.m_hopindex; + return false; + } + + // HACK: We are running into a crash on Linux due to the address list being + // *WAAAY* too long, which we aren't really using anways since the routing + // tables are doing all the work for us anyways. + // + // To work around the issue, just set default routes for hopindex zero. + if (config.m_hopindex == 0) { + if (!config.m_deviceIpv4Address.isNull()) { + addPeerPrefix(peer, IPAddressRange("0.0.0.0", 0, IPAddressRange::IPv4)); + } + if (!config.m_deviceIpv6Address.isNull()) { + addPeerPrefix(peer, IPAddressRange("::", 0, IPAddressRange::IPv6)); + } + } else { + for (const IPAddressRange& ip : config.m_allowedIPAddressRanges) { + bool ok = addPeerPrefix(peer, ip); + if (!ok) { + logger.error() << "Invalid IP address:" << ip.ipAddress(); + return false; + } + } + } + + // Set/update peer + strncpy(device->name, WG_INTERFACE, IFNAMSIZ); + device->flags = (wg_device_flags)0; + peer->flags = + (wg_peer_flags)(WGPEER_HAS_PUBLIC_KEY | WGPEER_REPLACE_ALLOWEDIPS); + if (wg_set_device(device) != 0) { + logger.error() << "Failed to set the new peer hop" << config.m_hopindex; + return false; + } + + return true; +} + +bool WireguardUtilsLinux::deletePeer(const QString& pubkey) { + wg_device* device = static_cast(calloc(1, sizeof(*device))); + if (!device) { + logger.error() << "Allocation failure"; + return false; + } + auto guard = qScopeGuard([&] { wg_free_device(device); }); + + wg_peer* peer = static_cast(calloc(1, sizeof(*peer))); + if (!peer) { + logger.error() << "Allocation failure"; + return false; + } + device->first_peer = device->last_peer = peer; + + logger.debug() << "Removing peer" << printablePubkey(pubkey); + + // Public Key + peer->flags = (wg_peer_flags)(WGPEER_HAS_PUBLIC_KEY | WGPEER_REMOVE_ME); + wg_key_from_base64(peer->public_key, qPrintable(pubkey)); + + // Set/update device + strncpy(device->name, WG_INTERFACE, IFNAMSIZ); + device->flags = (wg_device_flags)0; + if (wg_set_device(device) != 0) { + logger.error() << "Failed to remove the peer"; + return false; + } + + return true; +} + +bool WireguardUtilsLinux::deleteInterface() { + // Clear firewall rules + NetfilterClearTables(); + + // Clear routing policy rules + if (!rtmSendRule(RTM_DELRULE, NLM_F_REQUEST | NLM_F_ACK, AF_INET)) { + return false; + } + if (!rtmSendRule(RTM_DELRULE, NLM_F_REQUEST | NLM_F_ACK, AF_INET6)) { + return false; + } + + // Delete the interface + int returnCode = wg_del_device(WG_INTERFACE); + if (returnCode != 0) { + logger.error() << "Deleting interface failed:" << strerror(-returnCode); + return false; + } + + return true; +} + +WireguardUtils::peerStatus WireguardUtilsLinux::getPeerStatus( + const QString& pubkey) { + wg_device* device = nullptr; + wg_peer* peer = nullptr; + peerStatus status = {0, 0}; + + if (wg_get_device(&device, WG_INTERFACE) != 0) { + logger.warning() << "Unable to get stats for" << WG_INTERFACE; + return status; + } + + wg_key key; + wg_key_from_base64(key, qPrintable(pubkey)); + wg_for_each_peer(device, peer) { + if (memcmp(&key, &peer->public_key, sizeof(key)) != 0) { + continue; + } + status.txBytes = peer->tx_bytes; + status.rxBytes = peer->rx_bytes; + break; + } + wg_free_device(device); + return status; +} + +bool WireguardUtilsLinux::updateRoutePrefix(const IPAddressRange& prefix, + int hopindex) { + logger.debug() << "Adding route to" << prefix.toString(); + int flags = NLM_F_REQUEST | NLM_F_CREATE | NLM_F_REPLACE | NLM_F_ACK; + return rtmSendRoute(RTM_NEWROUTE, flags, prefix, hopindex); +} + +bool WireguardUtilsLinux::deleteRoutePrefix(const IPAddressRange& prefix, + int hopindex) { + logger.debug() << "Removing route to" << prefix.toString(); + int flags = NLM_F_REQUEST | NLM_F_ACK; + return rtmSendRoute(RTM_DELROUTE, flags, prefix, hopindex); +} + +bool WireguardUtilsLinux::rtmSendRoute(int action, int flags, + const IPAddressRange& prefix, + int hopindex) { + constexpr size_t rtm_max_size = sizeof(struct rtmsg) + + 2 * RTA_SPACE(sizeof(uint32_t)) + + RTA_SPACE(sizeof(struct in6_addr)); + int index = if_nametoindex(WG_INTERFACE); + if (index <= 0) { + logger.error() << "if_nametoindex() failed:" << strerror(errno); + return false; + } + + wg_allowedip ip; + if (!buildAllowedIp(&ip, prefix)) { + logger.warning() << "Invalid destination prefix"; + return false; + } + + char buf[NLMSG_SPACE(rtm_max_size)]; + struct nlmsghdr* nlmsg = (struct nlmsghdr*)buf; + struct rtmsg* rtm = (struct rtmsg*)NLMSG_DATA(nlmsg); + + memset(buf, 0, sizeof(buf)); + nlmsg->nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg)); + nlmsg->nlmsg_type = action; + nlmsg->nlmsg_flags = flags; + nlmsg->nlmsg_pid = getpid(); + nlmsg->nlmsg_seq = m_nlseq++; + rtm->rtm_dst_len = ip.cidr; + rtm->rtm_family = ip.family; + rtm->rtm_type = RTN_UNICAST; + rtm->rtm_protocol = RTPROT_BOOT; + rtm->rtm_scope = RT_SCOPE_UNIVERSE; + + // Routes for the main hop should be placed into their own table. + if (hopindex == 0) { + rtm->rtm_table = RT_TABLE_UNSPEC; + nlmsg_append_attr32(buf, sizeof(buf), RTA_TABLE, WG_ROUTE_TABLE); + } else { + rtm->rtm_table = RT_TABLE_MAIN; + } + + if (rtm->rtm_family == AF_INET6) { + nlmsg_append_attr(buf, sizeof(buf), RTA_DST, &ip.ip6, sizeof(ip.ip6)); + } else { + nlmsg_append_attr(buf, sizeof(buf), RTA_DST, &ip.ip4, sizeof(ip.ip4)); + } + nlmsg_append_attr32(buf, sizeof(buf), RTA_OIF, index); + + struct sockaddr_nl nladdr; + memset(&nladdr, 0, sizeof(nladdr)); + nladdr.nl_family = AF_NETLINK; + size_t result = sendto(m_nlsock, buf, nlmsg->nlmsg_len, 0, + (struct sockaddr*)&nladdr, sizeof(nladdr)); + return (result == nlmsg->nlmsg_len); +} + +// PRIVATE METHODS +QStringList WireguardUtilsLinux::currentInterfaces() { + char* deviceNames = wg_list_device_names(); + QStringList devices; + if (!deviceNames) { + return devices; + } + char* deviceName; + size_t len; + wg_for_each_device_name(deviceNames, deviceName, len) { + devices.append(deviceName); + } + free(deviceNames); + return devices; +} + +bool WireguardUtilsLinux::setPeerEndpoint(struct sockaddr* sa, + const QString& address, int port) { + QString portString = QString::number(port); + + struct addrinfo hints; + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_DGRAM; + hints.ai_protocol = IPPROTO_UDP; + + struct addrinfo* resolved = nullptr; + auto guard = qScopeGuard([&] { freeaddrinfo(resolved); }); + int retries = 15; + + for (unsigned int timeout = 1000000;; + timeout = std::min((unsigned int)20000000, timeout * 6 / 5)) { + int rv = getaddrinfo(address.toLocal8Bit(), portString.toLocal8Bit(), + &hints, &resolved); + if (!rv) { + break; + } + + /* The set of return codes that are "permanent failures". All other + * possibilities are potentially transient. + * + * This is according to https://sourceware.org/glibc/wiki/NameResolver which + * states: "From the perspective of the application that calls getaddrinfo() + * it perhaps doesn't matter that much since EAI_FAIL, EAI_NONAME and + * EAI_NODATA are all permanent failure codes and the causes are all + * permanent failures in the sense that there is no point in retrying + * later." + * + * So this is what we do, except FreeBSD removed EAI_NODATA some time ago, + * so that's conditional. + */ + if (rv == EAI_NONAME || rv == EAI_FAIL || +#ifdef EAI_NODATA + rv == EAI_NODATA || +#endif + (retries >= 0 && !retries--)) { + logger.error() << "Failed to resolve the address endpoint"; + return false; + } + + logger.warning() << "Trying again in" << (timeout / 1000000.0) << "seconds"; + usleep(timeout); + } + + if ((resolved->ai_family == AF_INET && + resolved->ai_addrlen == sizeof(struct sockaddr_in)) || + (resolved->ai_family == AF_INET6 && + resolved->ai_addrlen == sizeof(struct sockaddr_in6))) { + memcpy(sa, resolved->ai_addr, resolved->ai_addrlen); + return true; + } + + logger.error() << "Invalid endpoint" << address; + return false; +} + +bool WireguardUtilsLinux::addPeerPrefix(wg_peer* peer, + const IPAddressRange& prefix) { + Q_ASSERT(peer); + + wg_allowedip* allowedip = + static_cast(calloc(1, sizeof(*allowedip))); + if (!allowedip) { + logger.error() << "Allocation failure"; + return false; + } + + if (!peer->first_allowedip) { + peer->first_allowedip = allowedip; + } else { + peer->last_allowedip->next_allowedip = allowedip; + } + peer->last_allowedip = allowedip; + + return buildAllowedIp(allowedip, prefix); +} + +static void nlmsg_append_attr(char* buf, size_t maxlen, int attrtype, + const void* attrdata, size_t attrlen) { + struct nlmsghdr* nlmsg = (struct nlmsghdr*)buf; + size_t newlen = NLMSG_ALIGN(nlmsg->nlmsg_len) + RTA_SPACE(attrlen); + if (newlen <= maxlen) { + struct rtattr* attr = (struct rtattr*)(buf + NLMSG_ALIGN(nlmsg->nlmsg_len)); + attr->rta_type = attrtype; + attr->rta_len = RTA_LENGTH(attrlen); + memcpy(RTA_DATA(attr), attrdata, attrlen); + nlmsg->nlmsg_len = newlen; + } +} + +static void nlmsg_append_attr32(char* buf, size_t maxlen, int attrtype, + uint32_t value) { + nlmsg_append_attr(buf, maxlen, attrtype, &value, sizeof(value)); +} + +bool WireguardUtilsLinux::rtmSendRule(int action, int flags, int addrfamily) { + constexpr size_t fib_max_size = + sizeof(struct fib_rule_hdr) + 2 * RTA_SPACE(sizeof(uint32_t)); + + char buf[NLMSG_SPACE(fib_max_size)]; + struct nlmsghdr* nlmsg = (struct nlmsghdr*)buf; + struct fib_rule_hdr* rule = (struct fib_rule_hdr*)NLMSG_DATA(nlmsg); + struct sockaddr_nl nladdr; + memset(&nladdr, 0, sizeof(nladdr)); + nladdr.nl_family = AF_NETLINK; + + /* Create a routing policy rule to select the wireguard routing table for + * unmarked packets. This is equivalent to: + * ip rule add not fwmark $WG_FIREWALL_MARK table $WG_ROUTE_TABLE + */ + memset(buf, 0, sizeof(buf)); + nlmsg->nlmsg_len = NLMSG_LENGTH(sizeof(struct fib_rule_hdr)); + nlmsg->nlmsg_type = action; + nlmsg->nlmsg_flags = flags; + nlmsg->nlmsg_pid = getpid(); + nlmsg->nlmsg_seq = m_nlseq++; + rule->family = addrfamily; + rule->table = RT_TABLE_UNSPEC; + rule->action = FR_ACT_TO_TBL; + rule->flags = FIB_RULE_INVERT; + nlmsg_append_attr32(buf, sizeof(buf), FRA_FWMARK, WG_FIREWALL_MARK); + nlmsg_append_attr32(buf, sizeof(buf), FRA_TABLE, WG_ROUTE_TABLE); + ssize_t result = sendto(m_nlsock, buf, nlmsg->nlmsg_len, 0, + (struct sockaddr*)&nladdr, sizeof(nladdr)); + if (result != nlmsg->nlmsg_len) { + return false; + } + + /* Create a routing policy rule to suppress zero-length prefix lookups from + * in the main routing table. This is equivalent to: + * ip rule add table main suppress_prefixlength 0 + */ + memset(buf, 0, sizeof(buf)); + nlmsg->nlmsg_len = NLMSG_LENGTH(sizeof(struct fib_rule_hdr)); + nlmsg->nlmsg_type = action; + nlmsg->nlmsg_flags = flags; + nlmsg->nlmsg_pid = getpid(); + nlmsg->nlmsg_seq = m_nlseq++; + rule->family = addrfamily; + rule->table = RT_TABLE_MAIN; + rule->action = FR_ACT_TO_TBL; + rule->flags = 0; + nlmsg_append_attr32(buf, sizeof(buf), FRA_SUPPRESS_PREFIXLEN, 0); + result = sendto(m_nlsock, buf, nlmsg->nlmsg_len, 0, (struct sockaddr*)&nladdr, + sizeof(nladdr)); + if (result != nlmsg->nlmsg_len) { + return false; + } + + return true; +} + +void WireguardUtilsLinux::nlsockReady() { + char buf[1024]; + ssize_t len = recv(m_nlsock, buf, sizeof(buf), MSG_DONTWAIT); + if (len <= 0) { + return; + } + + struct nlmsghdr* nlmsg = (struct nlmsghdr*)buf; + while (NLMSG_OK(nlmsg, len)) { + if (nlmsg->nlmsg_type == NLMSG_DONE) { + return; + } + if (nlmsg->nlmsg_type != NLMSG_ERROR) { + nlmsg = NLMSG_NEXT(nlmsg, len); + continue; + } + struct nlmsgerr* err = (struct nlmsgerr*)NLMSG_DATA(nlmsg); + if (err->error != 0) { + logger.debug() << "Netlink request failed:" << strerror(-err->error); + } + nlmsg = NLMSG_NEXT(nlmsg, len); + } +} + +// static +bool WireguardUtilsLinux::setupCgroupClass(const QString& path, + unsigned long classid) { + logger.debug() << "Creating control group:" << path; + int flags = S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH; + int err = mkdir(qPrintable(path), flags); + if ((err < 0) && (errno != EEXIST)) { + logger.error() << "Failed to create" << path + ":" << strerror(errno); + return false; + } + + QString netClassPath = path + "/net_cls.classid"; + FILE* fp = fopen(qPrintable(netClassPath), "w"); + if (!fp) { + logger.error() << "Failed to set classid:" << strerror(errno); + return false; + } + fprintf(fp, "%lu", classid); + fclose(fp); + return true; +} + +QString WireguardUtilsLinux::getExcludeCgroup() const { + if (m_cgroups.isNull()) { + return QString(); + } + return m_cgroups + VPN_EXCLUDE_CGROUP; +} + +QString WireguardUtilsLinux::getBlockCgroup() const { + if (m_cgroups.isNull()) { + return QString(); + } + return m_cgroups + VPN_BLOCK_CGROUP; +} + +// static +bool WireguardUtilsLinux::buildAllowedIp(wg_allowedip* ip, + const IPAddressRange& prefix) { + if (prefix.type() == IPAddressRange::IPv4) { + ip->family = AF_INET; + ip->cidr = prefix.range(); + return inet_pton(AF_INET, qPrintable(prefix.ipAddress()), &ip->ip4) == 1; + } + if (prefix.type() == IPAddressRange::IPv6) { + ip->family = AF_INET6; + ip->cidr = prefix.range(); + return inet_pton(AF_INET6, qPrintable(prefix.ipAddress()), &ip->ip6) == 1; + } + return false; +} + +// static +QString WireguardUtilsLinux::printablePubkey(const QString& pubkey) { + if (pubkey.length() < 12) { + return pubkey; + } else { + return pubkey.left(6) + "..." + pubkey.right(6); + } +} diff --git a/client/platforms/linux/daemon/wireguardutilslinux.h b/client/platforms/linux/daemon/wireguardutilslinux.h new file mode 100644 index 000000000..9cb22b900 --- /dev/null +++ b/client/platforms/linux/daemon/wireguardutilslinux.h @@ -0,0 +1,56 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef WIREGUARDUTILSLINUX_H +#define WIREGUARDUTILSLINUX_H + +#include "daemon/wireguardutils.h" +#include +#include +#include + +class WireguardUtilsLinux final : public WireguardUtils { + Q_OBJECT + + public: + WireguardUtilsLinux(QObject* parent); + ~WireguardUtilsLinux(); + bool interfaceExists() override; + bool addInterface(const InterfaceConfig& config) override; + bool deleteInterface() override; + + bool updatePeer(const InterfaceConfig& config) override; + bool deletePeer(const QString& pubkey) override; + peerStatus getPeerStatus(const QString& pubkey) override; + + bool updateRoutePrefix(const IPAddressRange& prefix, int hopindex) override; + bool deleteRoutePrefix(const IPAddressRange& prefix, int hopindex) override; + + QString getDefaultCgroup() const { return m_cgroups; } + QString getExcludeCgroup() const; + QString getBlockCgroup() const; + + private: + QStringList currentInterfaces(); + bool setPeerEndpoint(struct sockaddr* sa, const QString& address, int port); + bool addPeerPrefix(struct wg_peer* peer, const IPAddressRange& prefix); + bool rtmSendRule(int action, int flags, int addrfamily); + bool rtmSendRoute(int action, int flags, const IPAddressRange& prefix, + int hopindex); + static bool setupCgroupClass(const QString& path, unsigned long classid); + static bool buildAllowedIp(struct wg_allowedip*, + const IPAddressRange& prefix); + + static QString printablePubkey(const QString& pubkey); + + int m_nlsock = -1; + int m_nlseq = 0; + QSocketNotifier* m_notifier = nullptr; + QString m_cgroups; + + private slots: + void nlsockReady(); +}; + +#endif // WIREGUARDUTILSLINUX_H diff --git a/client/platforms/linux/dbusclient.cpp b/client/platforms/linux/dbusclient.cpp new file mode 100644 index 000000000..291e5c25e --- /dev/null +++ b/client/platforms/linux/dbusclient.cpp @@ -0,0 +1,126 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "dbusclient.h" +#include "ipaddressrange.h" +#include "leakdetector.h" +#include "logger.h" +#include "models/device.h" +#include "models/keys.h" +#include "models/server.h" +#include "mozillavpn.h" +#include "settingsholder.h" + +#include +#include +#include + +constexpr const char* DBUS_SERVICE = "org.mozilla.vpn.dbus"; +constexpr const char* DBUS_PATH = "/"; + +namespace { +Logger logger(LOG_LINUX, "DBusClient"); +} + +DBusClient::DBusClient(QObject* parent) : QObject(parent) { + MVPN_COUNT_CTOR(DBusClient); + + m_dbus = new OrgMozillaVpnDbusInterface(DBUS_SERVICE, DBUS_PATH, + QDBusConnection::systemBus(), this); + + connect(m_dbus, &OrgMozillaVpnDbusInterface::connected, this, + &DBusClient::connected); + connect(m_dbus, &OrgMozillaVpnDbusInterface::disconnected, this, + &DBusClient::disconnected); +} + +DBusClient::~DBusClient() { MVPN_COUNT_DTOR(DBusClient); } + +QDBusPendingCallWatcher* DBusClient::version() { + logger.debug() << "Version via DBus"; + QDBusPendingReply reply = m_dbus->version(); + QDBusPendingCallWatcher* watcher = new QDBusPendingCallWatcher(reply, this); + QObject::connect(watcher, &QDBusPendingCallWatcher::finished, watcher, + &QDBusPendingCallWatcher::deleteLater); + return watcher; +} + +QDBusPendingCallWatcher* DBusClient::activate( + const Server& server, const Device* device, const Keys* keys, int hopindex, + const QList& allowedIPAddressRanges, + const QStringList& vpnDisabledApps, const QHostAddress& dnsServer) { + QJsonObject json; + json.insert("privateKey", QJsonValue(keys->privateKey())); + json.insert("deviceIpv4Address", QJsonValue(device->ipv4Address())); + json.insert("deviceIpv6Address", QJsonValue(device->ipv6Address())); + json.insert("serverIpv4Gateway", QJsonValue(server.ipv4Gateway())); + json.insert("serverIpv6Gateway", QJsonValue(server.ipv6Gateway())); + json.insert("serverPublicKey", QJsonValue(server.publicKey())); + json.insert("serverIpv4AddrIn", QJsonValue(server.ipv4AddrIn())); + json.insert("serverIpv6AddrIn", QJsonValue(server.ipv6AddrIn())); + json.insert("serverPort", QJsonValue((double)server.choosePort())); + json.insert("dnsServer", QJsonValue(dnsServer.toString())); + json.insert("hopindex", QJsonValue((double)hopindex)); + + QJsonArray allowedIPAddesses; + for (const IPAddressRange& i : allowedIPAddressRanges) { + QJsonObject range; + range.insert("address", QJsonValue(i.ipAddress())); + range.insert("range", QJsonValue((double)i.range())); + range.insert("isIpv6", QJsonValue(i.type() == IPAddressRange::IPv6)); + allowedIPAddesses.append(range); + }; + json.insert("allowedIPAddressRanges", allowedIPAddesses); + + QJsonArray disabledApps; + for (const QString& i : vpnDisabledApps) { + disabledApps.append(QJsonValue(i)); + logger.debug() << "Disabling:" << i; + } + json.insert("vpnDisabledApps", disabledApps); + + logger.debug() << "Activate via DBus"; + QDBusPendingReply reply = + m_dbus->activate(QJsonDocument(json).toJson(QJsonDocument::Compact)); + QDBusPendingCallWatcher* watcher = new QDBusPendingCallWatcher(reply, this); + QObject::connect(watcher, &QDBusPendingCallWatcher::finished, watcher, + &QDBusPendingCallWatcher::deleteLater); + return watcher; +} + +QDBusPendingCallWatcher* DBusClient::deactivate() { + logger.debug() << "Deactivate via DBus"; + QDBusPendingReply reply = m_dbus->deactivate(); + QDBusPendingCallWatcher* watcher = new QDBusPendingCallWatcher(reply, this); + QObject::connect(watcher, &QDBusPendingCallWatcher::finished, watcher, + &QDBusPendingCallWatcher::deleteLater); + return watcher; +} + +QDBusPendingCallWatcher* DBusClient::status() { + logger.debug() << "Status via DBus"; + QDBusPendingReply reply = m_dbus->status(); + QDBusPendingCallWatcher* watcher = new QDBusPendingCallWatcher(reply, this); + QObject::connect(watcher, &QDBusPendingCallWatcher::finished, watcher, + &QDBusPendingCallWatcher::deleteLater); + return watcher; +} + +QDBusPendingCallWatcher* DBusClient::getLogs() { + logger.debug() << "Get logs via DBus"; + QDBusPendingReply reply = m_dbus->getLogs(); + QDBusPendingCallWatcher* watcher = new QDBusPendingCallWatcher(reply, this); + QObject::connect(watcher, &QDBusPendingCallWatcher::finished, watcher, + &QDBusPendingCallWatcher::deleteLater); + return watcher; +} + +QDBusPendingCallWatcher* DBusClient::cleanupLogs() { + logger.debug() << "Cleanup logs via DBus"; + QDBusPendingReply reply = m_dbus->cleanupLogs(); + QDBusPendingCallWatcher* watcher = new QDBusPendingCallWatcher(reply, this); + QObject::connect(watcher, &QDBusPendingCallWatcher::finished, watcher, + &QDBusPendingCallWatcher::deleteLater); + return watcher; +} diff --git a/client/platforms/linux/dbusclient.h b/client/platforms/linux/dbusclient.h new file mode 100644 index 000000000..4f70c3daa --- /dev/null +++ b/client/platforms/linux/dbusclient.h @@ -0,0 +1,51 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DBUSCLIENT_H +#define DBUSCLIENT_H + +#include "dbus_interface.h" + +#include +#include +#include + +class Server; +class Device; +class Keys; +class IPAddressRange; +class QDBusPendingCallWatcher; + +class DBusClient final : public QObject { + Q_OBJECT + Q_DISABLE_COPY_MOVE(DBusClient) + + public: + DBusClient(QObject* parent); + ~DBusClient(); + + QDBusPendingCallWatcher* version(); + + QDBusPendingCallWatcher* activate( + const Server& server, const Device* device, const Keys* keys, + int hopindex, const QList& allowedIPAddressRanges, + const QStringList& vpnDisabledApps, const QHostAddress& dnsServer); + + QDBusPendingCallWatcher* deactivate(); + + QDBusPendingCallWatcher* status(); + + QDBusPendingCallWatcher* getLogs(); + + QDBusPendingCallWatcher* cleanupLogs(); + + signals: + void connected(int hopindex); + void disconnected(int hopindex); + + private: + OrgMozillaVpnDbusInterface* m_dbus; +}; + +#endif // DBUSCLIENT_H diff --git a/client/platforms/linux/linuxappimageprovider.cpp b/client/platforms/linux/linuxappimageprovider.cpp new file mode 100644 index 000000000..f39719f15 --- /dev/null +++ b/client/platforms/linux/linuxappimageprovider.cpp @@ -0,0 +1,79 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "linuxappimageprovider.h" +#include "logger.h" +#include "leakdetector.h" + +#include +#include +#include +#include +#include +#include + +constexpr const char* PIXMAP_FALLBACK_PATH = "/usr/share/pixmaps/"; +constexpr const char* DESKTOP_ICON_LOCATION = "/usr/share/icons/"; + +namespace { +Logger logger(LOG_CONTROLLER, "LinuxAppImageProvider"); +} + +LinuxAppImageProvider::LinuxAppImageProvider(QObject* parent) + : AppImageProvider(parent, QQuickImageProvider::Image, + QQmlImageProviderBase::ForceAsynchronousImageLoading) { + MVPN_COUNT_CTOR(LinuxAppImageProvider); + + QStringList searchPaths = QIcon::fallbackSearchPaths(); + + QProcessEnvironment pe = QProcessEnvironment::systemEnvironment(); + if (pe.contains("XDG_DATA_DIRS")) { + QStringList parts = pe.value("XDG_DATA_DIRS").split(":"); + for (const QString& part : parts) { + addFallbackPaths(part + "/icons", searchPaths); + } + } else { + addFallbackPaths(DESKTOP_ICON_LOCATION, searchPaths); + } + + if (pe.contains("HOME")) { + addFallbackPaths(pe.value("HOME") + "/.local/share/icons", searchPaths); + } + + searchPaths << PIXMAP_FALLBACK_PATH; + QIcon::setFallbackSearchPaths(searchPaths); +} + +LinuxAppImageProvider::~LinuxAppImageProvider() { + MVPN_COUNT_DTOR(LinuxAppImageProvider); +} + +void LinuxAppImageProvider::addFallbackPaths(const QString& iconDir, + QStringList& searchPaths) { + searchPaths << iconDir; + + QDirIterator iter(iconDir, QDir::Dirs | QDir::NoDotAndDotDot); + while (iter.hasNext()) { + QFileInfo fileinfo(iter.next()); + logger.debug() << "Adding QIcon fallback:" << fileinfo.absoluteFilePath(); + searchPaths << fileinfo.absoluteFilePath(); + } +} + +// from QQuickImageProvider +QImage LinuxAppImageProvider::requestImage(const QString& id, QSize* size, + const QSize& requestedSize) { + QSettings entry(id, QSettings::IniFormat); + entry.beginGroup("Desktop Entry"); + QString name = entry.value("Icon").toString(); + + QIcon icon = QIcon::fromTheme(name); + QPixmap pixmap = icon.pixmap(requestedSize); + size->setHeight(pixmap.height()); + size->setWidth(pixmap.width()); + logger.debug() << "Loaded icon" << icon.name() << "size:" << pixmap.width() + << "x" << pixmap.height(); + + return pixmap.toImage(); +} diff --git a/client/platforms/linux/linuxappimageprovider.h b/client/platforms/linux/linuxappimageprovider.h new file mode 100644 index 000000000..5643f6e02 --- /dev/null +++ b/client/platforms/linux/linuxappimageprovider.h @@ -0,0 +1,22 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef LINUXAPPIMAGEPROVIDER_H +#define LINUXAPPIMAGEPROVIDER_H + +#include "appimageprovider.h" + +class LinuxAppImageProvider final : public AppImageProvider { + public: + LinuxAppImageProvider(QObject* parent); + ~LinuxAppImageProvider(); + QImage requestImage(const QString& id, QSize* size, + const QSize& requestedSize) override; + + private: + static void addFallbackPaths(const QString& dataDir, + QStringList& fallbackPaths); +}; + +#endif // LINUXAPPIMAGEPROVIDER_H diff --git a/client/platforms/linux/linuxapplistprovider.cpp b/client/platforms/linux/linuxapplistprovider.cpp new file mode 100644 index 000000000..011cedd5e --- /dev/null +++ b/client/platforms/linux/linuxapplistprovider.cpp @@ -0,0 +1,74 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "linuxapplistprovider.h" +#include "leakdetector.h" + +#include +#include +#include +#include +#include +#include + +#include "logger.h" +#include "leakdetector.h" + +constexpr const char* DESKTOP_ENTRY_LOCATION = "/usr/share/applications/"; + +namespace { +Logger logger(LOG_CONTROLLER, "LinuxAppListProvider"); +} + +LinuxAppListProvider::LinuxAppListProvider(QObject* parent) + : AppListProvider(parent) { + MVPN_COUNT_CTOR(LinuxAppListProvider); +} + +LinuxAppListProvider::~LinuxAppListProvider() { + MVPN_COUNT_DTOR(LinuxAppListProvider); +} + +void LinuxAppListProvider::fetchEntries(const QString& dataDir, + QMap& map) { + logger.debug() << "Fetch Application list from" << dataDir; + + QDirIterator iter(dataDir, QStringList() << "*.desktop", QDir::Files); + while (iter.hasNext()) { + QFileInfo fileinfo(iter.next()); + QSettings entry(fileinfo.filePath(), QSettings::IniFormat); + entry.beginGroup("Desktop Entry"); + + /* Filter out everything except visible applications. */ + if (entry.value("Type").toString() != "Application") { + continue; + } + if (entry.value("NoDisplay", QVariant(false)).toBool()) { + continue; + } + + map[fileinfo.absoluteFilePath()] = entry.value("Name").toString(); + } +} + +void LinuxAppListProvider::getApplicationList() { + logger.debug() << "Fetch Application list from Linux desktop"; + QMap out; + + QProcessEnvironment pe = QProcessEnvironment::systemEnvironment(); + if (pe.contains("XDG_DATA_DIRS")) { + QStringList parts = pe.value("XDG_DATA_DIRS").split(":"); + for (const QString& part : parts) { + fetchEntries(part.trimmed() + "/applications", out); + } + } else { + fetchEntries(DESKTOP_ENTRY_LOCATION, out); + } + + if (pe.contains("HOME")) { + fetchEntries(pe.value("HOME") + "/.local/share/applications", out); + } + + emit newAppList(out); +} diff --git a/client/platforms/linux/linuxapplistprovider.h b/client/platforms/linux/linuxapplistprovider.h new file mode 100644 index 000000000..3c6aa7367 --- /dev/null +++ b/client/platforms/linux/linuxapplistprovider.h @@ -0,0 +1,23 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef LINUXAPPLISTPROVIDER_H +#define LINUXAPPLISTPROVIDER_H + +#include +#include +#include + +class LinuxAppListProvider final : public AppListProvider { + Q_OBJECT + public: + explicit LinuxAppListProvider(QObject* parent); + ~LinuxAppListProvider(); + void getApplicationList() override; + + private: + void fetchEntries(const QString& dataDir, QMap& map); +}; + +#endif // LINUXAPPLISTPROVIDER_H diff --git a/client/platforms/linux/linuxcontroller.cpp b/client/platforms/linux/linuxcontroller.cpp new file mode 100644 index 000000000..00a66a3f8 --- /dev/null +++ b/client/platforms/linux/linuxcontroller.cpp @@ -0,0 +1,213 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "linuxcontroller.h" +#include "backendlogsobserver.h" +#include "dbusclient.h" +#include "errorhandler.h" +#include "ipaddressrange.h" +#include "leakdetector.h" +#include "logger.h" +#include "models/device.h" +#include "models/keys.h" +#include "models/server.h" +#include "mozillavpn.h" + +#include +#include +#include +#include +#include +#include + +namespace { +Logger logger({LOG_LINUX, LOG_CONTROLLER}, "LinuxController"); +} + +LinuxController::LinuxController() { + MVPN_COUNT_CTOR(LinuxController); + + m_dbus = new DBusClient(this); + connect(m_dbus, &DBusClient::connected, this, &LinuxController::hopConnected); + connect(m_dbus, &DBusClient::disconnected, this, + &LinuxController::hopDisconnected); +} + +LinuxController::~LinuxController() { MVPN_COUNT_DTOR(LinuxController); } + +void LinuxController::initialize(const Device* device, const Keys* keys) { + Q_UNUSED(device); + Q_UNUSED(keys); + + QDBusPendingCallWatcher* watcher = m_dbus->status(); + connect(watcher, &QDBusPendingCallWatcher::finished, this, + &LinuxController::initializeCompleted); +} + +void LinuxController::initializeCompleted(QDBusPendingCallWatcher* call) { + QDBusPendingReply reply = *call; + if (reply.isError()) { + logger.error() << "Error received from the DBus service"; + emit initialized(false, false, QDateTime()); + return; + } + + QString status = reply.argumentAt<0>(); + logger.debug() << "Status:" << status; + + QJsonDocument json = QJsonDocument::fromJson(status.toLocal8Bit()); + Q_ASSERT(json.isObject()); + + QJsonObject obj = json.object(); + Q_ASSERT(obj.contains("status")); + QJsonValue statusValue = obj.value("status"); + Q_ASSERT(statusValue.isBool()); + + emit initialized(true, statusValue.toBool(), QDateTime::currentDateTime()); +} + +void LinuxController::activate( + const QList& serverList, const Device* device, const Keys* keys, + const QList& allowedIPAddressRanges, + const QList& vpnDisabledApps, const QHostAddress& dnsServer, + Reason reason) { + Q_UNUSED(reason); + Q_UNUSED(vpnDisabledApps); + + // Activate connections starting from the outermost tunnel + for (int hopindex = serverList.count() - 1; hopindex > 0; hopindex--) { + const Server& hop = serverList[hopindex]; + const Server& next = serverList[hopindex - 1]; + QList hopAddressRanges = { + IPAddressRange(next.ipv4AddrIn()), IPAddressRange(next.ipv6AddrIn())}; + logger.debug() << "LinuxController hopindex" << hopindex << "activated"; + connect(m_dbus->activate(hop, device, keys, hopindex, hopAddressRanges, + QStringList(), QHostAddress(hop.ipv4Gateway())), + &QDBusPendingCallWatcher::finished, this, + &LinuxController::operationCompleted); + } + + // Activate the final hop last + logger.debug() << "LinuxController activated"; + const Server& server = serverList[0]; + connect(m_dbus->activate(server, device, keys, 0, allowedIPAddressRanges, + vpnDisabledApps, dnsServer), + &QDBusPendingCallWatcher::finished, this, + &LinuxController::operationCompleted); +} + +void LinuxController::deactivate(Reason reason) { + logger.debug() << "LinuxController deactivated"; + + if (reason == ReasonSwitching) { + logger.debug() << "No disconnect for quick server switching"; + emit disconnected(); + return; + } + + connect(m_dbus->deactivate(), &QDBusPendingCallWatcher::finished, this, + &LinuxController::operationCompleted); +} + +void LinuxController::operationCompleted(QDBusPendingCallWatcher* call) { + QDBusPendingReply reply = *call; + if (reply.isError()) { + logger.error() << "Error received from the DBus service"; + MozillaVPN::instance()->errorHandle(ErrorHandler::ControllerError); + emit disconnected(); + return; + } + + bool status = reply.argumentAt<0>(); + if (status) { + logger.debug() << "DBus service says: all good."; + // we will receive the connected/disconnected() signal; + return; + } + + logger.error() << "DBus service says: error."; + MozillaVPN::instance()->errorHandle(ErrorHandler::ControllerError); + emit disconnected(); +} + +void LinuxController::hopConnected(int hopindex) { + if (hopindex == 0) { + logger.debug() << "LinuxController connected"; + emit connected(); + } else { + logger.debug() << "LinuxController hopindex" << hopindex << "connected"; + } +} + +void LinuxController::hopDisconnected(int hopindex) { + if (hopindex == 0) { + logger.debug() << "LinuxController disconnected"; + emit disconnected(); + } else { + logger.debug() << "LinuxController hopindex" << hopindex << "disconnected"; + } +} + +void LinuxController::checkStatus() { + logger.debug() << "Check status"; + + QDBusPendingCallWatcher* watcher = m_dbus->status(); + connect(watcher, &QDBusPendingCallWatcher::finished, this, + &LinuxController::checkStatusCompleted); +} + +void LinuxController::checkStatusCompleted(QDBusPendingCallWatcher* call) { + QDBusPendingReply reply = *call; + if (reply.isError()) { + logger.error() << "Error received from the DBus service"; + return; + } + + QString status = reply.argumentAt<0>(); + logger.debug() << "Status:" << status; + + QJsonDocument json = QJsonDocument::fromJson(status.toLocal8Bit()); + Q_ASSERT(json.isObject()); + + QJsonObject obj = json.object(); + Q_ASSERT(obj.contains("status")); + QJsonValue statusValue = obj.value("status"); + Q_ASSERT(statusValue.isBool()); + + if (!statusValue.toBool()) { + logger.error() << "Unable to retrieve the status from the interface."; + return; + } + + Q_ASSERT(obj.contains("serverIpv4Gateway")); + QJsonValue serverIpv4Gateway = obj.value("serverIpv4Gateway"); + Q_ASSERT(serverIpv4Gateway.isString()); + + Q_ASSERT(obj.contains("deviceIpv4Address")); + QJsonValue deviceIpv4Address = obj.value("deviceIpv4Address"); + Q_ASSERT(deviceIpv4Address.isString()); + + Q_ASSERT(obj.contains("txBytes")); + QJsonValue txBytes = obj.value("txBytes"); + Q_ASSERT(txBytes.isDouble()); + + Q_ASSERT(obj.contains("rxBytes")); + QJsonValue rxBytes = obj.value("rxBytes"); + Q_ASSERT(rxBytes.isDouble()); + + emit statusUpdated(serverIpv4Gateway.toString(), deviceIpv4Address.toString(), + txBytes.toDouble(), rxBytes.toDouble()); +} + +void LinuxController::getBackendLogs( + std::function&& a_callback) { + std::function callback = std::move(a_callback); + + QDBusPendingCallWatcher* watcher = m_dbus->getLogs(); + connect(watcher, &QDBusPendingCallWatcher::finished, + new BackendLogsObserver(this, std::move(callback)), + &BackendLogsObserver::completed); +} + +void LinuxController::cleanupBackendLogs() { m_dbus->cleanupLogs(); } diff --git a/client/platforms/linux/linuxcontroller.h b/client/platforms/linux/linuxcontroller.h new file mode 100644 index 000000000..e46b1af6c --- /dev/null +++ b/client/platforms/linux/linuxcontroller.h @@ -0,0 +1,49 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef LINUXCONTROLLER_H +#define LINUXCONTROLLER_H + +#include "controllerimpl.h" + +#include + +class DBusClient; +class QDBusPendingCallWatcher; + +class LinuxController final : public ControllerImpl { + Q_DISABLE_COPY_MOVE(LinuxController) + + public: + LinuxController(); + ~LinuxController(); + + void initialize(const Device* device, const Keys* keys) override; + + void activate(const QList& serverList, const Device* device, + const Keys* keys, + const QList& allowedIPAddressRanges, + const QList& vpnDisabledApps, + const QHostAddress& dnsServer, Reason reason) override; + + void deactivate(Reason reason) override; + + void checkStatus() override; + + void getBackendLogs(std::function&& callback) override; + + void cleanupBackendLogs() override; + + private slots: + void checkStatusCompleted(QDBusPendingCallWatcher* call); + void initializeCompleted(QDBusPendingCallWatcher* call); + void operationCompleted(QDBusPendingCallWatcher* call); + void hopConnected(int hopindex); + void hopDisconnected(int hopindex); + + private: + DBusClient* m_dbus = nullptr; +}; + +#endif // LINUXCONTROLLER_H diff --git a/client/platforms/linux/linuxcryptosettings.cpp b/client/platforms/linux/linuxcryptosettings.cpp new file mode 100644 index 000000000..016f81c2a --- /dev/null +++ b/client/platforms/linux/linuxcryptosettings.cpp @@ -0,0 +1,17 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "cryptosettings.h" + +void CryptoSettings::resetKey() {} + +bool CryptoSettings::getKey(uint8_t key[CRYPTO_SETTINGS_KEY_SIZE]) { + Q_UNUSED(key); + return false; +} + +// static +CryptoSettings::Version CryptoSettings::getSupportedVersion() { + return CryptoSettings::NoEncryption; +} diff --git a/client/platforms/linux/linuxdependencies.cpp b/client/platforms/linux/linuxdependencies.cpp new file mode 100644 index 000000000..912c4a947 --- /dev/null +++ b/client/platforms/linux/linuxdependencies.cpp @@ -0,0 +1,127 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "linuxdependencies.h" +#include "dbusclient.h" +#include "logger.h" + +#include +#include +#include +#include + +#include + +constexpr const char* WG_QUICK = "wg-quick"; + +namespace { + +Logger logger(LOG_LINUX, "LinuxDependencies"); + +void showAlert(const QString& message) { + logger.debug() << "Show alert:" << message; + + QMessageBox alert; + alert.setText(message); + alert.exec(); +} + +bool findInPath(const char* what) { + char* path = getenv("PATH"); + Q_ASSERT(path); + + QStringList parts = QString(path).split(":"); + for (const QString& part : parts) { + QDir pathDir(part); + QFileInfo file(pathDir.filePath(what)); + if (file.exists()) { + logger.debug() << what << "found" << file.filePath(); + return true; + } + } + + return false; +} + +bool checkDaemonVersion() { + logger.debug() << "Check Daemon Version"; + + DBusClient* dbus = new DBusClient(nullptr); + QDBusPendingCallWatcher* watcher = dbus->version(); + + bool completed = false; + bool value = false; + QObject::connect( + watcher, &QDBusPendingCallWatcher::finished, + [completed = &completed, value = &value](QDBusPendingCallWatcher* call) { + *completed = true; + + QDBusPendingReply reply = *call; + if (reply.isError()) { + logger.error() << "DBus message received - error"; + *value = false; + return; + } + + QString version = reply.argumentAt<0>(); + *value = version == PROTOCOL_VERSION; + + logger.debug() << "DBus message received - daemon version:" << version + << " - current version:" << PROTOCOL_VERSION; + }); + + while (!completed) { + QCoreApplication::processEvents(); + } + + delete dbus; + return value; +} + +} // namespace + +// static +bool LinuxDependencies::checkDependencies() { + char* path = getenv("PATH"); + if (!path) { + showAlert("No PATH env found."); + return false; + } + + if (!findInPath(WG_QUICK)) { + showAlert("Unable to locate wg-quick"); + return false; + } + + if (!checkDaemonVersion()) { + showAlert("mozillavpn linuxdaemon needs to be updated or restarted."); + return false; + } + + return true; +} + +// static +QString LinuxDependencies::findCgroupPath(const QString& type) { + struct mntent entry; + char buf[PATH_MAX]; + + FILE* fp = fopen("/etc/mtab", "r"); + if (fp == NULL) { + return QString(); + } + + while (getmntent_r(fp, &entry, buf, sizeof(buf)) != NULL) { + if (strcmp(entry.mnt_type, "cgroup") != 0) { + continue; + } + if (hasmntopt(&entry, type.toLocal8Bit().constData()) != NULL) { + fclose(fp); + return QString(entry.mnt_dir); + } + } + fclose(fp); + + return QString(); +} diff --git a/client/platforms/linux/linuxdependencies.h b/client/platforms/linux/linuxdependencies.h new file mode 100644 index 000000000..1043579c6 --- /dev/null +++ b/client/platforms/linux/linuxdependencies.h @@ -0,0 +1,22 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef LINUXDEPENDENCIES_H +#define LINUXDEPENDENCIES_H + +#include + +class LinuxDependencies final { + public: + static bool checkDependencies(); + static QString findCgroupPath(const QString& type); + + private: + LinuxDependencies() = default; + ~LinuxDependencies() = default; + + Q_DISABLE_COPY(LinuxDependencies) +}; + +#endif // LINUXDEPENDENCIES_H diff --git a/client/platforms/linux/linuxnetworkwatcher.cpp b/client/platforms/linux/linuxnetworkwatcher.cpp new file mode 100644 index 000000000..79d1b83bc --- /dev/null +++ b/client/platforms/linux/linuxnetworkwatcher.cpp @@ -0,0 +1,55 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "linuxnetworkwatcher.h" +#include "linuxnetworkwatcherworker.h" +#include "leakdetector.h" +#include "logger.h" +#include "timersingleshot.h" + +namespace { +Logger logger(LOG_LINUX, "LinuxNetworkWatcher"); +} + +LinuxNetworkWatcher::LinuxNetworkWatcher(QObject* parent) + : NetworkWatcherImpl(parent) { + MVPN_COUNT_CTOR(LinuxNetworkWatcher); + + m_thread.start(); +} + +LinuxNetworkWatcher::~LinuxNetworkWatcher() { + MVPN_COUNT_DTOR(LinuxNetworkWatcher); + + delete m_worker; + + m_thread.quit(); + m_thread.wait(); +} + +void LinuxNetworkWatcher::initialize() { + logger.debug() << "initialize"; + + m_worker = new LinuxNetworkWatcherWorker(&m_thread); + + connect(this, &LinuxNetworkWatcher::checkDevicesInThread, m_worker, + &LinuxNetworkWatcherWorker::checkDevices); + + connect(m_worker, &LinuxNetworkWatcherWorker::unsecuredNetwork, this, + &LinuxNetworkWatcher::unsecuredNetwork); + + // Let's wait a few seconds to allow the UI to be fully loaded and shown. + // This is not strictly needed, but it's better for user experience because + // it makes the UI faster to appear, plus it gives a bit of delay between the + // UI to appear and the first notification. + TimerSingleShot::create(this, 2000, [this]() { + QMetaObject::invokeMethod(m_worker, "initialize", Qt::QueuedConnection); + }); +} + +void LinuxNetworkWatcher::start() { + logger.debug() << "actived"; + NetworkWatcherImpl::start(); + emit checkDevicesInThread(); +} diff --git a/client/platforms/linux/linuxnetworkwatcher.h b/client/platforms/linux/linuxnetworkwatcher.h new file mode 100644 index 000000000..9ea74acb1 --- /dev/null +++ b/client/platforms/linux/linuxnetworkwatcher.h @@ -0,0 +1,33 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef LINUXNETWORKWATCHER_H +#define LINUXNETWORKWATCHER_H + +#include "networkwatcherimpl.h" + +#include + +class LinuxNetworkWatcherWorker; + +class LinuxNetworkWatcher final : public NetworkWatcherImpl { + Q_OBJECT + + public: + explicit LinuxNetworkWatcher(QObject* parent); + ~LinuxNetworkWatcher(); + + void initialize() override; + + void start() override; + + signals: + void checkDevicesInThread(); + + private: + LinuxNetworkWatcherWorker* m_worker = nullptr; + QThread m_thread; +}; + +#endif // LINUXNETWORKWATCHER_H diff --git a/client/platforms/linux/linuxnetworkwatcherworker.cpp b/client/platforms/linux/linuxnetworkwatcherworker.cpp new file mode 100644 index 000000000..462cdc2a7 --- /dev/null +++ b/client/platforms/linux/linuxnetworkwatcherworker.cpp @@ -0,0 +1,175 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "linuxnetworkwatcherworker.h" +#include "leakdetector.h" +#include "logger.h" + +#include + +// https://developer.gnome.org/NetworkManager/stable/nm-dbus-types.html#NMDeviceType +#ifndef NM_DEVICE_TYPE_WIFI +# define NM_DEVICE_TYPE_WIFI 2 +#endif + +// https://developer.gnome.org/NetworkManager/stable/nm-dbus-types.html#NM80211ApFlags +// Wifi network has no security +#ifndef NM_802_11_AP_SEC_NONE +# define NM_802_11_AP_SEC_NONE 0x00000000 +#endif + +// Wifi network has WEP (40 bits) +#ifndef NM_802_11_AP_SEC_PAIR_WEP40 +# define NM_802_11_AP_SEC_PAIR_WEP40 0x00000001 +#endif + +// Wifi network has WEP (104 bits) +#ifndef NM_802_11_AP_SEC_PAIR_WEP104 +# define NM_802_11_AP_SEC_PAIR_WEP104 0x00000002 +#endif + +#define NM_802_11_AP_SEC_WEAK_CRYPTO \ + (NM_802_11_AP_SEC_PAIR_WEP40 | NM_802_11_AP_SEC_PAIR_WEP104) + +constexpr const char* DBUS_NETWORKMANAGER = "org.freedesktop.NetworkManager"; + +namespace { +Logger logger(LOG_LINUX, "LinuxNetworkWatcherWorker"); +} + +static inline bool checkUnsecureFlags(int rsnFlags, int wpaFlags) { + // If neither WPA nor WPA2/RSN are supported, then the network is unencrypted + if (rsnFlags == NM_802_11_AP_SEC_NONE && wpaFlags == NM_802_11_AP_SEC_NONE) { + return false; + } + + // Consider the user of weak cryptography to be unsecure + if ((rsnFlags & NM_802_11_AP_SEC_WEAK_CRYPTO) || + (wpaFlags & NM_802_11_AP_SEC_WEAK_CRYPTO)) { + return false; + } + // Otherwise, the network is secured with reasonable cryptography + return true; +} + +LinuxNetworkWatcherWorker::LinuxNetworkWatcherWorker(QThread* thread) { + MVPN_COUNT_CTOR(LinuxNetworkWatcherWorker); + moveToThread(thread); +} + +LinuxNetworkWatcherWorker::~LinuxNetworkWatcherWorker() { + MVPN_COUNT_DTOR(LinuxNetworkWatcherWorker); +} + +void LinuxNetworkWatcherWorker::initialize() { + logger.debug() << "initialize"; + + logger.debug() + << "Retrieving the list of wifi network devices from NetworkManager"; + + // To know the NeworkManager DBus methods and properties, read the official + // documentation: + // https://developer.gnome.org/NetworkManager/stable/gdbus-org.freedesktop.NetworkManager.html + + QDBusInterface nm(DBUS_NETWORKMANAGER, "/org/freedesktop/NetworkManager", + DBUS_NETWORKMANAGER, QDBusConnection::systemBus()); + if (!nm.isValid()) { + logger.error() + << "Failed to connect to the network manager via system dbus"; + return; + } + + QDBusMessage msg = nm.call("GetDevices"); + QDBusArgument arg = msg.arguments().at(0).value(); + if (arg.currentType() != QDBusArgument::ArrayType) { + logger.error() << "Expected an array of devices"; + return; + } + + QList paths = qdbus_cast >(arg); + for (const QDBusObjectPath& path : paths) { + QString devicePath = path.path(); + QDBusInterface device(DBUS_NETWORKMANAGER, devicePath, + "org.freedesktop.NetworkManager.Device", + QDBusConnection::systemBus()); + if (device.property("DeviceType").toInt() != NM_DEVICE_TYPE_WIFI) { + continue; + } + + logger.debug() << "Found a wifi device:" << devicePath; + m_devicePaths.append(devicePath); + + // Here we monitor the changes. + QDBusConnection::systemBus().connect( + DBUS_NETWORKMANAGER, devicePath, "org.freedesktop.DBus.Properties", + "PropertiesChanged", this, + SLOT(propertyChanged(QString, QVariantMap, QStringList))); + } + + if (m_devicePaths.isEmpty()) { + logger.warning() << "No wifi devices found"; + return; + } + + // We could be already be activated. + checkDevices(); +} + +void LinuxNetworkWatcherWorker::propertyChanged(QString interface, + QVariantMap properties, + QStringList list) { + Q_UNUSED(list); + + logger.debug() << "Properties changed for interface" << interface; + + if (!properties.contains("ActiveAccessPoint")) { + logger.debug() << "Access point did not changed. Ignoring the changes"; + return; + } + + checkDevices(); +} + +void LinuxNetworkWatcherWorker::checkDevices() { + logger.debug() << "Checking devices"; + + for (const QString& devicePath : m_devicePaths) { + QDBusInterface wifiDevice(DBUS_NETWORKMANAGER, devicePath, + "org.freedesktop.NetworkManager.Device.Wireless", + QDBusConnection::systemBus()); + + // Check the access point path + QString accessPointPath = wifiDevice.property("ActiveAccessPoint") + .value() + .path(); + if (accessPointPath.isEmpty()) { + logger.warning() << "No access point found"; + continue; + } + + QDBusInterface ap(DBUS_NETWORKMANAGER, accessPointPath, + "org.freedesktop.NetworkManager.AccessPoint", + QDBusConnection::systemBus()); + + QVariant rsnFlags = ap.property("RsnFlags"); + QVariant wpaFlags = ap.property("WpaFlags"); + if (!rsnFlags.isValid() || !wpaFlags.isValid()) { + // We are probably not connected. + continue; + } + + if (!checkUnsecureFlags(rsnFlags.toInt(), wpaFlags.toInt())) { + QString ssid = ap.property("Ssid").toString(); + QString bssid = ap.property("HwAddress").toString(); + + // We have found 1 unsecured network. We don't need to check other wifi + // network devices. + logger.warning() << "Unsecured AP detected!" + << "rsnFlags:" << rsnFlags.toInt() + << "wpaFlags:" << wpaFlags.toInt() << "ssid:" << ssid; + emit unsecuredNetwork(ssid, bssid); + break; + } + } +} diff --git a/client/platforms/linux/linuxnetworkwatcherworker.h b/client/platforms/linux/linuxnetworkwatcherworker.h new file mode 100644 index 000000000..cc4c6a366 --- /dev/null +++ b/client/platforms/linux/linuxnetworkwatcherworker.h @@ -0,0 +1,41 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef LINUXNETWORKWATCHERWORKER_H +#define LINUXNETWORKWATCHERWORKER_H + +#include +#include +#include + +class QThread; + +class LinuxNetworkWatcherWorker final : public QObject { + Q_OBJECT + Q_DISABLE_COPY_MOVE(LinuxNetworkWatcherWorker) + + public: + explicit LinuxNetworkWatcherWorker(QThread* thread); + ~LinuxNetworkWatcherWorker(); + + void checkDevices(); + + signals: + void unsecuredNetwork(const QString& networkName, const QString& networkId); + + public slots: + void initialize(); + + private slots: + void propertyChanged(QString interface, QVariantMap properties, + QStringList list); + + private: + // We collect the list of DBus wifi network device paths during the + // initialization. When a property of them changes, we check if the access + // point is active and unsecure. + QStringList m_devicePaths; +}; + +#endif // LINUXNETWORKWATCHERWORKER_H diff --git a/client/platforms/linux/linuxpingsender.cpp b/client/platforms/linux/linuxpingsender.cpp new file mode 100644 index 000000000..36e0a2a91 --- /dev/null +++ b/client/platforms/linux/linuxpingsender.cpp @@ -0,0 +1,191 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "linuxpingsender.h" +#include "leakdetector.h" +#include "logger.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { +Logger logger({LOG_LINUX, LOG_NETWORKING}, "LinuxPingSender"); +} + +int LinuxPingSender::createSocket() { + // Try creating an ICMP socket. This would be the ideal choice, but it can + // fail depending on the kernel config (see: sys.net.ipv4.ping_group_range) + m_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP); + if (m_socket >= 0) { + m_ident = 0; + return m_socket; + } + if ((errno != EPERM) && (errno != EACCES)) { + return -1; + } + + // As a fallback, create a raw socket, which requires root permissions + // or CAP_NET_RAW to be granted to the VPN client. + m_socket = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP); + if (m_socket < 0) { + return -1; + } + m_ident = getpid() & 0xffff; + + // Attach a BPF filter to discard everything but replies to our echo. + struct sock_filter bpf_prog[] = { + BPF_STMT(BPF_LDX | BPF_B | BPF_MSH, 0), /* Skip IP header. */ + BPF_STMT(BPF_LD | BPF_H | BPF_IND, 4), /* Load icmp echo ident */ + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, m_ident, 1, 0), /* Ours? */ + BPF_STMT(BPF_RET | BPF_K, 0), /* Unexpected identifier. Reject. */ + BPF_STMT(BPF_LD | BPF_B | BPF_IND, 0), /* Load icmp type */ + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, ICMP_ECHOREPLY, 1, 0), /* Echo? */ + BPF_STMT(BPF_RET | BPF_K, 0), /* Unexpected type. Reject. */ + BPF_STMT(BPF_RET | BPF_K, ~0U), /* Packet passes the filter. */ + }; + struct sock_fprog filter = { + .len = sizeof(bpf_prog) / sizeof(struct sock_filter), + .filter = bpf_prog, + }; + setsockopt(m_socket, SOL_SOCKET, SO_ATTACH_FILTER, &filter, sizeof(filter)); + + return m_socket; +} + +LinuxPingSender::LinuxPingSender(const QString& source, QObject* parent) + : PingSender(parent), m_source(source) { + MVPN_COUNT_CTOR(LinuxPingSender); + logger.debug() << "LinuxPingSender(" + source + ") created"; + + m_socket = createSocket(); + if (m_socket < 0) { + logger.error() << "Socket creation error: " << strerror(errno); + return; + } + + struct sockaddr_in addr; + memset(&addr, 0, sizeof addr); + addr.sin_family = AF_INET; + if (inet_aton(source.toLocal8Bit().constData(), &addr.sin_addr) == 0) { + logger.error() << "source" << source << "error:" << strerror(errno); + return; + } + if (bind(m_socket, (struct sockaddr*)&addr, sizeof(addr)) != 0) { + close(m_socket); + m_socket = -1; + logger.error() << "bind error:" << strerror(errno); + return; + } + + m_notifier = new QSocketNotifier(m_socket, QSocketNotifier::Read, this); + if (m_ident) { + connect(m_notifier, &QSocketNotifier::activated, this, + &LinuxPingSender::rawSocketReady); + } else { + connect(m_notifier, &QSocketNotifier::activated, this, + &LinuxPingSender::icmpSocketReady); + } +} + +LinuxPingSender::~LinuxPingSender() { + MVPN_COUNT_DTOR(LinuxPingSender); + if (m_socket >= 0) { + close(m_socket); + } +} + +void LinuxPingSender::sendPing(const QString& dest, quint16 sequence) { + // QProcess is not supported on iOS. Because of this we cannot use the `ping` + // app as fallback on this platform. +#ifndef MVPN_IOS + // Use the generic ping sender if we failed to open an ICMP socket. + if (m_socket < 0) { + QStringList args; + args << "-c" + << "1"; + args << "-I" << m_source; + args << dest; + genericSendPing(args, sequence); + return; + } +#endif + + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + if (inet_aton(dest.toLocal8Bit().constData(), &addr.sin_addr) == 0) { + return; + } + + struct icmphdr packet; + memset(&packet, 0, sizeof(packet)); + packet.type = ICMP_ECHO; + packet.un.echo.id = htons(m_ident); + packet.un.echo.sequence = htons(sequence); + packet.checksum = inetChecksum(&packet, sizeof(packet)); + + int rc = sendto(m_socket, &packet, sizeof(packet), 0, (struct sockaddr*)&addr, + sizeof(addr)); + if (rc < 0) { + logger.error() << "failed to send:" << strerror(errno); + } +} + +void LinuxPingSender::icmpSocketReady() { + socklen_t slen = 0; + unsigned char data[2048]; + int rc = recvfrom(m_socket, data, sizeof(data), MSG_DONTWAIT, NULL, &slen); + if (rc <= 0) { + logger.error() << "recvfrom failed:" << strerror(errno); + return; + } + + struct icmphdr packet; + if (rc >= (int)sizeof(packet)) { + memcpy(&packet, data, sizeof(packet)); + if (packet.type == ICMP_ECHOREPLY) { + emit recvPing(htons(packet.un.echo.sequence)); + } + } +} + +void LinuxPingSender::rawSocketReady() { + socklen_t slen = 0; + unsigned char data[2048]; + int rc = recvfrom(m_socket, data, sizeof(data), MSG_DONTWAIT, NULL, &slen); + if (rc <= 0) { + logger.error() << "recvfrom failed:" << strerror(errno); + return; + } + + // Check the IP header + const struct iphdr* ip = (struct iphdr*)data; + int iphdrlen = ip->ihl * 4; + if (rc < iphdrlen || iphdrlen < (int)sizeof(struct iphdr)) { + logger.error() << "malformed IP packet:" << strerror(errno); + return; + } + + // Check the ICMP packet + struct icmphdr packet; + if (inetChecksum(data + iphdrlen, rc - iphdrlen) != 0) { + logger.warning() << "invalid checksum"; + return; + } + if (rc >= (iphdrlen + (int)sizeof(packet))) { + memcpy(&packet, data + iphdrlen, sizeof(packet)); + quint16 id = htons(m_ident); + if ((packet.type == ICMP_ECHOREPLY) && (packet.un.echo.id == id)) { + emit recvPing(htons(packet.un.echo.sequence)); + } + } +} diff --git a/client/platforms/linux/linuxpingsender.h b/client/platforms/linux/linuxpingsender.h new file mode 100644 index 000000000..e2ff705d3 --- /dev/null +++ b/client/platforms/linux/linuxpingsender.h @@ -0,0 +1,38 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef LINUXPINGSENDER_H +#define LINUXPINGSENDER_H + +#include "pingsender.h" + +#include + +class QSocketNotifier; + +class LinuxPingSender final : public PingSender { + Q_OBJECT + Q_DISABLE_COPY_MOVE(LinuxPingSender) + + public: + LinuxPingSender(const QString& source, QObject* parent = nullptr); + ~LinuxPingSender(); + + void sendPing(const QString& dest, quint16 sequence) override; + + private: + int createSocket(); + + private slots: + void rawSocketReady(); + void icmpSocketReady(); + + private: + QSocketNotifier* m_notifier = nullptr; + QString m_source; + int m_socket = 0; + quint16 m_ident = 0; +}; + +#endif // LINUXPINGSENDER_H diff --git a/client/platforms/linux/linuxsystemtraynotificationhandler.cpp b/client/platforms/linux/linuxsystemtraynotificationhandler.cpp new file mode 100644 index 000000000..408eef49b --- /dev/null +++ b/client/platforms/linux/linuxsystemtraynotificationhandler.cpp @@ -0,0 +1,109 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "platforms/linux/linuxsystemtraynotificationhandler.h" +#include "constants.h" +#include "leakdetector.h" +#include "logger.h" + +#include + +constexpr const char* DBUS_ITEM = "org.freedesktop.Notifications"; +constexpr const char* DBUS_PATH = "/org/freedesktop/Notifications"; +constexpr const char* DBUS_INTERFACE = "org.freedesktop.Notifications"; +constexpr const char* ACTION_ID = "mozilla_vpn_notification"; + +namespace { +Logger logger(LOG_LINUX, "LinuxSystemTrayNotificationHandler"); +} // namespace + +// static +bool LinuxSystemTrayNotificationHandler::requiredCustomImpl() { + if (!QDBusConnection::sessionBus().isConnected()) { + return false; + } + + QDBusConnectionInterface* interface = + QDBusConnection::sessionBus().interface(); + if (!interface) { + return false; + } + + // This custom systemTrayHandler implementation is required only on Unity. + QStringList registeredServices = interface->registeredServiceNames().value(); + return registeredServices.contains("com.canonical.Unity"); +} + +LinuxSystemTrayNotificationHandler::LinuxSystemTrayNotificationHandler( + QObject* parent) + : SystemTrayNotificationHandler(parent) { + MVPN_COUNT_CTOR(LinuxSystemTrayNotificationHandler); + + QDBusConnection::sessionBus().connect(DBUS_ITEM, DBUS_PATH, DBUS_INTERFACE, + "ActionInvoked", this, + SLOT(actionInvoked(uint, QString))); +} + +LinuxSystemTrayNotificationHandler::~LinuxSystemTrayNotificationHandler() { + MVPN_COUNT_DTOR(LinuxSystemTrayNotificationHandler); +} + +void LinuxSystemTrayNotificationHandler::notify(Message type, + const QString& title, + const QString& message, + int timerMsec) { + QString actionMessage; + switch (type) { + case None: + return SystemTrayNotificationHandler::notify(type, title, message, + timerMsec); + + case UnsecuredNetwork: + actionMessage = qtTrId("vpn.toggle.on"); + break; + + case CaptivePortalBlock: + actionMessage = qtTrId("vpn.toggle.off"); + break; + + case CaptivePortalUnblock: + actionMessage = qtTrId("vpn.toggle.on"); + break; + + default: + Q_ASSERT(false); + } + + m_lastMessage = type; + emit notificationShown(title, message); + + QDBusInterface n(DBUS_ITEM, DBUS_PATH, DBUS_INTERFACE, + QDBusConnection::sessionBus()); + if (!n.isValid()) { + qWarning("Failed to connect to the notification manager via system dbus"); + return; + } + + uint32_t replacesId = 0; // Don't replace. + const char* appIcon = MVPN_ICON_PATH; + QStringList actions{ACTION_ID, actionMessage}; + QMap hints; + + QDBusReply reply = n.call("Notify", "Mozilla VPN", replacesId, appIcon, + title, message, actions, hints, timerMsec); + if (!reply.isValid()) { + logger.warning() << "Failed to show the notification"; + } + + m_lastNotificationId = reply; +} + +void LinuxSystemTrayNotificationHandler::actionInvoked(uint actionId, + QString action) { + logger.debug() << "Notification clicked" << actionId << action; + + if (action == ACTION_ID && m_lastNotificationId == actionId) { + messageClickHandle(); + } +} diff --git a/client/platforms/linux/linuxsystemtraynotificationhandler.h b/client/platforms/linux/linuxsystemtraynotificationhandler.h new file mode 100644 index 000000000..359d3512e --- /dev/null +++ b/client/platforms/linux/linuxsystemtraynotificationhandler.h @@ -0,0 +1,34 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef LINUXNOTIFICATIONNOTIFICATIONHANDLER_H +#define LINUXNOTIFICATIONNOTIFICATIONHANDLER_H + +#include "./ui/systemtray_notificationhandler.h" + +#include + +class LinuxSystemTrayNotificationHandler final + : public SystemTrayNotificationHandler { + Q_OBJECT + Q_DISABLE_COPY_MOVE(LinuxSystemTrayNotificationHandler) + + public: + static bool requiredCustomImpl(); + + LinuxSystemTrayNotificationHandler(QObject* parent); + ~LinuxSystemTrayNotificationHandler(); + + private: + void notify(Message type, const QString& title, const QString& message, + int timerMsec) override; + + private slots: + void actionInvoked(uint actionId, QString action); + + private: + uint m_lastNotificationId = 0; +}; + +#endif // LINUXNOTIFICATIONNOTIFICATIONHANDLER_H diff --git a/client/ui/notificationhandler.cpp b/client/ui/notificationhandler.cpp index 81f6430ad..4a4f3c5fd 100644 --- a/client/ui/notificationhandler.cpp +++ b/client/ui/notificationhandler.cpp @@ -12,7 +12,7 @@ #else # if defined(Q_OS_LINUX) -# include "platforms/linux/linuxsystemtraynotificationhandler.h" +# include "linuxsystemtraynotificationhandler.h" # endif # include "systemtray_notificationhandler.h"