From 07c38e9b6ca879729197d67f0ebc1f02c186632f Mon Sep 17 00:00:00 2001 From: Mykola Baibuz Date: Thu, 14 Sep 2023 19:44:17 +0300 Subject: [PATCH 1/9] WireGuard rework for MacOS and Windows (#314) WireGuard rework for MacOS and Windows --- client/3rd-prebuilt | 2 +- client/daemon/daemon.cpp | 137 +-- client/daemon/daemon.h | 11 +- client/daemon/interfaceconfig.cpp | 122 +++ client/daemon/interfaceconfig.h | 23 +- client/daemon/wireguardutils.h | 12 +- client/mozilla/controllerimpl.h | 2 +- client/mozilla/dnspingsender.cpp | 53 +- client/mozilla/dnspingsender.h | 4 + client/mozilla/localsocketcontroller.cpp | 29 +- client/mozilla/networkwatcher.cpp | 85 +- client/mozilla/networkwatcher.h | 2 + client/mozilla/pinghelper.h | 4 + client/mozilla/pingsenderfactory.cpp | 9 +- client/mozilla/shared/ipaddress.h | 5 + .../macos/daemon/macosroutemonitor.cpp | 84 +- .../macos/daemon/macosroutemonitor.h | 10 +- .../macos/daemon/wireguardutilsmacos.cpp | 40 +- .../macos/daemon/wireguardutilsmacos.h | 8 +- client/platforms/macos/macosutils.mm | 1 + .../windows/daemon/dnsutilswindows.cpp | 177 ++++ .../windows/daemon/dnsutilswindows.h | 34 + .../windows/daemon/windowsdaemon.cpp | 81 ++ .../platforms/windows/daemon/windowsdaemon.h | 48 + .../windows/daemon/windowsdaemontunnel.cpp | 77 ++ .../windows/daemon/windowsdaemontunnel.h | 18 + .../windows/daemon/windowsfirewall.cpp | 850 ++++++++++++++++++ .../windows/daemon/windowsfirewall.h | 72 ++ .../windows/daemon/windowsroutemonitor.cpp | 317 +++++++ .../windows/daemon/windowsroutemonitor.h | 47 + .../windows/daemon/windowssplittunnel.cpp | 547 +++++++++++ .../windows/daemon/windowssplittunnel.h | 180 ++++ .../windows/daemon/windowstunnellogger.cpp | 135 +++ .../windows/daemon/windowstunnellogger.h | 36 + .../windows/daemon/windowstunnelservice.cpp | 345 +++++++ .../windows/daemon/windowstunnelservice.h | 45 + .../windows/daemon/wireguardutilswindows.cpp | 275 ++++++ .../windows/daemon/wireguardutilswindows.h | 53 ++ client/platforms/windows/windowscommons.cpp | 186 ++++ client/platforms/windows/windowscommons.h | 28 + .../windows/windowsnetworkwatcher.cpp | 144 +++ .../platforms/windows/windowsnetworkwatcher.h | 34 + .../platforms/windows/windowspingsender.cpp | 133 +++ client/platforms/windows/windowspingsender.h | 34 + .../windows/windowsservicemanager.cpp | 139 +++ .../platforms/windows/windowsservicemanager.h | 60 ++ client/platforms/windows/windowsutils.cpp | 62 ++ client/platforms/windows/windowsutils.h | 23 + client/protocols/wireguardprotocol.cpp | 10 +- client/protocols/wireguardprotocol.h | 4 +- service/CMakeLists.txt | 4 - service/server/CMakeLists.txt | 37 +- service/server/localserver.cpp | 12 +- service/server/localserver.h | 17 +- service/server/main.cpp | 30 +- service/server/systemservice.cpp | 29 + service/wireguard-service/CMakeLists.txt | 34 - service/wireguard-service/main.cpp | 31 - .../wireguardtunnelservice.cpp | 160 ---- .../wireguardtunnelservice.h | 22 - 60 files changed, 4779 insertions(+), 434 deletions(-) create mode 100644 client/daemon/interfaceconfig.cpp create mode 100644 client/platforms/windows/daemon/dnsutilswindows.cpp create mode 100644 client/platforms/windows/daemon/dnsutilswindows.h create mode 100644 client/platforms/windows/daemon/windowsdaemon.cpp create mode 100644 client/platforms/windows/daemon/windowsdaemon.h create mode 100644 client/platforms/windows/daemon/windowsdaemontunnel.cpp create mode 100644 client/platforms/windows/daemon/windowsdaemontunnel.h create mode 100644 client/platforms/windows/daemon/windowsfirewall.cpp create mode 100644 client/platforms/windows/daemon/windowsfirewall.h create mode 100644 client/platforms/windows/daemon/windowsroutemonitor.cpp create mode 100644 client/platforms/windows/daemon/windowsroutemonitor.h create mode 100644 client/platforms/windows/daemon/windowssplittunnel.cpp create mode 100644 client/platforms/windows/daemon/windowssplittunnel.h create mode 100644 client/platforms/windows/daemon/windowstunnellogger.cpp create mode 100644 client/platforms/windows/daemon/windowstunnellogger.h create mode 100644 client/platforms/windows/daemon/windowstunnelservice.cpp create mode 100644 client/platforms/windows/daemon/windowstunnelservice.h create mode 100644 client/platforms/windows/daemon/wireguardutilswindows.cpp create mode 100644 client/platforms/windows/daemon/wireguardutilswindows.h create mode 100644 client/platforms/windows/windowscommons.cpp create mode 100644 client/platforms/windows/windowscommons.h create mode 100644 client/platforms/windows/windowsnetworkwatcher.cpp create mode 100644 client/platforms/windows/windowsnetworkwatcher.h create mode 100644 client/platforms/windows/windowspingsender.cpp create mode 100644 client/platforms/windows/windowspingsender.h create mode 100644 client/platforms/windows/windowsservicemanager.cpp create mode 100644 client/platforms/windows/windowsservicemanager.h create mode 100644 client/platforms/windows/windowsutils.cpp create mode 100644 client/platforms/windows/windowsutils.h delete mode 100644 service/wireguard-service/CMakeLists.txt delete mode 100644 service/wireguard-service/main.cpp delete mode 100644 service/wireguard-service/wireguardtunnelservice.cpp delete mode 100644 service/wireguard-service/wireguardtunnelservice.h diff --git a/client/3rd-prebuilt b/client/3rd-prebuilt index 75ab7e24..66d39ba0 160000 --- a/client/3rd-prebuilt +++ b/client/3rd-prebuilt @@ -1 +1 @@ -Subproject commit 75ab7e2418b83af7f8ed0a448ec5081b37b54442 +Subproject commit 66d39ba0e294f65ed4933f01f9eedd56032610d3 diff --git a/client/daemon/daemon.cpp b/client/daemon/daemon.cpp index 882f9508..3a0dc4d9 100644 --- a/client/daemon/daemon.cpp +++ b/client/daemon/daemon.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include "leakdetector.h" @@ -64,9 +65,12 @@ bool Daemon::activate(const InterfaceConfig& config) { // method calls switchServer(). // // At the end, if the activation succeds, the `connected` signal is emitted. + // If the activation abort's for any reason `the `activationFailure` signal is + // emitted. logger.debug() << "Activating interface"; + auto emit_failure_guard = qScopeGuard([this] { emit activationFailure(); }); - if (m_connections.contains(config.m_hopindex)) { + if (m_connections.contains(config.m_hopType)) { if (supportServerSwitching(config)) { logger.debug() << "Already connected. Server switching supported."; @@ -85,10 +89,12 @@ bool Daemon::activate(const InterfaceConfig& config) { bool status = run(Switch, config); logger.debug() << "Connection status:" << status; if (status) { - m_connections[config.m_hopindex] = ConnectionState(config); + m_connections[config.m_hopType] = ConnectionState(config); m_handshakeTimer.start(HANDSHAKE_POLL_MSEC); + emit_failure_guard.dismiss(); + return true; } - return status; + return false; } logger.warning() << "Already connected. Server switching not supported."; @@ -96,8 +102,12 @@ bool Daemon::activate(const InterfaceConfig& config) { return false; } - Q_ASSERT(!m_connections.contains(config.m_hopindex)); - return activate(config); + Q_ASSERT(!m_connections.contains(config.m_hopType)); + if (activate(config)) { + emit_failure_guard.dismiss(); + return true; + } + return false; } prepareActivation(config); @@ -112,13 +122,7 @@ bool Daemon::activate(const InterfaceConfig& config) { // Configure routing for excluded addresses. for (const QString& i : config.m_excludedAddresses) { - QHostAddress address(i); - if (m_excludedAddrSet.contains(address)) { - m_excludedAddrSet[address]++; - continue; - } - wgutils()->addExclusionRoute(address); - m_excludedAddrSet[address] = 1; + addExclusionRoute(IPAddress(i)); } // Add the peer to this interface. @@ -142,7 +146,7 @@ bool Daemon::activate(const InterfaceConfig& config) { // set routing for (const IPAddress& ip : config.m_allowedIPAddressRanges) { - if (!wgutils()->updateRoutePrefix(ip, config.m_hopindex)) { + if (!wgutils()->updateRoutePrefix(ip)) { logger.debug() << "Routing configuration failed for" << logger.sensitive(ip.toString()); return false; @@ -152,15 +156,21 @@ bool Daemon::activate(const InterfaceConfig& config) { bool status = run(Up, config); logger.debug() << "Connection status:" << status; if (status) { - m_connections[config.m_hopindex] = ConnectionState(config); + m_connections[config.m_hopType] = ConnectionState(config); m_handshakeTimer.start(HANDSHAKE_POLL_MSEC); + emit_failure_guard.dismiss(); + return true; } - - return status; + return false; } bool Daemon::maybeUpdateResolvers(const InterfaceConfig& config) { - if ((config.m_hopindex == 0) && supportDnsUtils()) { + if (!supportDnsUtils()) { + return true; + } + + if ((config.m_hopType == InterfaceConfig::MultiHopExit) || + (config.m_hopType == InterfaceConfig::SingleHop)) { QList resolvers; resolvers.append(QHostAddress(config.m_dnsServer)); @@ -199,6 +209,28 @@ bool Daemon::parseStringList(const QJsonObject& obj, const QString& name, return true; } +bool Daemon::addExclusionRoute(const IPAddress& prefix) { + if (m_excludedAddrSet.contains(prefix)) { + m_excludedAddrSet[prefix]++; + return true; + } + if (!wgutils()->addExclusionRoute(prefix)) { + return false; + } + m_excludedAddrSet[prefix] = 1; + return true; +} + +bool Daemon::delExclusionRoute(const IPAddress& prefix) { + Q_ASSERT(m_excludedAddrSet.contains(prefix)); + if (m_excludedAddrSet[prefix] > 1) { + m_excludedAddrSet[prefix]--; + return true; + } + m_excludedAddrSet.remove(prefix); + return wgutils()->deleteExclusionRoute(prefix); +} + // static bool Daemon::parseConfig(const QJsonObject& obj, InterfaceConfig& config) { #define GETVALUE(name, where, jsontype) \ @@ -216,8 +248,8 @@ bool Daemon::parseConfig(const QJsonObject& obj, InterfaceConfig& config) { GETVALUE("privateKey", config.m_privateKey, String); GETVALUE("serverPublicKey", config.m_serverPublicKey, String); - GETVALUE("serverPort", config.m_serverPort, Double); GETVALUE("serverPskKey", config.m_serverPskKey, String); + GETVALUE("serverPort", config.m_serverPort, Double); config.m_deviceIpv4Address = obj.value("deviceIpv4Address").toString(); config.m_deviceIpv6Address = obj.value("deviceIpv6Address").toString(); @@ -247,15 +279,24 @@ bool Daemon::parseConfig(const QJsonObject& obj, InterfaceConfig& config) { config.m_dnsServer = value.toString(); } - if (!obj.contains("hopindex")) { - config.m_hopindex = 0; + if (!obj.contains("hopType")) { + config.m_hopType = InterfaceConfig::SingleHop; } else { - QJsonValue value = obj.value("hopindex"); - if (!value.isDouble()) { - logger.error() << "hopindex is not a number"; + QJsonValue value = obj.value("hopType"); + if (!value.isString()) { + logger.error() << "hopType is not a string"; + return false; + } + + bool okay; + QByteArray vdata = value.toString().toUtf8(); + QMetaEnum meta = QMetaEnum::fromType(); + config.m_hopType = + InterfaceConfig::HopType(meta.keyToValue(vdata.constData(), &okay)); + if (!okay) { + logger.error() << "hopType" << value.toString() << "is not valid"; return false; } - config.m_hopindex = value.toInt(); } if (!obj.contains(JSON_ALLOWEDIPADDRESSRANGES)) { @@ -325,8 +366,8 @@ bool Daemon::deactivate(bool emitSignals) { Q_ASSERT(wgutils() != nullptr); // Deactivate the main interface. - if (m_connections.contains(0)) { - const ConnectionState& state = m_connections.value(0); + if (!m_connections.isEmpty()) { + const ConnectionState& state = m_connections.first(); if (!run(Down, state.m_config)) { return false; } @@ -349,9 +390,9 @@ bool Daemon::deactivate(bool emitSignals) { // Cleanup peers and routing for (const ConnectionState& state : m_connections) { const InterfaceConfig& config = state.m_config; - logger.debug() << "Deleting routes for hop" << config.m_hopindex; + logger.debug() << "Deleting routes for" << config.m_hopType; for (const IPAddress& ip : config.m_allowedIPAddressRanges) { - wgutils()->deleteRoutePrefix(ip, config.m_hopindex); + wgutils()->deleteRoutePrefix(ip); } wgutils()->deletePeer(config); } @@ -376,14 +417,14 @@ QString Daemon::logs() { return {}; } -void Daemon::cleanLogs() { } +void Daemon::cleanLogs() { } bool Daemon::supportServerSwitching(const InterfaceConfig& config) const { - if (!m_connections.contains(config.m_hopindex)) { + if (!m_connections.contains(config.m_hopType)) { return false; } const InterfaceConfig& current = - m_connections.value(config.m_hopindex).m_config; + m_connections.value(config.m_hopType).m_config; return current.m_privateKey == config.m_privateKey && current.m_deviceIpv4Address == config.m_deviceIpv4Address && @@ -395,21 +436,15 @@ bool Daemon::supportServerSwitching(const InterfaceConfig& config) const { bool Daemon::switchServer(const InterfaceConfig& config) { Q_ASSERT(wgutils() != nullptr); - logger.debug() << "Switching server for hop" << config.m_hopindex; + logger.debug() << "Switching server for" << config.m_hopType; - Q_ASSERT(m_connections.contains(config.m_hopindex)); + Q_ASSERT(m_connections.contains(config.m_hopType)); const InterfaceConfig& lastConfig = - m_connections.value(config.m_hopindex).m_config; + m_connections.value(config.m_hopType).m_config; // Configure routing for new excluded addresses. for (const QString& i : config.m_excludedAddresses) { - QHostAddress address(i); - if (m_excludedAddrSet.contains(address)) { - m_excludedAddrSet[address]++; - continue; - } - wgutils()->addExclusionRoute(address); - m_excludedAddrSet[address] = 1; + addExclusionRoute(IPAddress(i)); } // Activate the new peer and its routes. @@ -418,7 +453,7 @@ bool Daemon::switchServer(const InterfaceConfig& config) { return false; } for (const IPAddress& ip : config.m_allowedIPAddressRanges) { - if (!wgutils()->updateRoutePrefix(ip, config.m_hopindex)) { + if (!wgutils()->updateRoutePrefix(ip)) { logger.error() << "Server switch failed to update the routing table"; break; } @@ -426,18 +461,11 @@ bool Daemon::switchServer(const InterfaceConfig& config) { // Remove routing entries for the old peer. for (const QString& i : lastConfig.m_excludedAddresses) { - QHostAddress address(i); - Q_ASSERT(m_excludedAddrSet.contains(address)); - if (m_excludedAddrSet[address] > 1) { - m_excludedAddrSet[address]--; - continue; - } - wgutils()->deleteExclusionRoute(address); - m_excludedAddrSet.remove(address); + delExclusionRoute(QHostAddress(i)); } for (const IPAddress& ip : lastConfig.m_allowedIPAddressRanges) { if (!config.m_allowedIPAddressRanges.contains(ip)) { - wgutils()->deleteRoutePrefix(ip, config.m_hopindex); + wgutils()->deleteRoutePrefix(ip); } } @@ -448,7 +476,7 @@ bool Daemon::switchServer(const InterfaceConfig& config) { } } - m_connections[config.m_hopindex] = ConnectionState(config); + m_connections[config.m_hopType] = ConnectionState(config); return true; } @@ -457,12 +485,12 @@ QJsonObject Daemon::getStatus() { QJsonObject json; logger.debug() << "Status request"; - if (!m_connections.contains(0) || !wgutils()->interfaceExists()) { + if (!wgutils()->interfaceExists() || m_connections.isEmpty()) { json.insert("connected", QJsonValue(false)); return json; } - const ConnectionState& connection = m_connections.value(0); + const ConnectionState& connection = m_connections.first(); QList peers = wgutils()->getPeerStatus(); for (const WireguardUtils::PeerStatus& status : peers) { if (status.m_pubkey != connection.m_config.m_serverPublicKey) { @@ -495,6 +523,7 @@ void Daemon::checkHandshake() { if (connection.m_date.isValid()) { continue; } + logger.debug() << "awaiting" << config.m_serverPublicKey; // Check if the handshake has completed. for (const WireguardUtils::PeerStatus& status : peers) { diff --git a/client/daemon/daemon.h b/client/daemon/daemon.h index 8046472f..7e323ccd 100644 --- a/client/daemon/daemon.h +++ b/client/daemon/daemon.h @@ -43,11 +43,18 @@ class Daemon : public QObject { signals: void connected(const QString& pubkey); + /** + * Can be fired if a call to activate() was unsucessfull + * and connected systems should rollback + */ + void activationFailure(); void disconnected(); void backendFailure(); private: bool maybeUpdateResolvers(const InterfaceConfig& config); + bool addExclusionRoute(const IPAddress& address); + bool delExclusionRoute(const IPAddress& address); protected: virtual bool run(Op op, const InterfaceConfig& config) { @@ -75,8 +82,8 @@ class Daemon : public QObject { QDateTime m_date; InterfaceConfig m_config; }; - QMap m_connections; - QHash m_excludedAddrSet; + QMap m_connections; + QHash m_excludedAddrSet; QTimer m_handshakeTimer; }; diff --git a/client/daemon/interfaceconfig.cpp b/client/daemon/interfaceconfig.cpp new file mode 100644 index 00000000..68bebca0 --- /dev/null +++ b/client/daemon/interfaceconfig.cpp @@ -0,0 +1,122 @@ +/* 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 "interfaceconfig.h" + +#include +#include +#include +#include +#include + +QJsonObject InterfaceConfig::toJson() const { + QJsonObject json; + QMetaEnum metaEnum = QMetaEnum::fromType(); + + json.insert("hopType", QJsonValue(metaEnum.valueToKey(m_hopType))); + json.insert("privateKey", QJsonValue(m_privateKey)); + json.insert("deviceIpv4Address", QJsonValue(m_deviceIpv4Address)); + json.insert("deviceIpv6Address", QJsonValue(m_deviceIpv6Address)); + json.insert("serverPublicKey", QJsonValue(m_serverPublicKey)); + json.insert("serverPskKey", QJsonValue(m_serverPskKey)); + json.insert("serverIpv4AddrIn", QJsonValue(m_serverIpv4AddrIn)); + json.insert("serverIpv6AddrIn", QJsonValue(m_serverIpv6AddrIn)); + json.insert("serverPort", QJsonValue((double)m_serverPort)); + if ((m_hopType == InterfaceConfig::MultiHopExit) || + (m_hopType == InterfaceConfig::SingleHop)) { + json.insert("serverIpv4Gateway", QJsonValue(m_serverIpv4Gateway)); + json.insert("serverIpv6Gateway", QJsonValue(m_serverIpv6Gateway)); + json.insert("dnsServer", QJsonValue(m_dnsServer)); + } + + QJsonArray allowedIPAddesses; + for (const IPAddress& i : m_allowedIPAddressRanges) { + QJsonObject range; + range.insert("address", QJsonValue(i.address().toString())); + range.insert("range", QJsonValue((double)i.prefixLength())); + range.insert("isIpv6", + QJsonValue(i.type() == QAbstractSocket::IPv6Protocol)); + allowedIPAddesses.append(range); + }; + json.insert("allowedIPAddressRanges", allowedIPAddesses); + + QJsonArray jsExcludedAddresses; + for (const QString& i : m_excludedAddresses) { + jsExcludedAddresses.append(QJsonValue(i)); + } + json.insert("excludedAddresses", jsExcludedAddresses); + + QJsonArray disabledApps; + for (const QString& i : m_vpnDisabledApps) { + disabledApps.append(QJsonValue(i)); + } + json.insert("vpnDisabledApps", disabledApps); + + return json; +} + +QString InterfaceConfig::toWgConf(const QMap& extra) const { +#define VALIDATE(x) \ + if (x.contains("\n")) return ""; + + VALIDATE(m_privateKey); + VALIDATE(m_deviceIpv4Address); + VALIDATE(m_deviceIpv6Address); + VALIDATE(m_serverIpv4Gateway); + VALIDATE(m_serverIpv6Gateway); + VALIDATE(m_serverPublicKey); + VALIDATE(m_serverIpv4AddrIn); + VALIDATE(m_serverIpv6AddrIn); +#undef VALIDATE + + QString content; + QTextStream out(&content); + out << "[Interface]\n"; + out << "PrivateKey = " << m_privateKey << "\n"; + + QStringList addresses; + if (!m_deviceIpv4Address.isNull()) { + addresses.append(m_deviceIpv4Address); + } + if (!m_deviceIpv6Address.isNull()) { + addresses.append(m_deviceIpv6Address); + } + if (addresses.isEmpty()) { + return ""; + } + out << "Address = " << addresses.join(", ") << "\n"; + + if (!m_dnsServer.isNull()) { + QStringList dnsServers(m_dnsServer); + // If the DNS is not the Gateway, it's a user defined DNS + // thus, not add any other :) + if (m_dnsServer == m_serverIpv4Gateway) { + dnsServers.append(m_serverIpv6Gateway); + } + out << "DNS = " << dnsServers.join(", ") << "\n"; + } + + // If any extra config was provided, append it now. + for (const QString& key : extra.keys()) { + out << key << " = " << extra[key] << "\n"; + } + + out << "\n[Peer]\n"; + out << "PublicKey = " << m_serverPublicKey << "\n"; + out << "Endpoint = " << m_serverIpv4AddrIn.toUtf8() << ":" << m_serverPort + << "\n"; + + /* In theory, we should use the ipv6 endpoint, but wireguard doesn't seem + * to be happy if there are 2 endpoints. + out << "Endpoint = [" << config.m_serverIpv6AddrIn << "]:" + << config.m_serverPort << "\n"; + */ + QStringList ranges; + for (const IPAddress& ip : m_allowedIPAddressRanges) { + ranges.append(ip.toString()); + } + out << "AllowedIPs = " << ranges.join(", ") << "\n"; + + return content; +} diff --git a/client/daemon/interfaceconfig.h b/client/daemon/interfaceconfig.h index 51e8dd71..61ffdd83 100644 --- a/client/daemon/interfaceconfig.h +++ b/client/daemon/interfaceconfig.h @@ -10,22 +10,39 @@ #include "ipaddress.h" -struct InterfaceConfig { - int m_hopindex = 0; +class QJsonObject; + +class InterfaceConfig { + Q_GADGET + + public: + InterfaceConfig() {} + + enum HopType { SingleHop, MultiHopEntry, MultiHopExit }; + Q_ENUM(HopType) + + HopType m_hopType; QString m_privateKey; QString m_deviceIpv4Address; QString m_deviceIpv6Address; QString m_serverIpv4Gateway; QString m_serverIpv6Gateway; QString m_serverPublicKey; - QString m_serverPskKey; QString m_serverIpv4AddrIn; + QString m_serverPskKey; QString m_serverIpv6AddrIn; QString m_dnsServer; int m_serverPort = 0; QList m_allowedIPAddressRanges; QStringList m_excludedAddresses; QStringList m_vpnDisabledApps; +#if defined(MZ_ANDROID) || defined(MZ_IOS) + QString m_installationId; +#endif + + QJsonObject toJson() const; + QString toWgConf( + const QMap& extra = QMap()) const; }; #endif // INTERFACECONFIG_H diff --git a/client/daemon/wireguardutils.h b/client/daemon/wireguardutils.h index 278e2dfe..cdee40ef 100644 --- a/client/daemon/wireguardutils.h +++ b/client/daemon/wireguardutils.h @@ -5,6 +5,8 @@ #ifndef WIREGUARDUTILS_H #define WIREGUARDUTILS_H +#define _WINSOCKAPI_ + #include #include #include @@ -12,7 +14,7 @@ #include "interfaceconfig.h" -constexpr const char* WG_INTERFACE = "moz0"; +constexpr const char* WG_INTERFACE = "amn0"; constexpr uint16_t WG_KEEPALIVE_PERIOD = 60; @@ -41,11 +43,11 @@ class WireguardUtils : public QObject { virtual bool deletePeer(const InterfaceConfig& config) = 0; virtual QList getPeerStatus() = 0; - virtual bool updateRoutePrefix(const IPAddress& prefix, int hopindex) = 0; - virtual bool deleteRoutePrefix(const IPAddress& prefix, int hopindex) = 0; + virtual bool updateRoutePrefix(const IPAddress& prefix) = 0; + virtual bool deleteRoutePrefix(const IPAddress& prefix) = 0; - virtual bool addExclusionRoute(const QHostAddress& address) = 0; - virtual bool deleteExclusionRoute(const QHostAddress& address) = 0; + virtual bool addExclusionRoute(const IPAddress& prefix) = 0; + virtual bool deleteExclusionRoute(const IPAddress& prefix) = 0; }; #endif // WIREGUARDUTILS_H diff --git a/client/mozilla/controllerimpl.h b/client/mozilla/controllerimpl.h index 4804ba3c..6da9f7c1 100644 --- a/client/mozilla/controllerimpl.h +++ b/client/mozilla/controllerimpl.h @@ -5,9 +5,9 @@ #ifndef CONTROLLERIMPL_H #define CONTROLLERIMPL_H -#include #include #include +#include class Keys; class Device; diff --git a/client/mozilla/dnspingsender.cpp b/client/mozilla/dnspingsender.cpp index f690fdaa..ffab5661 100644 --- a/client/mozilla/dnspingsender.cpp +++ b/client/mozilla/dnspingsender.cpp @@ -52,18 +52,51 @@ DnsPingSender::DnsPingSender(const QHostAddress& source, QObject* parent) : PingSender(parent) { MZ_COUNT_CTOR(DnsPingSender); - if (source.isNull()) { - m_socket.bind(); - } else { - m_socket.bind(source); - } + m_source = source; connect(&m_socket, &QUdpSocket::readyRead, this, &DnsPingSender::readData); } DnsPingSender::~DnsPingSender() { MZ_COUNT_DTOR(DnsPingSender); } +void DnsPingSender::start() { + auto state = m_socket.state(); + if (state != QAbstractSocket::UnconnectedState) { + logger.info() + << "Attempted to start UDP socket, but it's in an invalid state:" + << state; + return; + } + + bool bindResult = false; + if (m_source.isNull()) { + bindResult = m_socket.bind(); + } else { + bindResult = m_socket.bind(m_source); + } + + if (!bindResult) { + logger.error() << "Unable to bind UDP socket. Socket state:" << state; + return; + } + + logger.debug() << "UDP socket bound to:" + << m_socket.localAddress().toString(); + return; +} + void DnsPingSender::sendPing(const QHostAddress& dest, quint16 sequence) { + if (dest.isNull()) { + logger.error() << "Attempted to send DNS ping to invalid destination:" + << dest.toString() << "Ignoring."; + return; + } + + if (!m_socket.isValid()) { + logger.error() << "Attempted to send DNS ping, but socket is invalid."; + return; + } + QByteArray packet; // Assemble a DNS query header. @@ -82,7 +115,14 @@ void DnsPingSender::sendPing(const QHostAddress& dest, quint16 sequence) { packet.append(query, sizeof(query)); // Send the datagram. - m_socket.writeDatagram(packet, dest, DNS_PORT); + logger.debug() << "Sending" << packet.size() << "bytes to UDP socket."; + auto bytesWritten = m_socket.writeDatagram(packet, dest, DNS_PORT); + + if (bytesWritten >= 0) { + logger.debug() << "Number of bytes written to UDP socket:" << bytesWritten; + } else { + logger.error() << "Error writing to UDP socket:" << m_socket.error(); + } } void DnsPingSender::readData() { @@ -112,6 +152,7 @@ void DnsPingSender::readData() { continue; } + logger.debug() << "Received valid DNS reply"; emit recvPing(qFromBigEndian(header.id)); } } diff --git a/client/mozilla/dnspingsender.h b/client/mozilla/dnspingsender.h index 6539dc8d..6c1d525f 100644 --- a/client/mozilla/dnspingsender.h +++ b/client/mozilla/dnspingsender.h @@ -19,11 +19,15 @@ class DnsPingSender final : public PingSender { void sendPing(const QHostAddress& dest, quint16 sequence) override; + void start(); + void stop() { m_socket.close(); } + private: void readData(); private: QUdpSocket m_socket; + QHostAddress m_source; }; #endif // DNSPINGSENDER_H diff --git a/client/mozilla/localsocketcontroller.cpp b/client/mozilla/localsocketcontroller.cpp index cd25ddc1..40bc0bba 100644 --- a/client/mozilla/localsocketcontroller.cpp +++ b/client/mozilla/localsocketcontroller.cpp @@ -1,7 +1,6 @@ /* 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 "protocols/protocols_defs.h" #include "localsocketcontroller.h" @@ -14,10 +13,14 @@ #include #include +//#include "errorhandler.h" #include "ipaddress.h" #include "leakdetector.h" #include "logger.h" +//#include "models/device.h" +//#include "models/keys.h" #include "models/server.h" +//#include "settingsholder.h" // How many times do we try to reconnect. constexpr int MAX_CONNECTION_RETRY = 10; @@ -112,23 +115,22 @@ void LocalSocketController::daemonConnected() { } void LocalSocketController::activate(const QJsonObject &rawConfig) { - QJsonObject wgConfig = rawConfig.value("wireguard_config_data").toObject(); QJsonObject json; json.insert("type", "activate"); -// json.insert("hopindex", QJsonValue((double)hop.m_hopindex)); + // json.insert("hopindex", QJsonValue((double)hop.m_hopindex)); json.insert("privateKey", wgConfig.value(amnezia::config_key::client_priv_key)); json.insert("deviceIpv4Address", wgConfig.value(amnezia::config_key::client_ip)); json.insert("deviceIpv6Address", "dead::1"); json.insert("serverPublicKey", wgConfig.value(amnezia::config_key::server_pub_key)); json.insert("serverPskKey", wgConfig.value(amnezia::config_key::psk_key)); json.insert("serverIpv4AddrIn", wgConfig.value(amnezia::config_key::hostName)); -// json.insert("serverIpv6AddrIn", QJsonValue(hop.m_server.ipv6AddrIn())); + // json.insert("serverIpv6AddrIn", QJsonValue(hop.m_server.ipv6AddrIn())); json.insert("serverPort", wgConfig.value(amnezia::config_key::port).toInt()); json.insert("serverIpv4Gateway", wgConfig.value(amnezia::config_key::hostName)); -// json.insert("serverIpv6Gateway", QJsonValue(hop.m_server.ipv6Gateway())); + // json.insert("serverIpv6Gateway", QJsonValue(hop.m_server.ipv6Gateway())); json.insert("dnsServer", rawConfig.value(amnezia::config_key::dns1)); QJsonArray jsAllowedIPAddesses; @@ -148,17 +150,16 @@ void LocalSocketController::activate(const QJsonObject &rawConfig) { json.insert("allowedIPAddressRanges", jsAllowedIPAddesses); - QJsonArray jsExcludedAddresses; - jsExcludedAddresses.append(wgConfig.value(amnezia::config_key::hostName)); - json.insert("excludedAddresses", jsExcludedAddresses); + QJsonArray jsExcludedAddresses; + jsExcludedAddresses.append(wgConfig.value(amnezia::config_key::hostName)); + json.insert("excludedAddresses", jsExcludedAddresses); -// QJsonArray splitTunnelApps; -// for (const auto& uri : hop.m_vpnDisabledApps) { -// splitTunnelApps.append(QJsonValue(uri)); -// } -// json.insert("vpnDisabledApps", splitTunnelApps); - + // QJsonArray splitTunnelApps; + // for (const auto& uri : hop.m_vpnDisabledApps) { + // splitTunnelApps.append(QJsonValue(uri)); + // } + // json.insert("vpnDisabledApps", splitTunnelApps); write(json); } diff --git a/client/mozilla/networkwatcher.cpp b/client/mozilla/networkwatcher.cpp index a3323fbc..54beb11c 100644 --- a/client/mozilla/networkwatcher.cpp +++ b/client/mozilla/networkwatcher.cpp @@ -6,13 +6,16 @@ #include +//#include "controller.h" #include "leakdetector.h" #include "logger.h" +//#include "mozillavpn.h" #include "networkwatcherimpl.h" #include "platforms/dummy/dummynetworkwatcher.h" +//#include "settingsholder.h" #ifdef MZ_WINDOWS -//# include "platforms/windows/windowsnetworkwatcher.h" +# include "platforms/windows/windowsnetworkwatcher.h" #endif #ifdef MZ_LINUX @@ -51,9 +54,9 @@ void NetworkWatcher::initialize() { logger.debug() << "Initialize"; #if defined(MZ_WINDOWS) - //m_impl = new WindowsNetworkWatcher(this); + m_impl = new WindowsNetworkWatcher(this); #elif defined(MZ_LINUX) - //m_impl = new LinuxNetworkWatcher(this); +// m_impl = new LinuxNetworkWatcher(this); #elif defined(MZ_MACOS) m_impl = new MacOSNetworkWatcher(this); #elif defined(MZ_WASM) @@ -73,11 +76,34 @@ void NetworkWatcher::initialize() { m_impl->initialize(); - //TODO IMPL FOR AMNEZIA + +// TODO: IMPL FOR AMNEZIA +#if 0 + SettingsHolder* settingsHolder = SettingsHolder::instance(); + Q_ASSERT(settingsHolder); + + m_active = settingsHolder->unsecuredNetworkAlert() || + settingsHolder->captivePortalAlert(); + m_reportUnsecuredNetwork = settingsHolder->unsecuredNetworkAlert(); + if (m_active) { + m_impl->start(); + } + + connect(settingsHolder, &SettingsHolder::unsecuredNetworkAlertChanged, this, + &NetworkWatcher::settingsChanged); + connect(settingsHolder, &SettingsHolder::captivePortalAlertChanged, this, + &NetworkWatcher::settingsChanged); + +#endif } void NetworkWatcher::settingsChanged() { - //TODO IMPL FOR AMNEZIA +// TODO: IMPL FOR AMNEZIA +#if 0 + SettingsHolder* settingsHolder = SettingsHolder::instance(); + m_active = settingsHolder->unsecuredNetworkAlert() || + settingsHolder->captivePortalAlert(); + m_reportUnsecuredNetwork = settingsHolder->unsecuredNetworkAlert(); if (m_active) { logger.debug() @@ -88,6 +114,7 @@ void NetworkWatcher::settingsChanged() { logger.debug() << "Stopping Network Watcher"; m_impl->stop(); } +#endif } void NetworkWatcher::unsecuredNetwork(const QString& networkName, @@ -95,9 +122,55 @@ void NetworkWatcher::unsecuredNetwork(const QString& networkName, logger.debug() << "Unsecured network:" << logger.sensitive(networkName) << "id:" << logger.sensitive(networkId); - //TODO IMPL FOR AMNEZIA +#ifndef UNIT_TEST + if (!m_reportUnsecuredNetwork) { + logger.debug() << "Disabled. Ignoring unsecured network"; + return; + } +// TODO: IMPL FOR AMNEZIA +#if 0 + MozillaVPN* vpn = MozillaVPN::instance(); + + if (vpn->state() != App::StateMain) { + logger.debug() << "VPN not ready. Ignoring unsecured network"; + return; + } + + Controller::State state = vpn->controller()->state(); + if (state == Controller::StateOn || state == Controller::StateConnecting || + state == Controller::StateCheckSubscription || + state == Controller::StateSwitching || + state == Controller::StateSilentSwitching) { + logger.debug() << "VPN on. Ignoring unsecured network"; + return; + } + + if (!m_networks.contains(networkId)) { + m_networks.insert(networkId, QElapsedTimer()); + } else if (!m_networks[networkId].hasExpired(NETWORK_WATCHER_TIMER_MSEC)) { + logger.debug() << "Notification already shown. Ignoring unsecured network"; + return; + } + + // Let's activate the QElapsedTimer to avoid notification loops. + m_networks[networkId].start(); + + // We don't connect the system tray handler in the CTOR because it can be too + // early. Maybe the NotificationHandler has not been created yet. We do it at + // the first detection of an unsecured network. + if (m_firstNotification) { + connect(NotificationHandler::instance(), + &NotificationHandler::notificationClicked, this, + &NetworkWatcher::notificationClicked); + m_firstNotification = false; + } + + NotificationHandler::instance()->unsecuredNetworkNotification(networkName); +#endif +#endif } + QString NetworkWatcher::getCurrentTransport() { auto type = m_impl->getTransportType(); QMetaEnum metaEnum = QMetaEnum::fromType(); diff --git a/client/mozilla/networkwatcher.h b/client/mozilla/networkwatcher.h index 0c4444c2..7c30416e 100644 --- a/client/mozilla/networkwatcher.h +++ b/client/mozilla/networkwatcher.h @@ -33,6 +33,8 @@ class NetworkWatcher final : public QObject { private: void settingsChanged(); + // void notificationClicked(NotificationHandler::Message message); + private: bool m_active = false; bool m_reportUnsecuredNetwork = false; diff --git a/client/mozilla/pinghelper.h b/client/mozilla/pinghelper.h index 38e3a298..00466f53 100644 --- a/client/mozilla/pinghelper.h +++ b/client/mozilla/pinghelper.h @@ -59,6 +59,10 @@ class PingHelper final : public QObject { QTimer m_pingTimer; PingSender* m_pingSender = nullptr; + +#ifdef UNIT_TEST + friend class TestConnectionHealth; +#endif }; #endif // PINGHELPER_H diff --git a/client/mozilla/pingsenderfactory.cpp b/client/mozilla/pingsenderfactory.cpp index 6e376f0d..c7ecdec8 100644 --- a/client/mozilla/pingsenderfactory.cpp +++ b/client/mozilla/pingsenderfactory.cpp @@ -9,7 +9,7 @@ #elif defined(MZ_MACOS) || defined(MZ_IOS) # include "platforms/macos/macospingsender.h" #elif defined(MZ_WINDOWS) -// #include "platforms/windows/windowspingsender.h" +# include "platforms/windows/windowspingsender.h" #elif defined(MZ_DUMMY) || defined(UNIT_TEST) # include "platforms/dummy/dummypingsender.h" #else @@ -19,13 +19,12 @@ PingSender* PingSenderFactory::create(const QHostAddress& source, QObject* parent) { #if defined(MZ_LINUX) || defined(MZ_ANDROID) - return nullptr; - //return new LinuxPingSender(source, parent); + return nullptr; + // return new LinuxPingSender(source, parent); #elif defined(MZ_MACOS) || defined(MZ_IOS) return new MacOSPingSender(source, parent); #elif defined(MZ_WINDOWS) - return nullptr; - //return new WindowsPingSender(source, parent); + return new WindowsPingSender(source, parent); #else return new DummyPingSender(source, parent); #endif diff --git a/client/mozilla/shared/ipaddress.h b/client/mozilla/shared/ipaddress.h index 05b04de2..4bb0c4b2 100644 --- a/client/mozilla/shared/ipaddress.h +++ b/client/mozilla/shared/ipaddress.h @@ -50,4 +50,9 @@ class IPAddress final { int m_prefixLength; }; +inline size_t qHash(const IPAddress& key, size_t seed) { + const QHostAddress& address = key.address(); + return qHash(address, seed) ^ key.prefixLength(); +} + #endif // IPADDRESS_H diff --git a/client/platforms/macos/daemon/macosroutemonitor.cpp b/client/platforms/macos/daemon/macosroutemonitor.cpp index 25f4662b..9f1da4ec 100644 --- a/client/platforms/macos/daemon/macosroutemonitor.cpp +++ b/client/platforms/macos/daemon/macosroutemonitor.cpp @@ -124,26 +124,23 @@ void MacosRouteMonitor::handleRtmDelete(const struct rt_msghdr* rtm, const struct sockaddr* dst = reinterpret_cast(addrlist[0].constData()); QAbstractSocket::NetworkLayerProtocol protocol; - unsigned int plen; if (dst->sa_family == AF_INET) { m_defaultGatewayIpv4.clear(); m_defaultIfindexIpv4 = 0; protocol = QAbstractSocket::IPv4Protocol; - plen = 32; } else if (dst->sa_family == AF_INET6) { m_defaultGatewayIpv6.clear(); m_defaultIfindexIpv6 = 0; protocol = QAbstractSocket::IPv6Protocol; - plen = 128; } logger.debug() << "Lost default route via" << ifname << logger.sensitive(addrToString(addrlist[1])); - for (const QHostAddress& addr : m_exclusionRoutes) { - if (addr.protocol() == protocol) { + for (const IPAddress& prefix : m_exclusionRoutes) { + if (prefix.address().protocol() == protocol) { logger.debug() << "Removing exclusion route to" - << logger.sensitive(addr.toString()); - rtmSendRoute(RTM_DELETE, addr, plen, rtm->rtm_index, nullptr); + << logger.sensitive(prefix.toString()); + rtmSendRoute(RTM_DELETE, prefix, rtm->rtm_index, nullptr); } } } @@ -227,7 +224,6 @@ void MacosRouteMonitor::handleRtmUpdate(const struct rt_msghdr* rtm, const struct sockaddr* dst = reinterpret_cast(addrlist[0].constData()); QAbstractSocket::NetworkLayerProtocol protocol; - unsigned int plen; int rtm_type = RTM_ADD; if (dst->sa_family == AF_INET) { if (m_defaultIfindexIpv4 != 0) { @@ -236,7 +232,6 @@ void MacosRouteMonitor::handleRtmUpdate(const struct rt_msghdr* rtm, m_defaultGatewayIpv4 = addrlist[1]; m_defaultIfindexIpv4 = ifindex; protocol = QAbstractSocket::IPv4Protocol; - plen = 32; } else if (dst->sa_family == AF_INET6) { if (m_defaultIfindexIpv6 != 0) { rtm_type = RTM_CHANGE; @@ -244,7 +239,6 @@ void MacosRouteMonitor::handleRtmUpdate(const struct rt_msghdr* rtm, m_defaultGatewayIpv6 = addrlist[1]; m_defaultIfindexIpv6 = ifindex; protocol = QAbstractSocket::IPv6Protocol; - plen = 128; } else { return; } @@ -252,11 +246,11 @@ void MacosRouteMonitor::handleRtmUpdate(const struct rt_msghdr* rtm, // Update the exclusion routes with the new default route. logger.debug() << "Updating default route via" << ifname << addrToString(addrlist[1]); - for (const QHostAddress& addr : m_exclusionRoutes) { - if (addr.protocol() == protocol) { + for (const IPAddress& prefix : m_exclusionRoutes) { + if (prefix.address().protocol() == protocol) { logger.debug() << "Updating exclusion route to" - << logger.sensitive(addr.toString()); - rtmSendRoute(rtm_type, addr, plen, ifindex, addrlist[1].constData()); + << logger.sensitive(prefix.toString()); + rtmSendRoute(rtm_type, prefix, ifindex, addrlist[1].constData()); } } } @@ -353,8 +347,8 @@ void MacosRouteMonitor::rtmAppendAddr(struct rt_msghdr* rtm, size_t maxlen, } } -bool MacosRouteMonitor::rtmSendRoute(int action, const QHostAddress& prefix, - unsigned int plen, unsigned int ifindex, +bool MacosRouteMonitor::rtmSendRoute(int action, const IPAddress& prefix, + unsigned int ifindex, const void* gateway) { constexpr size_t rtm_max_size = sizeof(struct rt_msghdr) + sizeof(struct sockaddr_in6) * 2 + @@ -375,9 +369,9 @@ bool MacosRouteMonitor::rtmSendRoute(int action, const QHostAddress& prefix, memset(&rtm->rtm_rmx, 0, sizeof(rtm->rtm_rmx)); // Append RTA_DST - if (prefix.protocol() == QAbstractSocket::IPv6Protocol) { + if (prefix.address().protocol() == QAbstractSocket::IPv6Protocol) { struct sockaddr_in6 sin6; - Q_IPV6ADDR dst = prefix.toIPv6Address(); + Q_IPV6ADDR dst = prefix.address().toIPv6Address(); memset(&sin6, 0, sizeof(sin6)); sin6.sin6_family = AF_INET6; sin6.sin6_len = sizeof(sin6); @@ -385,7 +379,7 @@ bool MacosRouteMonitor::rtmSendRoute(int action, const QHostAddress& prefix, rtmAppendAddr(rtm, rtm_max_size, RTA_DST, &sin6); } else { struct sockaddr_in sin; - quint32 dst = prefix.toIPv4Address(); + quint32 dst = prefix.address().toIPv4Address(); memset(&sin, 0, sizeof(sin)); sin.sin_family = AF_INET; sin.sin_len = sizeof(sin); @@ -403,7 +397,8 @@ bool MacosRouteMonitor::rtmSendRoute(int action, const QHostAddress& prefix, } // Append RTA_NETMASK - if (prefix.protocol() == QAbstractSocket::IPv6Protocol) { + unsigned int plen = prefix.prefixLength(); + if (prefix.address().protocol() == QAbstractSocket::IPv6Protocol) { struct sockaddr_in6 sin6; memset(&sin6, 0, sizeof(sin6)); sin6.sin6_family = AF_INET6; @@ -413,7 +408,7 @@ bool MacosRouteMonitor::rtmSendRoute(int action, const QHostAddress& prefix, sin6.sin6_addr.s6_addr[plen / 8] = 0xFF ^ (0xFF >> (plen % 8)); } rtmAppendAddr(rtm, rtm_max_size, RTA_NETMASK, &sin6); - } else if (prefix.protocol() == QAbstractSocket::IPv4Protocol) { + } else if (prefix.address().protocol() == QAbstractSocket::IPv4Protocol) { struct sockaddr_in sin; memset(&sin, 0, sizeof(sin)); sin.sin_family = AF_INET; @@ -497,34 +492,32 @@ bool MacosRouteMonitor::insertRoute(const IPAddress& prefix) { datalink.sdl_slen = 0; memcpy(&datalink.sdl_data, qPrintable(m_ifname), datalink.sdl_nlen); - return rtmSendRoute(RTM_ADD, prefix.address(), prefix.prefixLength(), - m_ifindex, &datalink); + return rtmSendRoute(RTM_ADD, prefix, m_ifindex, &datalink); } bool MacosRouteMonitor::deleteRoute(const IPAddress& prefix) { - return rtmSendRoute(RTM_DELETE, prefix.address(), prefix.prefixLength(), - m_ifindex, nullptr); + return rtmSendRoute(RTM_DELETE, prefix, m_ifindex, nullptr); } -bool MacosRouteMonitor::addExclusionRoute(const QHostAddress& address) { +bool MacosRouteMonitor::addExclusionRoute(const IPAddress& prefix) { logger.debug() << "Adding exclusion route for" - << logger.sensitive(address.toString()); + << logger.sensitive(prefix.toString()); - if (m_exclusionRoutes.contains(address)) { + if (m_exclusionRoutes.contains(prefix)) { logger.warning() << "Exclusion route already exists"; return false; } - m_exclusionRoutes.append(address); + m_exclusionRoutes.append(prefix); // If the default route is known, then updte the routing table immediately. - if ((address.protocol() == QAbstractSocket::IPv4Protocol) && + if ((prefix.address().protocol() == QAbstractSocket::IPv4Protocol) && (m_defaultIfindexIpv4 != 0) && !m_defaultGatewayIpv4.isEmpty()) { - return rtmSendRoute(RTM_ADD, address, 32, m_defaultIfindexIpv4, + return rtmSendRoute(RTM_ADD, prefix, m_defaultIfindexIpv4, m_defaultGatewayIpv4.constData()); } - if ((address.protocol() == QAbstractSocket::IPv6Protocol) && + if ((prefix.address().protocol() == QAbstractSocket::IPv6Protocol) && (m_defaultIfindexIpv6 != 0) && !m_defaultGatewayIpv6.isEmpty()) { - return rtmSendRoute(RTM_ADD, address, 128, m_defaultIfindexIpv6, + return rtmSendRoute(RTM_ADD, prefix, m_defaultIfindexIpv6, m_defaultGatewayIpv6.constData()); } @@ -532,16 +525,15 @@ bool MacosRouteMonitor::addExclusionRoute(const QHostAddress& address) { return true; } -bool MacosRouteMonitor::deleteExclusionRoute(const QHostAddress& address) { +bool MacosRouteMonitor::deleteExclusionRoute(const IPAddress& prefix) { logger.debug() << "Deleting exclusion route for" - << logger.sensitive(address.toString()); + << logger.sensitive(prefix.toString()); - m_exclusionRoutes.removeAll(address); - if (address.protocol() == QAbstractSocket::IPv4Protocol) { - return rtmSendRoute(RTM_DELETE, address, 32, m_defaultIfindexIpv4, nullptr); - } else if (address.protocol() == QAbstractSocket::IPv6Protocol) { - return rtmSendRoute(RTM_DELETE, address, 128, m_defaultIfindexIpv6, - nullptr); + m_exclusionRoutes.removeAll(prefix); + if (prefix.address().protocol() == QAbstractSocket::IPv4Protocol) { + return rtmSendRoute(RTM_DELETE, prefix, m_defaultIfindexIpv4, nullptr); + } else if (prefix.address().protocol() == QAbstractSocket::IPv6Protocol) { + return rtmSendRoute(RTM_DELETE, prefix, m_defaultIfindexIpv6, nullptr); } else { return false; } @@ -549,11 +541,11 @@ bool MacosRouteMonitor::deleteExclusionRoute(const QHostAddress& address) { void MacosRouteMonitor::flushExclusionRoutes() { while (!m_exclusionRoutes.isEmpty()) { - QHostAddress address = m_exclusionRoutes.takeFirst(); - if (address.protocol() == QAbstractSocket::IPv4Protocol) { - rtmSendRoute(RTM_DELETE, address, 32, m_defaultIfindexIpv4, nullptr); - } else if (address.protocol() == QAbstractSocket::IPv6Protocol) { - rtmSendRoute(RTM_DELETE, address, 128, m_defaultIfindexIpv6, nullptr); + IPAddress prefix = m_exclusionRoutes.takeFirst(); + if (prefix.address().protocol() == QAbstractSocket::IPv4Protocol) { + rtmSendRoute(RTM_DELETE, prefix, m_defaultIfindexIpv4, nullptr); + } else if (prefix.address().protocol() == QAbstractSocket::IPv6Protocol) { + rtmSendRoute(RTM_DELETE, prefix, m_defaultIfindexIpv6, nullptr); } } } diff --git a/client/platforms/macos/daemon/macosroutemonitor.h b/client/platforms/macos/daemon/macosroutemonitor.h index 2d5c54bb..b2483d76 100644 --- a/client/platforms/macos/daemon/macosroutemonitor.h +++ b/client/platforms/macos/daemon/macosroutemonitor.h @@ -28,16 +28,16 @@ class MacosRouteMonitor final : public QObject { bool deleteRoute(const IPAddress& prefix); int interfaceFlags() { return m_ifflags; } - bool addExclusionRoute(const QHostAddress& address); - bool deleteExclusionRoute(const QHostAddress& address); + bool addExclusionRoute(const IPAddress& prefix); + bool deleteExclusionRoute(const IPAddress& prefix); void flushExclusionRoutes(); private: void handleRtmDelete(const struct rt_msghdr* msg, const QByteArray& payload); void handleRtmUpdate(const struct rt_msghdr* msg, const QByteArray& payload); void handleIfaceInfo(const struct if_msghdr* msg, const QByteArray& payload); - bool rtmSendRoute(int action, const QHostAddress& prefix, unsigned int plen, - unsigned int ifindex, const void* gateway); + bool rtmSendRoute(int action, const IPAddress& prefix, unsigned int ifindex, + const void* gateway); bool rtmFetchRoutes(int family); static void rtmAppendAddr(struct rt_msghdr* rtm, size_t maxlen, int rtaddr, const void* sa); @@ -50,7 +50,7 @@ class MacosRouteMonitor final : public QObject { static QString addrToString(const struct sockaddr* sa); static QString addrToString(const QByteArray& data); - QList m_exclusionRoutes; + QList m_exclusionRoutes; QByteArray m_defaultGatewayIpv4; QByteArray m_defaultGatewayIpv6; unsigned int m_defaultIfindexIpv4 = 0; diff --git a/client/platforms/macos/daemon/wireguardutilsmacos.cpp b/client/platforms/macos/daemon/wireguardutilsmacos.cpp index eaaf6dec..1f422462 100644 --- a/client/platforms/macos/daemon/wireguardutilsmacos.cpp +++ b/client/platforms/macos/daemon/wireguardutilsmacos.cpp @@ -97,7 +97,6 @@ bool WireguardUtilsMacos::addInterface(const InterfaceConfig& config) { // Send a UAPI command to configure the interface QString message("set=1\n"); QByteArray privateKey = QByteArray::fromBase64(config.m_privateKey.toUtf8()); - QTextStream out(&message); out << "private_key=" << QString(privateKey.toHex()) << "\n"; out << "replace_peers=true\n"; @@ -133,9 +132,14 @@ bool WireguardUtilsMacos::deleteInterface() { // dummy implementations for now bool WireguardUtilsMacos::updatePeer(const InterfaceConfig& config) { - QByteArray publicKey = QByteArray::fromBase64(qPrintable(config.m_serverPublicKey)); + QByteArray publicKey = + QByteArray::fromBase64(qPrintable(config.m_serverPublicKey)); + QByteArray pskKey = QByteArray::fromBase64(qPrintable(config.m_serverPskKey)); + logger.debug() << "Configuring peer" << config.m_serverPublicKey + << "via" << config.m_serverIpv4AddrIn; + // Update/create the peer config QString message; QTextStream out(&message); @@ -150,6 +154,7 @@ bool WireguardUtilsMacos::updatePeer(const InterfaceConfig& config) { logger.warning() << "Failed to create peer with no endpoints"; return false; } + out << config.m_serverPort << "\n"; out << "replace_allowed_ips=true\n"; @@ -158,7 +163,13 @@ bool WireguardUtilsMacos::updatePeer(const InterfaceConfig& config) { out << "allowed_ip=" << ip.toString() << "\n"; } - logger.debug() << message; + // Exclude the server address, except for multihop exit servers. + if ((config.m_hopType != InterfaceConfig::MultiHopExit) && + (m_rtmonitor != nullptr)) { + m_rtmonitor->addExclusionRoute(IPAddress(config.m_serverIpv4AddrIn)); + m_rtmonitor->addExclusionRoute(IPAddress(config.m_serverIpv6AddrIn)); + } + int err = uapiErrno(uapiCommand(message)); if (err != 0) { logger.error() << "Peer configuration failed:" << strerror(err); @@ -170,6 +181,13 @@ bool WireguardUtilsMacos::deletePeer(const InterfaceConfig& config) { QByteArray publicKey = QByteArray::fromBase64(qPrintable(config.m_serverPublicKey)); + // Clear exclustion routes for this peer. + if ((config.m_hopType != InterfaceConfig::MultiHopExit) && + (m_rtmonitor != nullptr)) { + m_rtmonitor->deleteExclusionRoute(IPAddress(config.m_serverIpv4AddrIn)); + m_rtmonitor->deleteExclusionRoute(IPAddress(config.m_serverIpv6AddrIn)); + } + QString message; QTextStream out(&message); out << "set=1\n"; @@ -223,9 +241,7 @@ QList WireguardUtilsMacos::getPeerStatus() { return peerList; } -bool WireguardUtilsMacos::updateRoutePrefix(const IPAddress& prefix, - int hopindex) { - Q_UNUSED(hopindex); +bool WireguardUtilsMacos::updateRoutePrefix(const IPAddress& prefix) { if (!m_rtmonitor) { return false; } @@ -246,9 +262,7 @@ bool WireguardUtilsMacos::updateRoutePrefix(const IPAddress& prefix, return false; } -bool WireguardUtilsMacos::deleteRoutePrefix(const IPAddress& prefix, - int hopindex) { - Q_UNUSED(hopindex); +bool WireguardUtilsMacos::deleteRoutePrefix(const IPAddress& prefix) { if (!m_rtmonitor) { return false; } @@ -268,18 +282,18 @@ bool WireguardUtilsMacos::deleteRoutePrefix(const IPAddress& prefix, } } -bool WireguardUtilsMacos::addExclusionRoute(const QHostAddress& address) { +bool WireguardUtilsMacos::addExclusionRoute(const IPAddress& prefix) { if (!m_rtmonitor) { return false; } - return m_rtmonitor->addExclusionRoute(address); + return m_rtmonitor->addExclusionRoute(prefix); } -bool WireguardUtilsMacos::deleteExclusionRoute(const QHostAddress& address) { +bool WireguardUtilsMacos::deleteExclusionRoute(const IPAddress& prefix) { if (!m_rtmonitor) { return false; } - return m_rtmonitor->deleteExclusionRoute(address); + return m_rtmonitor->deleteExclusionRoute(prefix); } QString WireguardUtilsMacos::uapiCommand(const QString& command) { diff --git a/client/platforms/macos/daemon/wireguardutilsmacos.h b/client/platforms/macos/daemon/wireguardutilsmacos.h index ba830c1c..aa9f19eb 100644 --- a/client/platforms/macos/daemon/wireguardutilsmacos.h +++ b/client/platforms/macos/daemon/wireguardutilsmacos.h @@ -29,11 +29,11 @@ class WireguardUtilsMacos final : public WireguardUtils { bool deletePeer(const InterfaceConfig& config) override; QList getPeerStatus() override; - bool updateRoutePrefix(const IPAddress& prefix, int hopindex) override; - bool deleteRoutePrefix(const IPAddress& prefix, int hopindex) override; + bool updateRoutePrefix(const IPAddress& prefix) override; + bool deleteRoutePrefix(const IPAddress& prefix) override; - bool addExclusionRoute(const QHostAddress& address) override; - bool deleteExclusionRoute(const QHostAddress& address) override; + bool addExclusionRoute(const IPAddress& prefix) override; + bool deleteExclusionRoute(const IPAddress& prefix) override; signals: void backendFailure(); diff --git a/client/platforms/macos/macosutils.mm b/client/platforms/macos/macosutils.mm index a704f428..cbe30583 100644 --- a/client/platforms/macos/macosutils.mm +++ b/client/platforms/macos/macosutils.mm @@ -55,6 +55,7 @@ bool dockClickHandler(id self, SEL cmd, ...) { Q_UNUSED(cmd); logger.debug() << "Dock icon clicked."; + //TODO IMPL FOR AMNEZIA //QmlEngineHolder::instance()->showWindow(); return FALSE; diff --git a/client/platforms/windows/daemon/dnsutilswindows.cpp b/client/platforms/windows/daemon/dnsutilswindows.cpp new file mode 100644 index 00000000..a6485529 --- /dev/null +++ b/client/platforms/windows/daemon/dnsutilswindows.cpp @@ -0,0 +1,177 @@ +/* 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 "dnsutilswindows.h" + +#include +#include + +#include +#include + +#include "leakdetector.h" +#include "logger.h" + +constexpr uint32_t WINDOWS_NETSH_TIMEOUT_MSEC = 2000; + +namespace { +Logger logger("DnsUtilsWindows"); +} + +DnsUtilsWindows::DnsUtilsWindows(QObject* parent) : DnsUtils(parent) { + MZ_COUNT_CTOR(DnsUtilsWindows); + logger.debug() << "DnsUtilsWindows created."; + + typedef DWORD WindowsSetDnsCallType(GUID, const void*); + HMODULE library = LoadLibrary(TEXT("iphlpapi.dll")); + if (library) { + m_setInterfaceDnsSettingsProcAddr = (WindowsSetDnsCallType*)GetProcAddress( + library, "SetInterfaceDnsSettings"); + } +} + +DnsUtilsWindows::~DnsUtilsWindows() { + MZ_COUNT_DTOR(DnsUtilsWindows); + restoreResolvers(); + logger.debug() << "DnsUtilsWindows destroyed."; +} + +bool DnsUtilsWindows::updateResolvers(const QString& ifname, + const QList& resolvers) { + NET_LUID luid; + if (ConvertInterfaceAliasToLuid((wchar_t*)ifname.utf16(), &luid) != 0) { + logger.error() << "Failed to resolve LUID for" << ifname; + return false; + } + m_luid = luid.Value; + + logger.debug() << "Configuring DNS for" << ifname; + if (m_setInterfaceDnsSettingsProcAddr == nullptr) { + return updateResolversNetsh(resolvers); + } + return updateResolversWin32(resolvers); +} + +bool DnsUtilsWindows::updateResolversWin32( + const QList& resolvers) { + GUID guid; + NET_LUID luid; + luid.Value = m_luid; + if (ConvertInterfaceLuidToGuid(&luid, &guid) != NO_ERROR) { + logger.error() << "Failed to resolve GUID"; + return false; + } + + QStringList v4resolvers; + QStringList v6resolvers; + for (const QHostAddress& addr : resolvers) { + if (addr.protocol() == QAbstractSocket::IPv4Protocol) { + v4resolvers.append(addr.toString()); + } + if (addr.protocol() == QAbstractSocket::IPv6Protocol) { + v6resolvers.append(addr.toString()); + } + } + + DNS_INTERFACE_SETTINGS settings; + settings.Version = DNS_INTERFACE_SETTINGS_VERSION1; + settings.Flags = DNS_SETTING_NAMESERVER | DNS_SETTING_SEARCHLIST; + settings.Domain = nullptr; + settings.NameServer = nullptr; + settings.SearchList = (wchar_t*)L"."; + settings.RegistrationEnabled = false; + settings.RegisterAdapterName = false; + settings.EnableLLMNR = false; + settings.QueryAdapterName = false; + settings.ProfileNameServer = nullptr; + + // Configure nameservers for IPv4 + QString v4resolverstring = v4resolvers.join(","); + settings.NameServer = (wchar_t*)v4resolverstring.utf16(); + DWORD v4result = m_setInterfaceDnsSettingsProcAddr(guid, &settings); + if (v4result != NO_ERROR) { + logger.error() << "Failed to configure IPv4 resolvers:" << v4result; + } + + // Configure nameservers for IPv6 + QString v6resolverstring = v6resolvers.join(","); + settings.Flags |= DNS_SETTING_IPV6; + settings.NameServer = (wchar_t*)v6resolverstring.utf16(); + DWORD v6result = m_setInterfaceDnsSettingsProcAddr(guid, &settings); + if (v6result != NO_ERROR) { + logger.error() << "Failed to configure IPv6 resolvers" << v6result; + } + + return ((v4result == NO_ERROR) && (v6result == NO_ERROR)); +} + +constexpr const char* netshFlushTemplate = + "interface %1 set dnsservers name=%2 address=none valdiate=no " + "register=both\r\n"; +constexpr const char* netshAddTemplate = + "interface %1 add dnsservers name=%2 address=%3 validate=no\r\n"; + +bool DnsUtilsWindows::updateResolversNetsh( + const QList& resolvers) { + QProcess netsh; + NET_LUID luid; + NET_IFINDEX ifindex; + luid.Value = m_luid; + if (ConvertInterfaceLuidToIndex(&luid, &ifindex) != NO_ERROR) { + logger.error() << "Failed to resolve GUID"; + return false; + } + + netsh.setProgram("netsh"); + netsh.start(); + if (!netsh.waitForStarted(WINDOWS_NETSH_TIMEOUT_MSEC)) { + logger.error() << "Failed to start netsh"; + return false; + } + + QTextStream cmdstream(&netsh); + + // Flush DNS servers + QString v4flush = QString(netshFlushTemplate).arg("ipv4").arg(ifindex); + QString v6flush = QString(netshFlushTemplate).arg("ipv6").arg(ifindex); + logger.debug() << "netsh write:" << v4flush.trimmed(); + cmdstream << v4flush; + logger.debug() << "netsh write:" << v6flush.trimmed(); + cmdstream << v6flush; + + // Add new DNS servers + for (const QHostAddress& addr : resolvers) { + const char* family = "ipv4"; + if (addr.protocol() == QAbstractSocket::IPv6Protocol) { + family = "ipv6"; + } + QString nsAddr = addr.toString(); + QString nsCommand = + QString(netshAddTemplate).arg(family).arg(ifindex).arg(nsAddr); + logger.debug() << "netsh write:" << nsCommand.trimmed(); + cmdstream << nsCommand; + } + + // Exit and cleanup netsh + cmdstream << "exit\r\n"; + cmdstream.flush(); + if (!netsh.waitForFinished(WINDOWS_NETSH_TIMEOUT_MSEC)) { + logger.error() << "Failed to exit netsh"; + return false; + } + + return netsh.exitCode() == 0; +} + +bool DnsUtilsWindows::restoreResolvers() { + if (m_luid == 0) { + return true; + } + + QList empty; + if (m_setInterfaceDnsSettingsProcAddr == nullptr) { + return updateResolversNetsh(empty); + } + return updateResolversWin32(empty); +} diff --git a/client/platforms/windows/daemon/dnsutilswindows.h b/client/platforms/windows/daemon/dnsutilswindows.h new file mode 100644 index 00000000..7d0573e4 --- /dev/null +++ b/client/platforms/windows/daemon/dnsutilswindows.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 DNSUTILSWINDOWS_H +#define DNSUTILSWINDOWS_H + +#include + +#include +#include + +#include "daemon/dnsutils.h" + +class DnsUtilsWindows final : public DnsUtils { + Q_OBJECT + Q_DISABLE_COPY_MOVE(DnsUtilsWindows) + + public: + explicit DnsUtilsWindows(QObject* parent); + virtual ~DnsUtilsWindows(); + bool updateResolvers(const QString& ifname, + const QList& resolvers) override; + bool restoreResolvers() override; + + private: + quint64 m_luid = 0; + DWORD (*m_setInterfaceDnsSettingsProcAddr)(GUID, const void*) = nullptr; + + bool updateResolversWin32(const QList& resolvers); + bool updateResolversNetsh(const QList& resolvers); +}; + +#endif // DNSUTILSWINDOWS_H diff --git a/client/platforms/windows/daemon/windowsdaemon.cpp b/client/platforms/windows/daemon/windowsdaemon.cpp new file mode 100644 index 00000000..b697a3b0 --- /dev/null +++ b/client/platforms/windows/daemon/windowsdaemon.cpp @@ -0,0 +1,81 @@ +/* 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 "windowsdaemon.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "dnsutilswindows.h" +#include "leakdetector.h" +#include "logger.h" +#include "platforms/windows/windowscommons.h" +#include "platforms/windows/windowsservicemanager.h" +#include "windowsfirewall.h" + +namespace { +Logger logger("WindowsDaemon"); +} + +WindowsDaemon::WindowsDaemon() : Daemon(nullptr), m_splitTunnelManager(this) { + MZ_COUNT_CTOR(WindowsDaemon); + + m_wgutils = new WireguardUtilsWindows(this); + m_dnsutils = new DnsUtilsWindows(this); + + connect(m_wgutils, &WireguardUtilsWindows::backendFailure, this, + &WindowsDaemon::monitorBackendFailure); + connect(this, &WindowsDaemon::activationFailure, + []() { WindowsFirewall::instance()->disableKillSwitch(); }); +} + +WindowsDaemon::~WindowsDaemon() { + MZ_COUNT_DTOR(WindowsDaemon); + logger.debug() << "Daemon released"; +} + +void WindowsDaemon::prepareActivation(const InterfaceConfig& config) { + // Before creating the interface we need to check which adapter + // routes to the server endpoint + auto serveraddr = QHostAddress(config.m_serverIpv4AddrIn); + m_inetAdapterIndex = WindowsCommons::AdapterIndexTo(serveraddr); +} + +bool WindowsDaemon::run(Op op, const InterfaceConfig& config) { + if (op == Down) { + m_splitTunnelManager.stop(); + return true; + } + + if (op == Up) { + logger.debug() << "Tunnel UP, Starting SplitTunneling"; + if (!WindowsSplitTunnel::isInstalled()) { + logger.warning() << "Split Tunnel Driver not Installed yet, fixing this."; + WindowsSplitTunnel::installDriver(); + } + } + + if (config.m_vpnDisabledApps.length() > 0) { + m_splitTunnelManager.start(m_inetAdapterIndex); + m_splitTunnelManager.setRules(config.m_vpnDisabledApps); + } else { + m_splitTunnelManager.stop(); + } + return true; +} + +void WindowsDaemon::monitorBackendFailure() { + logger.warning() << "Tunnel service is down"; + + emit backendFailure(); + deactivate(); +} diff --git a/client/platforms/windows/daemon/windowsdaemon.h b/client/platforms/windows/daemon/windowsdaemon.h new file mode 100644 index 00000000..7a1b3059 --- /dev/null +++ b/client/platforms/windows/daemon/windowsdaemon.h @@ -0,0 +1,48 @@ +/* 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 WINDOWSDAEMON_H +#define WINDOWSDAEMON_H + +#include "daemon/daemon.h" +#include "dnsutilswindows.h" +#include "windowssplittunnel.h" +#include "windowstunnelservice.h" +#include "wireguardutilswindows.h" + +#define TUNNEL_SERVICE_NAME L"WireGuardTunnel$AmneziaVPN" + +class WindowsDaemon final : public Daemon { + Q_DISABLE_COPY_MOVE(WindowsDaemon) + + public: + WindowsDaemon(); + ~WindowsDaemon(); + + void prepareActivation(const InterfaceConfig& config) override; + + protected: + bool run(Op op, const InterfaceConfig& config) override; + WireguardUtils* wgutils() const override { return m_wgutils; } + bool supportDnsUtils() const override { return true; } + DnsUtils* dnsutils() override { return m_dnsutils; } + + private: + void monitorBackendFailure(); + + private: + enum State { + Active, + Inactive, + }; + + State m_state = Inactive; + int m_inetAdapterIndex = -1; + + WireguardUtilsWindows* m_wgutils = nullptr; + DnsUtilsWindows* m_dnsutils = nullptr; + WindowsSplitTunnel m_splitTunnelManager; +}; + +#endif // WINDOWSDAEMON_H diff --git a/client/platforms/windows/daemon/windowsdaemontunnel.cpp b/client/platforms/windows/daemon/windowsdaemontunnel.cpp new file mode 100644 index 00000000..c2adeb47 --- /dev/null +++ b/client/platforms/windows/daemon/windowsdaemontunnel.cpp @@ -0,0 +1,77 @@ +/* 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 "windowsdaemontunnel.h" + +#include + +#include + +//#include "commandlineparser.h" +#include "constants.h" +#include "leakdetector.h" +#include "logger.h" +#include "platforms/windows/daemon/wireguardutilswindows.h" +#include "platforms/windows/windowsutils.h" + +namespace { +Logger logger("WindowsDaemonTunnel"); +} // namespace + +WindowsDaemonTunnel::WindowsDaemonTunnel() { + MZ_COUNT_CTOR(WindowsDaemonTunnel); +} + +WindowsDaemonTunnel::~WindowsDaemonTunnel() { + MZ_COUNT_DTOR(WindowsDaemonTunnel); +} + +int WindowsDaemonTunnel::run(QStringList& tokens) { + Q_ASSERT(!tokens.isEmpty()); + + logger.debug() << "Tunnel daemon service is starting"; + + QCoreApplication app(); + + QCoreApplication::setApplicationName("Amnezia VPN Tunnel"); + QCoreApplication::setApplicationVersion(Constants::versionString()); + + if (tokens.length() != 2) { + logger.error() << "Expected 1 parameter only: the config file."; + return 1; + } + QString maybeConfig = tokens.at(1); + + if (!maybeConfig.startsWith("[Interface]")) { + logger.error() << "parameter Does not seem to be a config"; + return 1; + } + // This process will be used by the wireguard tunnel. No need to call + // FreeLibrary. + HMODULE tunnelLib = LoadLibrary(TEXT("tunnel.dll")); + if (!tunnelLib) { + WindowsUtils::windowsLog("Failed to load tunnel.dll"); + return 1; + } + + typedef bool WireGuardTunnelService(const ushort* settings, + const ushort* name); + + WireGuardTunnelService* tunnelProc = (WireGuardTunnelService*)GetProcAddress( + tunnelLib, "WireGuardTunnelService"); + if (!tunnelProc) { + WindowsUtils::windowsLog("Failed to get WireGuardTunnelService function"); + return 1; + } + auto name = WireguardUtilsWindows::s_interfaceName(); + if (!tunnelProc(maybeConfig.utf16(), name.utf16())) { + logger.error() << "Failed to activate the tunnel service"; + return 1; + } + + return 0; +} + +//static Command::RegistrationProxy +// s_commandWindowsDaemonTunnel; diff --git a/client/platforms/windows/daemon/windowsdaemontunnel.h b/client/platforms/windows/daemon/windowsdaemontunnel.h new file mode 100644 index 00000000..569cccf1 --- /dev/null +++ b/client/platforms/windows/daemon/windowsdaemontunnel.h @@ -0,0 +1,18 @@ +/* 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 WINDOWSDAEMONTUNNEL_H +#define WINDOWSDAEMONTUNNEL_H + +#include + +class WindowsDaemonTunnel { + public: + explicit WindowsDaemonTunnel(); + ~WindowsDaemonTunnel(); + + int run(QStringList& tokens); +}; + +#endif // WINDOWSDAEMONTUNNEL_H diff --git a/client/platforms/windows/daemon/windowsfirewall.cpp b/client/platforms/windows/daemon/windowsfirewall.cpp new file mode 100644 index 00000000..2cf5e205 --- /dev/null +++ b/client/platforms/windows/daemon/windowsfirewall.cpp @@ -0,0 +1,850 @@ +/* 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 "windowsfirewall.h" + +#include +#include +#include +#include +#include +//#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "ipaddress.h" +#include "leakdetector.h" +#include "logger.h" +#include "platforms/windows/windowsutils.h" +#include "winsock.h" + +#define IPV6_ADDRESS_SIZE 16 + +// ID for the Firewall Sublayer +DEFINE_GUID(ST_FW_WINFW_BASELINE_SUBLAYER_KEY, 0xc78056ff, 0x2bc1, 0x4211, 0xaa, + 0xdd, 0x7f, 0x35, 0x8d, 0xef, 0x20, 0x2d); +// ID for the Mullvad Split-Tunnel Sublayer Provider +DEFINE_GUID(ST_FW_PROVIDER_KEY, 0xe2c114ee, 0xf32a, 0x4264, 0xa6, 0xcb, 0x3f, + 0xa7, 0x99, 0x63, 0x56, 0xd9); + +namespace { +Logger logger("WindowsFirewall"); +WindowsFirewall* s_instance = nullptr; + +// Note Filter Weight may be between 0-15! +constexpr uint8_t LOW_WEIGHT = 0; +constexpr uint8_t MED_WEIGHT = 7; +constexpr uint8_t HIGH_WEIGHT = 13; +constexpr uint8_t MAX_WEIGHT = 15; +} // namespace + +WindowsFirewall* WindowsFirewall::instance() { + if (s_instance == nullptr) { + s_instance = new WindowsFirewall(qApp); + } + return s_instance; +} + +WindowsFirewall::WindowsFirewall(QObject* parent) : QObject(parent) { + MZ_COUNT_CTOR(WindowsFirewall); + Q_ASSERT(s_instance == nullptr); + + HANDLE engineHandle = NULL; + DWORD result = ERROR_SUCCESS; + // Use dynamic sessions for efficiency and safety: + // -> Filtering policy objects are deleted even when the application crashes/ + // deamon goes down + FWPM_SESSION0 session; + memset(&session, 0, sizeof(session)); + session.flags = FWPM_SESSION_FLAG_DYNAMIC; + + logger.debug() << "Opening the filter engine."; + + result = + FwpmEngineOpen0(NULL, RPC_C_AUTHN_WINNT, NULL, &session, &engineHandle); + + if (result != ERROR_SUCCESS) { + WindowsUtils::windowsLog("FwpmEngineOpen0 failed"); + return; + } + logger.debug() << "Filter engine opened successfully."; + m_sessionHandle = engineHandle; +} + +WindowsFirewall::~WindowsFirewall() { + MZ_COUNT_DTOR(WindowsFirewall); + if (m_sessionHandle != INVALID_HANDLE_VALUE) { + CloseHandle(m_sessionHandle); + } +} + +bool WindowsFirewall::init() { + if (m_init) { + logger.warning() << "Alread initialised FW_WFP layer"; + return true; + } + if (m_sessionHandle == INVALID_HANDLE_VALUE) { + logger.error() << "Cant Init Sublayer with invalid wfp handle"; + return false; + } + // If we were not able to aquire a handle, this will fail anyway. + // We need to open up another handle because of wfp rules: + // If a wfp resource was created with SESSION_DYNAMIC, + // the session exlusively owns the resource, meaning the driver can't add + // filters to the sublayer. So let's have non dynamic session only for the + // sublayer creation. This means the Layer exists until the next Reboot. + DWORD result = ERROR_SUCCESS; + HANDLE wfp = INVALID_HANDLE_VALUE; + FWPM_SESSION0 session; + memset(&session, 0, sizeof(session)); + + logger.debug() << "Opening the filter engine"; + result = FwpmEngineOpen0(NULL, RPC_C_AUTHN_WINNT, NULL, &session, &wfp); + if (result != ERROR_SUCCESS) { + logger.error() << "FwpmEngineOpen0 failed. Return value:.\n" << result; + return false; + } + auto cleanup = qScopeGuard([&] { FwpmEngineClose0(wfp); }); + + // Check if the Layer Already Exists + FWPM_SUBLAYER0* maybeLayer; + result = FwpmSubLayerGetByKey0(wfp, &ST_FW_WINFW_BASELINE_SUBLAYER_KEY, + &maybeLayer); + if (result == ERROR_SUCCESS) { + logger.debug() << "The Sublayer Already Exists!"; + FwpmFreeMemory0((void**)&maybeLayer); + return true; + } + + // Step 1: Start Transaction + result = FwpmTransactionBegin(wfp, NULL); + if (result != ERROR_SUCCESS) { + logger.error() << "FwpmTransactionBegin0 failed. Return value:.\n" + << result; + return false; + } + + // Step 3: Add Sublayer + FWPM_SUBLAYER0 subLayer; + memset(&subLayer, 0, sizeof(subLayer)); + subLayer.subLayerKey = ST_FW_WINFW_BASELINE_SUBLAYER_KEY; + subLayer.displayData.name = (PWSTR)L"Amnezia-SplitTunnel-Sublayer"; + subLayer.displayData.description = + (PWSTR)L"Filters that enforce a good baseline"; + subLayer.weight = 0xFFFF; + + result = FwpmSubLayerAdd0(wfp, &subLayer, NULL); + if (result != ERROR_SUCCESS) { + logger.error() << "FwpmSubLayerAdd0 failed. Return value:.\n" << result; + return false; + } + // Step 4: Commit! + result = FwpmTransactionCommit0(wfp); + if (result != ERROR_SUCCESS) { + logger.error() << "FwpmTransactionCommit0 failed. Return value:.\n" + << result; + return false; + } + logger.debug() << "Initialised Sublayer"; + m_init = true; + return true; +} + +bool WindowsFirewall::enableKillSwitch(int vpnAdapterIndex) { +// Checks if the FW_Rule was enabled succesfully, +// disables the whole killswitch and returns false if not. +#define FW_OK(rule) \ + { \ + auto result = FwpmTransactionBegin(m_sessionHandle, NULL); \ + if (result != ERROR_SUCCESS) { \ + disableKillSwitch(); \ + return false; \ + } \ + if (!rule) { \ + FwpmTransactionAbort0(m_sessionHandle); \ + disableKillSwitch(); \ + return false; \ + } \ + result = FwpmTransactionCommit0(m_sessionHandle); \ + if (result != ERROR_SUCCESS) { \ + logger.error() << "FwpmTransactionCommit0 failed. Return value:.\n" \ + << result; \ + return false; \ + } \ + } + + logger.info() << "Enabling Killswitch Using Adapter:" << vpnAdapterIndex; + FW_OK(allowTrafficOfAdapter(vpnAdapterIndex, MED_WEIGHT, + "Allow usage of VPN Adapter")); + FW_OK(allowDHCPTraffic(MED_WEIGHT, "Allow DHCP Traffic")); + FW_OK(allowHyperVTraffic(MED_WEIGHT, "Allow Hyper-V Traffic")); + FW_OK(allowTrafficForAppOnAll(getCurrentPath(), MAX_WEIGHT, + "Allow all for AmneziaVPN.exe")); + FW_OK(blockTrafficOnPort(53, MED_WEIGHT, "Block all DNS")); + FW_OK( + allowLoopbackTraffic(MED_WEIGHT, "Allow Loopback traffic on device %1")); + + logger.debug() << "Killswitch on! Rules:" << m_activeRules.length(); + return true; +#undef FW_OK +} + +bool WindowsFirewall::enablePeerTraffic(const InterfaceConfig& config) { + // Start the firewall transaction + auto result = FwpmTransactionBegin(m_sessionHandle, NULL); + if (result != ERROR_SUCCESS) { + disableKillSwitch(); + return false; + } + auto cleanup = qScopeGuard([&] { + FwpmTransactionAbort0(m_sessionHandle); + disableKillSwitch(); + }); + + // Build the firewall rules for this peer. + logger.info() << "Enabling traffic for peer" + << config.m_serverPublicKey; + if (!blockTrafficTo(config.m_allowedIPAddressRanges, LOW_WEIGHT, + "Block Internet", config.m_serverPublicKey)) { + return false; + } + if (!config.m_dnsServer.isEmpty()) { + if (!allowTrafficTo(QHostAddress(config.m_dnsServer), 53, HIGH_WEIGHT, + "Allow DNS-Server", config.m_serverPublicKey)) { + return false; + } + // In some cases, we might configure a 2nd DNS server for IPv6, however + // this should probably be cleaned up by converting m_dnsServer into + // a QStringList instead. + if (config.m_dnsServer == config.m_serverIpv4Gateway) { + if (!allowTrafficTo(QHostAddress(config.m_serverIpv6Gateway), 53, + HIGH_WEIGHT, "Allow extra IPv6 DNS-Server", + config.m_serverPublicKey)) { + return false; + } + } + } + + result = FwpmTransactionCommit0(m_sessionHandle); + if (result != ERROR_SUCCESS) { + logger.error() << "FwpmTransactionCommit0 failed with error:" << result; + return false; + } + + cleanup.dismiss(); + return true; +} + +bool WindowsFirewall::disablePeerTraffic(const QString& pubkey) { + auto result = FwpmTransactionBegin(m_sessionHandle, NULL); + auto cleanup = qScopeGuard([&] { + if (result != ERROR_SUCCESS) { + FwpmTransactionAbort0(m_sessionHandle); + } + }); + if (result != ERROR_SUCCESS) { + logger.error() << "FwpmTransactionBegin0 failed. Return value:.\n" + << result; + return false; + } + + logger.info() << "Disabling traffic for peer" << pubkey; + for (const auto& filterID : m_peerRules.values(pubkey)) { + FwpmFilterDeleteById0(m_sessionHandle, filterID); + m_peerRules.remove(pubkey, filterID); + } + + // Commit! + result = FwpmTransactionCommit0(m_sessionHandle); + if (result != ERROR_SUCCESS) { + logger.error() << "FwpmTransactionCommit0 failed. Return value:.\n" + << result; + return false; + } + return true; +} + +bool WindowsFirewall::disableKillSwitch() { + auto result = FwpmTransactionBegin(m_sessionHandle, NULL); + auto cleanup = qScopeGuard([&] { + if (result != ERROR_SUCCESS) { + FwpmTransactionAbort0(m_sessionHandle); + } + }); + if (result != ERROR_SUCCESS) { + logger.error() << "FwpmTransactionBegin0 failed. Return value:.\n" + << result; + return false; + } + + for (const auto& filterID : m_peerRules.values()) { + FwpmFilterDeleteById0(m_sessionHandle, filterID); + } + + for (const auto& filterID : qAsConst(m_activeRules)) { + FwpmFilterDeleteById0(m_sessionHandle, filterID); + } + + // Commit! + result = FwpmTransactionCommit0(m_sessionHandle); + if (result != ERROR_SUCCESS) { + logger.error() << "FwpmTransactionCommit0 failed. Return value:.\n" + << result; + return false; + } + m_peerRules.clear(); + m_activeRules.clear(); + logger.debug() << "Firewall Disabled!"; + return true; +} + +bool WindowsFirewall::allowTrafficForAppOnAll(const QString& exePath, + int weight, + const QString& title) { + DWORD result = ERROR_SUCCESS; + Q_ASSERT(weight <= 15); + + // Get the AppID for the Executable; + QString appName = QFileInfo(exePath).baseName(); + std::wstring wstr = exePath.toStdWString(); + PCWSTR appPath = wstr.c_str(); + FWP_BYTE_BLOB* appID = NULL; + result = FwpmGetAppIdFromFileName0(appPath, &appID); + if (result != ERROR_SUCCESS) { + WindowsUtils::windowsLog("FwpmGetAppIdFromFileName0 failure"); + return false; + } + // Condition: Request must come from the .exe + FWPM_FILTER_CONDITION0 conds; + conds.fieldKey = FWPM_CONDITION_ALE_APP_ID; + conds.matchType = FWP_MATCH_EQUAL; + conds.conditionValue.type = FWP_BYTE_BLOB_TYPE; + conds.conditionValue.byteBlob = appID; + + // Assemble the Filter base + FWPM_FILTER0 filter; + memset(&filter, 0, sizeof(filter)); + filter.filterCondition = &conds; + filter.numFilterConditions = 1; + filter.action.type = FWP_ACTION_PERMIT; + filter.weight.type = FWP_UINT8; + filter.weight.uint8 = weight; + filter.subLayerKey = ST_FW_WINFW_BASELINE_SUBLAYER_KEY; + filter.flags = FWPM_FILTER_FLAG_CLEAR_ACTION_RIGHT; // Make this decision + // only blockable by veto + // Build and add the Filters + // #1 Permit outbound IPv4 traffic. + { + QString desc("Permit (out) IPv4 Traffic of: " + appName); + filter.layerKey = FWPM_LAYER_ALE_AUTH_CONNECT_V4; + if (!enableFilter(&filter, title, desc)) { + return false; + } + } + // #2 Permit inbound IPv4 traffic. + { + QString desc("Permit (in) IPv4 Traffic of: " + appName); + filter.layerKey = FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4; + if (!enableFilter(&filter, title, desc)) { + return false; + } + } + return true; +} + +bool WindowsFirewall::allowTrafficOfAdapter(int networkAdapter, uint8_t weight, + const QString& title) { + FWPM_FILTER_CONDITION0 conds; + // Condition: Request must be targeting the TUN interface + conds.fieldKey = FWPM_CONDITION_INTERFACE_INDEX; + conds.matchType = FWP_MATCH_EQUAL; + conds.conditionValue.type = FWP_UINT32; + conds.conditionValue.uint32 = networkAdapter; + + // Assemble the Filter base + FWPM_FILTER0 filter; + memset(&filter, 0, sizeof(filter)); + filter.filterCondition = &conds; + filter.numFilterConditions = 1; + filter.action.type = FWP_ACTION_PERMIT; + filter.weight.type = FWP_UINT8; + filter.weight.uint8 = weight; + filter.subLayerKey = ST_FW_WINFW_BASELINE_SUBLAYER_KEY; + + QString description("Allow %0 traffic on Adapter %1"); + // #1 Permit outbound IPv4 traffic. + filter.layerKey = FWPM_LAYER_ALE_AUTH_CONNECT_V4; + if (!enableFilter(&filter, title, + description.arg("out").arg(networkAdapter))) { + return false; + } + // #2 Permit inbound IPv4 traffic. + filter.layerKey = FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4; + if (!enableFilter(&filter, title, + description.arg("in").arg(networkAdapter))) { + return false; + } + // #3 Permit outbound IPv6 traffic. + filter.layerKey = FWPM_LAYER_ALE_AUTH_CONNECT_V6; + if (!enableFilter(&filter, title, + description.arg("out").arg(networkAdapter))) { + return false; + } + // #4 Permit inbound IPv6 traffic. + filter.layerKey = FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V6; + if (!enableFilter(&filter, title, + description.arg("in").arg(networkAdapter))) { + return false; + } + return true; +} + +bool WindowsFirewall::allowTrafficTo(const QHostAddress& targetIP, uint port, + int weight, const QString& title, + const QString& peer) { + bool isIPv4 = targetIP.protocol() == QAbstractSocket::IPv4Protocol; + GUID layerOut = + isIPv4 ? FWPM_LAYER_ALE_AUTH_CONNECT_V4 : FWPM_LAYER_ALE_AUTH_CONNECT_V6; + GUID layerIn = isIPv4 ? FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4 + : FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V6; + + quint32_be ipBigEndian; + quint32 ip = targetIP.toIPv4Address(); + qToBigEndian(ip, &ipBigEndian); + + // Allow Traffic to IP with PORT using any protocol + FWPM_FILTER_CONDITION0 conds[4]; + conds[0].fieldKey = FWPM_CONDITION_IP_PROTOCOL; + conds[0].matchType = FWP_MATCH_EQUAL; + conds[0].conditionValue.type = FWP_UINT8; + conds[0].conditionValue.uint8 = (IPPROTO_UDP); + + conds[1].fieldKey = FWPM_CONDITION_IP_PROTOCOL; + conds[1].matchType = FWP_MATCH_EQUAL; + conds[1].conditionValue.type = FWP_UINT8; + conds[1].conditionValue.uint16 = (IPPROTO_TCP); + + conds[2].fieldKey = FWPM_CONDITION_IP_REMOTE_PORT; + conds[2].matchType = FWP_MATCH_EQUAL; + conds[2].conditionValue.type = FWP_UINT16; + conds[2].conditionValue.uint16 = port; + + conds[3].fieldKey = FWPM_CONDITION_IP_REMOTE_ADDRESS; + conds[3].matchType = FWP_MATCH_EQUAL; + QByteArray buffer; + // will hold v6 Addess bytes if present + importAddress(targetIP, conds[3].conditionValue, &buffer); + + // Assemble the Filter base + FWPM_FILTER0 filter; + memset(&filter, 0, sizeof(filter)); + filter.filterCondition = conds; + filter.numFilterConditions = 4; + filter.action.type = FWP_ACTION_PERMIT; + filter.weight.type = FWP_UINT8; + filter.weight.uint8 = weight; + filter.subLayerKey = ST_FW_WINFW_BASELINE_SUBLAYER_KEY; + filter.flags = FWPM_FILTER_FLAG_CLEAR_ACTION_RIGHT; // Hard Permit! + + QString description("Permit traffic %1 %2 on port %3"); + filter.layerKey = layerOut; + if (!enableFilter(&filter, title, + description.arg("to").arg(targetIP.toString()).arg(port), + peer)) { + return false; + } + filter.layerKey = layerIn; + if (!enableFilter(&filter, title, + description.arg("from").arg(targetIP.toString()).arg(port), + peer)) { + return false; + } + return true; +} + +bool WindowsFirewall::allowDHCPTraffic(uint8_t weight, const QString& title) { + // Allow outbound DHCPv4 + { + FWPM_FILTER_CONDITION0 conds[4]; + // Condition: Request must be targeting the TUN interface + conds[0].fieldKey = FWPM_CONDITION_IP_PROTOCOL; + conds[0].matchType = FWP_MATCH_EQUAL; + conds[0].conditionValue.type = FWP_UINT8; + conds[0].conditionValue.uint8 = (IPPROTO_UDP); + + conds[1].fieldKey = FWPM_CONDITION_IP_LOCAL_PORT; + conds[1].matchType = FWP_MATCH_EQUAL; + conds[1].conditionValue.type = FWP_UINT16; + conds[1].conditionValue.uint16 = (68); + + conds[2].fieldKey = FWPM_CONDITION_IP_REMOTE_PORT; + conds[2].matchType = FWP_MATCH_EQUAL; + conds[2].conditionValue.type = FWP_UINT16; + conds[2].conditionValue.uint16 = 67; + + conds[3].fieldKey = FWPM_CONDITION_IP_REMOTE_ADDRESS; + conds[3].matchType = FWP_MATCH_EQUAL; + conds[3].conditionValue.type = FWP_UINT32; + conds[3].conditionValue.uint32 = (0xffffffff); + + // Assemble the Filter base + FWPM_FILTER0 filter; + memset(&filter, 0, sizeof(filter)); + filter.filterCondition = conds; + filter.numFilterConditions = 4; + filter.action.type = FWP_ACTION_PERMIT; + filter.weight.type = FWP_UINT8; + filter.weight.uint8 = weight; + filter.subLayerKey = ST_FW_WINFW_BASELINE_SUBLAYER_KEY; + + filter.layerKey = FWPM_LAYER_ALE_AUTH_CONNECT_V4; + + if (!enableFilter(&filter, title, "Allow Outbound DHCP")) { + return false; + } + } + // Allow inbound DHCPv4 + { + FWPM_FILTER_CONDITION0 conds[3]; + conds[0].fieldKey = FWPM_CONDITION_IP_PROTOCOL; + conds[0].matchType = FWP_MATCH_EQUAL; + conds[0].conditionValue.type = FWP_UINT8; + conds[0].conditionValue.uint8 = (IPPROTO_UDP); + + conds[1].fieldKey = FWPM_CONDITION_IP_LOCAL_PORT; + conds[1].matchType = FWP_MATCH_EQUAL; + conds[1].conditionValue.type = FWP_UINT16; + conds[1].conditionValue.uint16 = (68); + + conds[2].fieldKey = FWPM_CONDITION_IP_REMOTE_PORT; + conds[2].matchType = FWP_MATCH_EQUAL; + conds[2].conditionValue.type = FWP_UINT16; + conds[2].conditionValue.uint16 = 67; + + // Assemble the Filter base + FWPM_FILTER0 filter; + memset(&filter, 0, sizeof(filter)); + filter.filterCondition = conds; + filter.numFilterConditions = 3; + filter.action.type = FWP_ACTION_PERMIT; + filter.weight.type = FWP_UINT8; + filter.weight.uint8 = weight; + filter.subLayerKey = ST_FW_WINFW_BASELINE_SUBLAYER_KEY; + filter.layerKey = FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4; + + if (!enableFilter(&filter, title, "Allow inbound DHCP")) { + return false; + } + } + + // Allow outbound DHCPv6 + { + FWPM_FILTER_CONDITION0 conds[3]; + // Condition: Request must be targeting the TUN interface + conds[0].fieldKey = FWPM_CONDITION_IP_PROTOCOL; + conds[0].matchType = FWP_MATCH_EQUAL; + conds[0].conditionValue.type = FWP_UINT8; + conds[0].conditionValue.uint8 = (IPPROTO_UDP); + + conds[1].fieldKey = FWPM_CONDITION_IP_LOCAL_PORT; + conds[1].matchType = FWP_MATCH_EQUAL; + conds[1].conditionValue.type = FWP_UINT16; + conds[1].conditionValue.uint16 = (68); + + conds[2].fieldKey = FWPM_CONDITION_IP_REMOTE_PORT; + conds[2].matchType = FWP_MATCH_EQUAL; + conds[2].conditionValue.type = FWP_UINT16; + conds[2].conditionValue.uint16 = 67; + + // Assemble the Filter base + FWPM_FILTER0 filter; + memset(&filter, 0, sizeof(filter)); + filter.filterCondition = conds; + filter.numFilterConditions = 3; + filter.action.type = FWP_ACTION_PERMIT; + filter.weight.type = FWP_UINT8; + filter.weight.uint8 = weight; + filter.subLayerKey = ST_FW_WINFW_BASELINE_SUBLAYER_KEY; + filter.layerKey = FWPM_LAYER_ALE_AUTH_CONNECT_V6; + + if (!enableFilter(&filter, title, "Allow outbound DHCPv6")) { + return false; + } + } + + // Allow inbound DHCPv6 + { + FWPM_FILTER_CONDITION0 conds[3]; + conds[0].fieldKey = FWPM_CONDITION_IP_PROTOCOL; + conds[0].matchType = FWP_MATCH_EQUAL; + conds[0].conditionValue.type = FWP_UINT8; + conds[0].conditionValue.uint8 = (IPPROTO_UDP); + + conds[1].fieldKey = FWPM_CONDITION_IP_LOCAL_PORT; + conds[1].matchType = FWP_MATCH_EQUAL; + conds[1].conditionValue.type = FWP_UINT16; + conds[1].conditionValue.uint16 = (68); + + conds[2].fieldKey = FWPM_CONDITION_IP_REMOTE_PORT; + conds[2].matchType = FWP_MATCH_EQUAL; + conds[2].conditionValue.type = FWP_UINT16; + conds[2].conditionValue.uint16 = 67; + + // Assemble the Filter base + FWPM_FILTER0 filter; + memset(&filter, 0, sizeof(filter)); + filter.filterCondition = conds; + filter.numFilterConditions = 3; + filter.action.type = FWP_ACTION_PERMIT; + filter.weight.type = FWP_UINT8; + filter.weight.uint8 = weight; + filter.subLayerKey = ST_FW_WINFW_BASELINE_SUBLAYER_KEY; + filter.layerKey = FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V6; + if (!enableFilter(&filter, title, "Allow inbound DHCPv6")) { + return false; + } + } + return true; +} + +// Allows the internal Hyper-V Switches to work. +bool WindowsFirewall::allowHyperVTraffic(uint8_t weight, const QString& title) { + FWPM_FILTER_CONDITION0 cond; + // Condition: Request must be targeting the TUN interface + cond.fieldKey = FWPM_CONDITION_L2_FLAGS; + cond.matchType = FWP_MATCH_EQUAL; + cond.conditionValue.type = FWP_UINT32; + cond.conditionValue.uint32 = FWP_CONDITION_L2_IS_VM2VM; + + // Assemble the Filter base + FWPM_FILTER0 filter; + memset(&filter, 0, sizeof(filter)); + filter.filterCondition = &cond; + filter.numFilterConditions = 1; + filter.action.type = FWP_ACTION_PERMIT; + filter.weight.type = FWP_UINT8; + filter.weight.uint8 = weight; + filter.subLayerKey = ST_FW_WINFW_BASELINE_SUBLAYER_KEY; + + // #1 Permit Hyper-V => Hyper-V outbound. + filter.layerKey = FWPM_LAYER_OUTBOUND_MAC_FRAME_NATIVE; + if (!enableFilter(&filter, title, "Permit Hyper-V => Hyper-V outbound")) { + return false; + } + // #2 Permit Hyper-V => Hyper-V inbound. + filter.layerKey = FWPM_LAYER_INBOUND_MAC_FRAME_NATIVE; + if (!enableFilter(&filter, title, "Permit Hyper-V => Hyper-V inbound")) { + return false; + } + return true; +} + +bool WindowsFirewall::blockTrafficTo(const IPAddress& addr, uint8_t weight, + const QString& title, + const QString& peer) { + QString description("Block traffic %1 %2 "); + + auto lower = addr.address(); + auto upper = addr.broadcastAddress(); + + const bool isV4 = addr.type() == QAbstractSocket::IPv4Protocol; + const GUID layerKeyOut = + isV4 ? FWPM_LAYER_ALE_AUTH_CONNECT_V4 : FWPM_LAYER_ALE_AUTH_CONNECT_V6; + const GUID layerKeyIn = isV4 ? FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4 + : FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V6; + + // Assemble the Filter base + FWPM_FILTER0 filter; + memset(&filter, 0, sizeof(filter)); + filter.action.type = FWP_ACTION_BLOCK; + filter.weight.type = FWP_UINT8; + filter.weight.uint8 = weight; + filter.subLayerKey = ST_FW_WINFW_BASELINE_SUBLAYER_KEY; + + FWPM_FILTER_CONDITION0 cond[1] = {0}; + FWP_RANGE0 ipRange; + QByteArray lowIpV6Buffer; + QByteArray highIpV6Buffer; + + importAddress(lower, ipRange.valueLow, &lowIpV6Buffer); + importAddress(upper, ipRange.valueHigh, &highIpV6Buffer); + + cond[0].fieldKey = FWPM_CONDITION_IP_REMOTE_ADDRESS; + cond[0].matchType = FWP_MATCH_RANGE; + cond[0].conditionValue.type = FWP_RANGE_TYPE; + cond[0].conditionValue.rangeValue = &ipRange; + + filter.numFilterConditions = 1; + filter.filterCondition = cond; + + filter.layerKey = layerKeyOut; + if (!enableFilter(&filter, title, description.arg("to").arg(addr.toString()), + peer)) { + return false; + } + filter.layerKey = layerKeyIn; + if (!enableFilter(&filter, title, + description.arg("from").arg(addr.toString()), peer)) { + return false; + } + return true; +} + +bool WindowsFirewall::blockTrafficTo(const QList& rangeList, + uint8_t weight, const QString& title, + const QString& peer) { + for (auto range : rangeList) { + if (!blockTrafficTo(range, weight, title, peer)) { + logger.info() << "Setting Range of" << range.toString() << "failed"; + return false; + } + } + return true; +} + +// Returns the Path of the Current Executable this runs in +QString WindowsFirewall::getCurrentPath() { + const unsigned char initValue = 0xff; + QByteArray buffer(2048, initValue); + auto ok = GetModuleFileNameA(NULL, buffer.data(), buffer.size()); + + if (ok == ERROR_INSUFFICIENT_BUFFER) { + buffer.resize(buffer.size() * 2); + ok = GetModuleFileNameA(NULL, buffer.data(), buffer.size()); + } + if (ok == 0) { + WindowsUtils::windowsLog("Err fetching dos path"); + return ""; + } + + return QString::fromLocal8Bit(buffer); +} + +void WindowsFirewall::importAddress(const QHostAddress& addr, + OUT FWP_VALUE0_& value, + OUT QByteArray* v6DataBuffer) { + const bool isV4 = addr.protocol() == QAbstractSocket::IPv4Protocol; + if (isV4) { + value.type = FWP_UINT32; + value.uint32 = addr.toIPv4Address(); + return; + } + auto v6bytes = addr.toIPv6Address(); + v6DataBuffer->append((const char*)v6bytes.c, IPV6_ADDRESS_SIZE); + value.type = FWP_BYTE_ARRAY16_TYPE; + value.byteArray16 = (FWP_BYTE_ARRAY16*)v6DataBuffer->data(); +} +void WindowsFirewall::importAddress(const QHostAddress& addr, + OUT FWP_CONDITION_VALUE0_& value, + OUT QByteArray* v6DataBuffer) { + const bool isV4 = addr.protocol() == QAbstractSocket::IPv4Protocol; + if (isV4) { + value.type = FWP_UINT32; + value.uint32 = addr.toIPv4Address(); + return; + } + auto v6bytes = addr.toIPv6Address(); + v6DataBuffer->append((const char*)v6bytes.c, IPV6_ADDRESS_SIZE); + value.type = FWP_BYTE_ARRAY16_TYPE; + value.byteArray16 = (FWP_BYTE_ARRAY16*)v6DataBuffer->data(); +} + +bool WindowsFirewall::blockTrafficOnPort(uint port, uint8_t weight, + const QString& title) { + // Allow Traffic to IP with PORT using any protocol + FWPM_FILTER_CONDITION0 conds[3]; + conds[0].fieldKey = FWPM_CONDITION_IP_PROTOCOL; + conds[0].matchType = FWP_MATCH_EQUAL; + conds[0].conditionValue.type = FWP_UINT8; + conds[0].conditionValue.uint8 = (IPPROTO_UDP); + + conds[1].fieldKey = FWPM_CONDITION_IP_PROTOCOL; + conds[1].matchType = FWP_MATCH_EQUAL; + conds[1].conditionValue.type = FWP_UINT8; + conds[1].conditionValue.uint8 = (IPPROTO_TCP); + + conds[2].fieldKey = FWPM_CONDITION_IP_REMOTE_PORT; + conds[2].matchType = FWP_MATCH_EQUAL; + conds[2].conditionValue.type = FWP_UINT16; + conds[2].conditionValue.uint16 = port; + + // Assemble the Filter base + FWPM_FILTER0 filter; + memset(&filter, 0, sizeof(filter)); + filter.filterCondition = conds; + filter.numFilterConditions = 3; + filter.action.type = FWP_ACTION_BLOCK; + filter.weight.type = FWP_UINT8; + filter.weight.uint8 = weight; + filter.subLayerKey = ST_FW_WINFW_BASELINE_SUBLAYER_KEY; + + QString description("Block %1 on Port %2"); + filter.layerKey = FWPM_LAYER_ALE_AUTH_CONNECT_V6; + if (!enableFilter(&filter, title, description.arg("outgoing v6").arg(port))) { + return false; + } + filter.layerKey = FWPM_LAYER_ALE_AUTH_CONNECT_V4; + if (!enableFilter(&filter, title, description.arg("outgoing v4").arg(port))) { + return false; + } + + filter.layerKey = FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4; + if (!enableFilter(&filter, title, description.arg("incoming v4").arg(port))) { + return false; + } + filter.layerKey = FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V6; + if (!enableFilter(&filter, title, description.arg("incoming v6").arg(port))) { + return false; + } + return true; +} + +bool WindowsFirewall::enableFilter(FWPM_FILTER0* filter, const QString& title, + const QString& description, + const QString& peer) { + uint64_t filterID = 0; + auto name = title.toStdWString(); + auto desc = description.toStdWString(); + filter->displayData.name = (PWSTR)name.c_str(); + filter->displayData.description = (PWSTR)desc.c_str(); + auto result = FwpmFilterAdd0(m_sessionHandle, filter, NULL, &filterID); + if (result != ERROR_SUCCESS) { + logger.error() << "Failed to enable filter: " << title << " " + << description; + return false; + } + logger.info() << "Filter added: " << title << ":" << description; + if (peer.isEmpty()) { + m_activeRules.append(filterID); + } else { + m_peerRules.insert(peer, filterID); + } + return true; +} + +bool WindowsFirewall::allowLoopbackTraffic(uint8_t weight, + const QString& title) { + QList networkInterfaces = + QNetworkInterface::allInterfaces(); + for (const auto& iface : networkInterfaces) { + if (iface.type() != QNetworkInterface::Loopback) { + continue; + } + if (!allowTrafficOfAdapter(iface.index(), weight, + title.arg(iface.name()))) { + return false; + } + } + return true; +} diff --git a/client/platforms/windows/daemon/windowsfirewall.h b/client/platforms/windows/daemon/windowsfirewall.h new file mode 100644 index 00000000..e1891322 --- /dev/null +++ b/client/platforms/windows/daemon/windowsfirewall.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 WINDOWSFIREWALL_H +#define WINDOWSFIREWALL_H + +#pragma comment(lib, "Fwpuclnt") + +// Note: The windows.h import needs to come before the fwpmu.h import. +// clang-format off +#include +#include +// clang-format on + +#include +#include +#include +#include + +#include "../client/daemon/interfaceconfig.h" + +class IpAdressRange; +struct FWP_VALUE0_; +struct FWP_CONDITION_VALUE0_; + +class WindowsFirewall final : public QObject { + public: + ~WindowsFirewall(); + + static WindowsFirewall* instance(); + bool init(); + + bool enableKillSwitch(int vpnAdapterIndex); + bool enablePeerTraffic(const InterfaceConfig& config); + bool disablePeerTraffic(const QString& pubkey); + bool disableKillSwitch(); + + private: + WindowsFirewall(QObject* parent); + HANDLE m_sessionHandle; + bool m_init = false; + QList m_activeRules; + QMultiMap m_peerRules; + + bool allowTrafficForAppOnAll(const QString& exePath, int weight, + const QString& title); + bool blockTrafficTo(const QList& range, uint8_t weight, + const QString& title, const QString& peer = QString()); + bool blockTrafficTo(const IPAddress& addr, uint8_t weight, + const QString& title, const QString& peer = QString()); + bool blockTrafficOnPort(uint port, uint8_t weight, const QString& title); + bool allowTrafficTo(const QHostAddress& targetIP, uint port, int weight, + const QString& title, const QString& peer = QString()); + bool allowTrafficOfAdapter(int networkAdapter, uint8_t weight, + const QString& title); + bool allowDHCPTraffic(uint8_t weight, const QString& title); + bool allowHyperVTraffic(uint8_t weight, const QString& title); + bool allowLoopbackTraffic(uint8_t weight, const QString& title); + + // Utils + QString getCurrentPath(); + void importAddress(const QHostAddress& addr, OUT FWP_VALUE0_& value, + OUT QByteArray* v6DataBuffer); + void importAddress(const QHostAddress& addr, OUT FWP_CONDITION_VALUE0_& value, + OUT QByteArray* v6DataBuffer); + bool enableFilter(FWPM_FILTER0* filter, const QString& title, + const QString& description, + const QString& peer = QString()); +}; + +#endif // WINDOWSFIREWALL_H diff --git a/client/platforms/windows/daemon/windowsroutemonitor.cpp b/client/platforms/windows/daemon/windowsroutemonitor.cpp new file mode 100644 index 00000000..e60a9178 --- /dev/null +++ b/client/platforms/windows/daemon/windowsroutemonitor.cpp @@ -0,0 +1,317 @@ +/* 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 "windowsroutemonitor.h" + +#include + +#include "leakdetector.h" +#include "logger.h" + +namespace { +Logger logger("WindowsRouteMonitor"); +}; // namespace + +// Called by the kernel on route changes - perform some basic filtering and +// invoke the routeChanged slot to do the real work. +static void routeChangeCallback(PVOID context, PMIB_IPFORWARD_ROW2 row, + MIB_NOTIFICATION_TYPE type) { + WindowsRouteMonitor* monitor = (WindowsRouteMonitor*)context; + Q_UNUSED(type); + + // Ignore host route changes, and unsupported protocols. + if (row->DestinationPrefix.Prefix.si_family == AF_INET6) { + if (row->DestinationPrefix.PrefixLength >= 128) { + return; + } + } else if (row->DestinationPrefix.Prefix.si_family == AF_INET) { + if (row->DestinationPrefix.PrefixLength >= 32) { + return; + } + } else { + return; + } + + if (monitor->getLuid() != row->InterfaceLuid.Value) { + QMetaObject::invokeMethod(monitor, "routeChanged", Qt::QueuedConnection); + } +} + +// Perform prefix matching comparison on IP addresses in host order. +static int prefixcmp(const void* a, const void* b, size_t bits) { + size_t bytes = bits / 8; + if (bytes > 0) { + int diff = memcmp(a, b, bytes); + if (diff != 0) { + return diff; + } + } + + if (bits % 8) { + quint8 avalue = *((const quint8*)a + bytes) >> (8 - bits % 8); + quint8 bvalue = *((const quint8*)b + bytes) >> (8 - bits % 8); + return avalue - bvalue; + } + + return 0; +} + +WindowsRouteMonitor::WindowsRouteMonitor(QObject* parent) : QObject(parent) { + MZ_COUNT_CTOR(WindowsRouteMonitor); + logger.debug() << "WindowsRouteMonitor created."; + + NotifyRouteChange2(AF_INET, routeChangeCallback, this, FALSE, &m_routeHandle); +} + +WindowsRouteMonitor::~WindowsRouteMonitor() { + MZ_COUNT_DTOR(WindowsRouteMonitor); + CancelMibChangeNotify2(m_routeHandle); + flushExclusionRoutes(); + logger.debug() << "WindowsRouteMonitor destroyed."; +} + +void WindowsRouteMonitor::updateValidInterfaces(int family) { + PMIB_IPINTERFACE_TABLE table; + DWORD result = GetIpInterfaceTable(family, &table); + if (result != NO_ERROR) { + logger.warning() << "Failed to retrive interface table." << result; + return; + } + auto guard = qScopeGuard([&] { FreeMibTable(table); }); + + // Flush the list of interfaces that are valid for routing. + if ((family == AF_INET) || (family == AF_UNSPEC)) { + m_validInterfacesIpv4.clear(); + } + if ((family == AF_INET6) || (family == AF_UNSPEC)) { + m_validInterfacesIpv6.clear(); + } + + // Rebuild the list of interfaces that are valid for routing. + for (ULONG i = 0; i < table->NumEntries; i++) { + MIB_IPINTERFACE_ROW* row = &table->Table[i]; + if (row->InterfaceLuid.Value == m_luid) { + continue; + } + if (!row->Connected) { + continue; + } + + if (row->Family == AF_INET) { + logger.debug() << "Interface" << row->InterfaceIndex + << "is valid for IPv4 routing"; + m_validInterfacesIpv4.append(row->InterfaceLuid.Value); + } + if (row->Family == AF_INET6) { + logger.debug() << "Interface" << row->InterfaceIndex + << "is valid for IPv6 routing"; + m_validInterfacesIpv6.append(row->InterfaceLuid.Value); + } + } +} + +void WindowsRouteMonitor::updateExclusionRoute(MIB_IPFORWARD_ROW2* data, + void* ptable) { + PMIB_IPFORWARD_TABLE2 table = reinterpret_cast(ptable); + SOCKADDR_INET nexthop = {0}; + quint64 bestLuid = 0; + int bestMatch = -1; + ULONG bestMetric = ULONG_MAX; + + nexthop.si_family = data->DestinationPrefix.Prefix.si_family; + for (ULONG i = 0; i < table->NumEntries; i++) { + MIB_IPFORWARD_ROW2* row = &table->Table[i]; + // Ignore routes into the VPN interface. + if (row->InterfaceLuid.Value == m_luid) { + continue; + } + // Ignore host routes, and shorter potential matches. + if (row->DestinationPrefix.PrefixLength >= + data->DestinationPrefix.PrefixLength) { + continue; + } + if (row->DestinationPrefix.PrefixLength < bestMatch) { + continue; + } + + // Check if the routing table entry matches the destination. + if (data->DestinationPrefix.Prefix.si_family == AF_INET6) { + if (row->DestinationPrefix.Prefix.Ipv6.sin6_family != AF_INET6) { + continue; + } + if (!m_validInterfacesIpv6.contains(row->InterfaceLuid.Value)) { + continue; + } + if (prefixcmp(&data->DestinationPrefix.Prefix.Ipv6.sin6_addr, + &row->DestinationPrefix.Prefix.Ipv6.sin6_addr, + row->DestinationPrefix.PrefixLength) != 0) { + continue; + } + } else if (data->DestinationPrefix.Prefix.si_family == AF_INET) { + if (row->DestinationPrefix.Prefix.Ipv4.sin_family != AF_INET) { + continue; + } + if (!m_validInterfacesIpv4.contains(row->InterfaceLuid.Value)) { + continue; + } + if (prefixcmp(&data->DestinationPrefix.Prefix.Ipv4.sin_addr, + &row->DestinationPrefix.Prefix.Ipv4.sin_addr, + row->DestinationPrefix.PrefixLength) != 0) { + continue; + } + } else { + // Unsupported destination address family. + continue; + } + + // Prefer routes with lower metric if we find multiple matches + // with the same prefix length. + if ((row->DestinationPrefix.PrefixLength == bestMatch) && + (row->Metric >= bestMetric)) { + continue; + } + + // If we got here, then this is the longest prefix match so far. + memcpy(&nexthop, &row->NextHop, sizeof(SOCKADDR_INET)); + bestLuid = row->InterfaceLuid.Value; + bestMatch = row->DestinationPrefix.PrefixLength; + bestMetric = row->Metric; + } + + // If neither the interface nor next-hop have changed, then do nothing. + if ((data->InterfaceLuid.Value) == bestLuid && + memcmp(&nexthop, &data->NextHop, sizeof(SOCKADDR_INET)) == 0) { + return; + } + + // Update the routing table entry. + if (data->InterfaceLuid.Value != 0) { + DWORD result = DeleteIpForwardEntry2(data); + if ((result != NO_ERROR) && (result != ERROR_NOT_FOUND)) { + logger.error() << "Failed to delete route:" << result; + } + } + data->InterfaceLuid.Value = bestLuid; + memcpy(&data->NextHop, &nexthop, sizeof(SOCKADDR_INET)); + if (data->InterfaceLuid.Value != 0) { + DWORD result = CreateIpForwardEntry2(data); + if (result != NO_ERROR) { + logger.error() << "Failed to update route:" << result; + } + } +} + +bool WindowsRouteMonitor::addExclusionRoute(const IPAddress& prefix) { + logger.debug() << "Adding exclusion route for" + << logger.sensitive(prefix.toString()); + + if (m_exclusionRoutes.contains(prefix)) { + logger.warning() << "Exclusion route already exists"; + return false; + } + + // Allocate and initialize the MIB routing table row. + MIB_IPFORWARD_ROW2* data = new MIB_IPFORWARD_ROW2; + InitializeIpForwardEntry(data); + if (prefix.address().protocol() == QAbstractSocket::IPv6Protocol) { + Q_IPV6ADDR buf = prefix.address().toIPv6Address(); + + memcpy(&data->DestinationPrefix.Prefix.Ipv6.sin6_addr, &buf, sizeof(buf)); + data->DestinationPrefix.Prefix.Ipv6.sin6_family = AF_INET6; + data->DestinationPrefix.PrefixLength = prefix.prefixLength(); + } else { + quint32 buf = prefix.address().toIPv4Address(); + + data->DestinationPrefix.Prefix.Ipv4.sin_addr.s_addr = htonl(buf); + data->DestinationPrefix.Prefix.Ipv4.sin_family = AF_INET; + data->DestinationPrefix.PrefixLength = prefix.prefixLength(); + } + data->NextHop.si_family = data->DestinationPrefix.Prefix.si_family; + + // Set the rest of the flags for a static route. + data->ValidLifetime = 0xffffffff; + data->PreferredLifetime = 0xffffffff; + data->Metric = 0; + data->Protocol = MIB_IPPROTO_NETMGMT; + data->Loopback = false; + data->AutoconfigureAddress = false; + data->Publish = false; + data->Immortal = false; + data->Age = 0; + + PMIB_IPFORWARD_TABLE2 table; + int family; + if (prefix.address().protocol() == QAbstractSocket::IPv6Protocol) { + family = AF_INET6; + } else { + family = AF_INET; + } + + DWORD result = GetIpForwardTable2(family, &table); + if (result != NO_ERROR) { + logger.error() << "Failed to fetch routing table:" << result; + delete data; + return false; + } + updateValidInterfaces(family); + updateExclusionRoute(data, table); + FreeMibTable(table); + + m_exclusionRoutes[prefix] = data; + return true; +} + +bool WindowsRouteMonitor::deleteExclusionRoute(const IPAddress& prefix) { + logger.debug() << "Deleting exclusion route for" + << logger.sensitive(prefix.address().toString()); + + for (;;) { + MIB_IPFORWARD_ROW2* data = m_exclusionRoutes.take(prefix); + if (data == nullptr) { + break; + } + + DWORD result = DeleteIpForwardEntry2(data); + if ((result != ERROR_NOT_FOUND) && (result != NO_ERROR)) { + logger.error() << "Failed to delete route to" + << logger.sensitive(prefix.toString()) + << "result:" << result; + } + delete data; + } + + return true; +} + +void WindowsRouteMonitor::flushExclusionRoutes() { + for (auto i = m_exclusionRoutes.begin(); i != m_exclusionRoutes.end(); i++) { + MIB_IPFORWARD_ROW2* data = i.value(); + DWORD result = DeleteIpForwardEntry2(data); + if ((result != ERROR_NOT_FOUND) && (result != NO_ERROR)) { + logger.error() << "Failed to delete route to" + << logger.sensitive(i.key().toString()) + << "result:" << result; + } + delete data; + } + m_exclusionRoutes.clear(); +} + +void WindowsRouteMonitor::routeChanged() { + logger.debug() << "Routes changed"; + + PMIB_IPFORWARD_TABLE2 table; + DWORD result = GetIpForwardTable2(AF_UNSPEC, &table); + if (result != NO_ERROR) { + logger.error() << "Failed to fetch routing table:" << result; + return; + } + + updateValidInterfaces(AF_UNSPEC); + for (MIB_IPFORWARD_ROW2* data : m_exclusionRoutes) { + updateExclusionRoute(data, table); + } + + FreeMibTable(table); +} diff --git a/client/platforms/windows/daemon/windowsroutemonitor.h b/client/platforms/windows/daemon/windowsroutemonitor.h new file mode 100644 index 00000000..0ae9a8a2 --- /dev/null +++ b/client/platforms/windows/daemon/windowsroutemonitor.h @@ -0,0 +1,47 @@ +/* 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 WINDOWSROUTEMONITOR_H +#define WINDOWSROUTEMONITOR_H + +#include +#include +#include +#include +#include + +#include + +#include "ipaddress.h" + +class WindowsRouteMonitor final : public QObject { + Q_OBJECT + + public: + WindowsRouteMonitor(QObject* parent); + ~WindowsRouteMonitor(); + + bool addExclusionRoute(const IPAddress& prefix); + bool deleteExclusionRoute(const IPAddress& prefix); + void flushExclusionRoutes(); + + void setLuid(quint64 luid) { m_luid = luid; } + quint64 getLuid() { return m_luid; } + + public slots: + void routeChanged(); + + private: + void updateExclusionRoute(MIB_IPFORWARD_ROW2* data, void* table); + void updateValidInterfaces(int family); + + QHash m_exclusionRoutes; + QList m_validInterfacesIpv4; + QList m_validInterfacesIpv6; + + quint64 m_luid = 0; + HANDLE m_routeHandle = INVALID_HANDLE_VALUE; +}; + +#endif /* WINDOWSROUTEMONITOR_H */ diff --git a/client/platforms/windows/daemon/windowssplittunnel.cpp b/client/platforms/windows/daemon/windowssplittunnel.cpp new file mode 100644 index 00000000..26d22ae8 --- /dev/null +++ b/client/platforms/windows/daemon/windowssplittunnel.cpp @@ -0,0 +1,547 @@ +/* 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 "windowssplittunnel.h" + +#include "../windowscommons.h" +#include "../windowsservicemanager.h" +#include "logger.h" +#include "platforms/windows/windowsutils.h" +#include "windowsfirewall.h" + +#define PSAPI_VERSION 2 +#include +#include + +#include +#include +#include +#include + +namespace { +Logger logger("WindowsSplitTunnel"); +} + +WindowsSplitTunnel::WindowsSplitTunnel(QObject* parent) : QObject(parent) { + if (detectConflict()) { + logger.error() << "Conflict detected, abort Split-Tunnel init."; + uninstallDriver(); + return; + } + if (!isInstalled()) { + logger.debug() << "Driver is not Installed, doing so"; + auto handle = installDriver(); + if (handle == INVALID_HANDLE_VALUE) { + WindowsUtils::windowsLog("Failed to install Driver"); + return; + } + logger.debug() << "Driver installed"; + CloseServiceHandle(handle); + } else { + logger.debug() << "Driver is installed"; + } + initDriver(); +} + +WindowsSplitTunnel::~WindowsSplitTunnel() { + CloseHandle(m_driver); + uninstallDriver(); +} + +void WindowsSplitTunnel::initDriver() { + if (detectConflict()) { + logger.error() << "Conflict detected, abort Split-Tunnel init."; + return; + } + logger.debug() << "Try to open Split Tunnel Driver"; + // Open the Driver Symlink + m_driver = CreateFileW(DRIVER_SYMLINK, GENERIC_READ | GENERIC_WRITE, 0, + nullptr, OPEN_EXISTING, 0, nullptr); + ; + + if (m_driver == INVALID_HANDLE_VALUE) { + WindowsUtils::windowsLog("Failed to open Driver: "); + + // If the handle is not present, try again after the serivce has started; + auto driver_manager = WindowsServiceManager(DRIVER_SERVICE_NAME); + QObject::connect(&driver_manager, &WindowsServiceManager::serviceStarted, + this, &WindowsSplitTunnel::initDriver); + driver_manager.startService(); + return; + } + + logger.debug() << "Connected to the Driver"; + // Reset Driver as it has wfp handles probably >:( + + if (!WindowsFirewall::instance()->init()) { + logger.error() << "Init WFP-Sublayer failed, driver won't be functional"; + return; + } + + // We need to now check the state and init it, if required + + auto state = getState(); + if (state == STATE_UNKNOWN) { + logger.debug() << "Cannot check if driver is initialized"; + } + if (state >= STATE_INITIALIZED) { + logger.debug() << "Driver already initialized: " << state; + reset(); + + auto newState = getState(); + logger.debug() << "New state after reset:" << newState; + if (newState >= STATE_INITIALIZED) { + logger.debug() << "Reset unsuccesfull"; + return; + } + } + + DWORD bytesReturned; + auto ok = DeviceIoControl(m_driver, IOCTL_INITIALIZE, nullptr, 0, nullptr, 0, + &bytesReturned, nullptr); + if (!ok) { + auto err = GetLastError(); + logger.error() << "Driver init failed err -" << err; + logger.error() << "State:" << getState(); + + return; + } + logger.debug() << "Driver initialized" << getState(); +} + +void WindowsSplitTunnel::setRules(const QStringList& appPaths) { + auto state = getState(); + if (state != STATE_READY && state != STATE_RUNNING) { + logger.warning() << "Driver is not in the right State to set Rules" + << state; + return; + } + + logger.debug() << "Pushing new Ruleset for Split-Tunnel " << state; + auto config = generateAppConfiguration(appPaths); + + DWORD bytesReturned; + auto ok = DeviceIoControl(m_driver, IOCTL_SET_CONFIGURATION, &config[0], + (DWORD)config.size(), nullptr, 0, &bytesReturned, + nullptr); + if (!ok) { + auto err = GetLastError(); + WindowsUtils::windowsLog("Set Config Failed:"); + logger.error() << "Failed to set Config err code " << err; + return; + } + logger.debug() << "New Configuration applied: " << getState(); +} + +void WindowsSplitTunnel::start(int inetAdapterIndex) { + // To Start we need to send 2 things: + // Network info (what is vpn what is network) + logger.debug() << "Starting SplitTunnel"; + DWORD bytesReturned; + + if (getState() == STATE_STARTED) { + logger.debug() << "Driver needs Init Call"; + DWORD bytesReturned; + auto ok = DeviceIoControl(m_driver, IOCTL_INITIALIZE, nullptr, 0, nullptr, + 0, &bytesReturned, nullptr); + if (!ok) { + logger.error() << "Driver init failed"; + return; + } + } + + // Process Info (what is running already) + if (getState() == STATE_INITIALIZED) { + logger.debug() << "State is Init, requires process config"; + auto config = generateProcessBlob(); + auto ok = DeviceIoControl(m_driver, IOCTL_REGISTER_PROCESSES, &config[0], + (DWORD)config.size(), nullptr, 0, &bytesReturned, + nullptr); + if (!ok) { + logger.error() << "Failed to set Process Config"; + return; + } + logger.debug() << "Set Process Config ok || new State:" << getState(); + } + + if (getState() == STATE_INITIALIZED) { + logger.warning() << "Driver is still not ready after process list send"; + return; + } + logger.debug() << "Driver is ready || new State:" << getState(); + + auto config = generateIPConfiguration(inetAdapterIndex); + auto ok = DeviceIoControl(m_driver, IOCTL_REGISTER_IP_ADDRESSES, &config[0], + (DWORD)config.size(), nullptr, 0, &bytesReturned, + nullptr); + if (!ok) { + logger.error() << "Failed to set Network Config"; + return; + } + logger.debug() << "New Network Config Applied || new State:" << getState(); +} + +void WindowsSplitTunnel::stop() { + DWORD bytesReturned; + auto ok = DeviceIoControl(m_driver, IOCTL_CLEAR_CONFIGURATION, nullptr, 0, + nullptr, 0, &bytesReturned, nullptr); + if (!ok) { + logger.error() << "Stopping Split tunnel not successfull"; + return; + } + logger.debug() << "Stopping Split tunnel successfull"; +} + +void WindowsSplitTunnel::reset() { + DWORD bytesReturned; + auto ok = DeviceIoControl(m_driver, IOCTL_ST_RESET, nullptr, 0, nullptr, 0, + &bytesReturned, nullptr); + if (!ok) { + logger.error() << "Reset Split tunnel not successfull"; + return; + } + logger.debug() << "Reset Split tunnel successfull"; +} + +DRIVER_STATE WindowsSplitTunnel::getState() { + if (m_driver == INVALID_HANDLE_VALUE) { + logger.debug() << "Can't query State from non Opened Driver"; + return STATE_UNKNOWN; + } + DWORD bytesReturned; + SIZE_T outBuffer; + bool ok = DeviceIoControl(m_driver, IOCTL_GET_STATE, nullptr, 0, &outBuffer, + sizeof(outBuffer), &bytesReturned, nullptr); + if (!ok) { + WindowsUtils::windowsLog("getState response failure"); + return STATE_UNKNOWN; + } + if (bytesReturned == 0) { + WindowsUtils::windowsLog("getState response is empty"); + return STATE_UNKNOWN; + } + return static_cast(outBuffer); +} + +std::vector WindowsSplitTunnel::generateAppConfiguration( + const QStringList& appPaths) { + // Step 1: Calculate how much size the buffer will need + size_t cummulated_string_size = 0; + QStringList dosPaths; + for (auto const& path : appPaths) { + auto dosPath = convertPath(path); + dosPaths.append(dosPath); + cummulated_string_size += dosPath.toStdWString().size() * sizeof(wchar_t); + logger.debug() << dosPath; + } + size_t bufferSize = sizeof(CONFIGURATION_HEADER) + + (sizeof(CONFIGURATION_ENTRY) * appPaths.size()) + + cummulated_string_size; + std::vector outBuffer(bufferSize); + + auto header = (CONFIGURATION_HEADER*)&outBuffer[0]; + auto entry = (CONFIGURATION_ENTRY*)(header + 1); + + auto stringDest = &outBuffer[0] + sizeof(CONFIGURATION_HEADER) + + (sizeof(CONFIGURATION_ENTRY) * appPaths.size()); + + SIZE_T stringOffset = 0; + + for (const QString& path : dosPaths) { + auto wstr = path.toStdWString(); + auto cstr = wstr.c_str(); + auto stringLength = wstr.size() * sizeof(wchar_t); + + entry->ImageNameLength = (USHORT)stringLength; + entry->ImageNameOffset = stringOffset; + + memcpy(stringDest, cstr, stringLength); + + ++entry; + stringDest += stringLength; + stringOffset += stringLength; + } + + header->NumEntries = appPaths.length(); + header->TotalLength = bufferSize; + + return outBuffer; +} + +std::vector WindowsSplitTunnel::generateIPConfiguration( + int inetAdapterIndex) { + std::vector out(sizeof(IP_ADDRESSES_CONFIG)); + + auto config = reinterpret_cast(&out[0]); + + auto ifaces = QNetworkInterface::allInterfaces(); + // Always the VPN + getAddress(WindowsCommons::VPNAdapterIndex(), &config->TunnelIpv4, + &config->TunnelIpv6); + // 2nd best route + getAddress(inetAdapterIndex, &config->InternetIpv4, &config->InternetIpv6); + return out; +} +void WindowsSplitTunnel::getAddress(int adapterIndex, IN_ADDR* out_ipv4, + IN6_ADDR* out_ipv6) { + QNetworkInterface target = + QNetworkInterface::interfaceFromIndex(adapterIndex); + logger.debug() << "Getting adapter info for:" << target.humanReadableName(); + + // take the first v4/v6 Adress and convert to in_addr + for (auto address : target.addressEntries()) { + if (address.ip().protocol() == QAbstractSocket::IPv4Protocol) { + auto adrr = address.ip().toString(); + std::wstring wstr = adrr.toStdWString(); + logger.debug() << "IpV4" << logger.sensitive(adrr); + PCWSTR w_str_ip = wstr.c_str(); + auto ok = InetPtonW(AF_INET, w_str_ip, out_ipv4); + if (ok != 1) { + logger.debug() << "Ipv4 Conversation error" << WSAGetLastError(); + } + break; + } + } + for (auto address : target.addressEntries()) { + if (address.ip().protocol() == QAbstractSocket::IPv6Protocol) { + auto adrr = address.ip().toString(); + std::wstring wstr = adrr.toStdWString(); + logger.debug() << "IpV6" << logger.sensitive(adrr); + PCWSTR w_str_ip = wstr.c_str(); + auto ok = InetPtonW(AF_INET6, w_str_ip, out_ipv6); + if (ok != 1) { + logger.error() << "Ipv6 Conversation error" << WSAGetLastError(); + } + break; + } + } +} + +std::vector WindowsSplitTunnel::generateProcessBlob() { + // Get a Snapshot of all processes that are running: + HANDLE snapshot_handle = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (snapshot_handle == INVALID_HANDLE_VALUE) { + WindowsUtils::windowsLog("Creating Process snapshot failed"); + return std::vector(0); + } + auto cleanup = qScopeGuard([&] { CloseHandle(snapshot_handle); }); + // Load the First Entry, later iterate over all + PROCESSENTRY32W currentProcess; + currentProcess.dwSize = sizeof(PROCESSENTRY32W); + + if (FALSE == (Process32First(snapshot_handle, ¤tProcess))) { + WindowsUtils::windowsLog("Cant read first entry"); + } + + QMap processes; + + do { + auto process_handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, + currentProcess.th32ProcessID); + + if (process_handle == INVALID_HANDLE_VALUE) { + continue; + } + ProcessInfo info = getProcessInfo(process_handle, currentProcess); + processes.insert(info.ProcessId, info); + CloseHandle(process_handle); + + } while (FALSE != (Process32NextW(snapshot_handle, ¤tProcess))); + + auto process_list = processes.values(); + if (process_list.isEmpty()) { + logger.debug() << "Process Snapshot list was empty"; + return std::vector(0); + } + + logger.debug() << "Reading Processes NUM: " << process_list.size(); + // Determine the Size of the outBuffer: + size_t totalStringSize = 0; + + for (const auto& process : process_list) { + totalStringSize += (process.DevicePath.size() * sizeof(wchar_t)); + } + auto bufferSize = sizeof(PROCESS_DISCOVERY_HEADER) + + (sizeof(PROCESS_DISCOVERY_ENTRY) * processes.size()) + + totalStringSize; + + std::vector out(bufferSize); + + auto header = reinterpret_cast(&out[0]); + auto entry = reinterpret_cast(header + 1); + auto stringBuffer = reinterpret_cast(entry + processes.size()); + + SIZE_T currentStringOffset = 0; + + for (const auto& process : process_list) { + // Wierd DWORD -> Handle Pointer magic. + entry->ProcessId = (HANDLE)((size_t)process.ProcessId); + entry->ParentProcessId = (HANDLE)((size_t)process.ParentProcessId); + + if (process.DevicePath.empty()) { + entry->ImageNameOffset = 0; + entry->ImageNameLength = 0; + } else { + const auto imageNameLength = process.DevicePath.size() * sizeof(wchar_t); + + entry->ImageNameOffset = currentStringOffset; + entry->ImageNameLength = static_cast(imageNameLength); + + RtlCopyMemory(stringBuffer + currentStringOffset, &process.DevicePath[0], + imageNameLength); + + currentStringOffset += imageNameLength; + } + ++entry; + } + + header->NumEntries = processes.size(); + header->TotalLength = bufferSize; + + return out; +} + +void WindowsSplitTunnel::close() { + CloseHandle(m_driver); + m_driver = INVALID_HANDLE_VALUE; +} + +ProcessInfo WindowsSplitTunnel::getProcessInfo( + HANDLE process, const PROCESSENTRY32W& processMeta) { + ProcessInfo pi; + pi.ParentProcessId = processMeta.th32ParentProcessID; + pi.ProcessId = processMeta.th32ProcessID; + pi.CreationTime = {0, 0}; + pi.DevicePath = L""; + + FILETIME creationTime, null_time; + auto ok = GetProcessTimes(process, &creationTime, &null_time, &null_time, + &null_time); + if (ok) { + pi.CreationTime = creationTime; + } + wchar_t imagepath[MAX_PATH + 1]; + if (K32GetProcessImageFileNameW( + process, imagepath, sizeof(imagepath) / sizeof(*imagepath)) != 0) { + pi.DevicePath = imagepath; + } + return pi; +} + +// static +SC_HANDLE WindowsSplitTunnel::installDriver() { + LPCWSTR displayName = L"Amnezia Split Tunnel Service"; + QFileInfo driver(qApp->applicationDirPath() + "/" + DRIVER_FILENAME); + if (!driver.exists()) { + logger.error() << "Split Tunnel Driver File not found " + << driver.absoluteFilePath(); + return (SC_HANDLE)INVALID_HANDLE_VALUE; + } + auto path = driver.absolutePath() + "/" + DRIVER_FILENAME; + LPCWSTR binPath = (const wchar_t*)path.utf16(); + auto scm_rights = SC_MANAGER_ALL_ACCESS; + auto serviceManager = OpenSCManager(NULL, // local computer + NULL, // servicesActive database + scm_rights); + auto service = CreateService(serviceManager, DRIVER_SERVICE_NAME, displayName, + SERVICE_ALL_ACCESS, SERVICE_KERNEL_DRIVER, + SERVICE_DEMAND_START, SERVICE_ERROR_NORMAL, + binPath, nullptr, 0, nullptr, nullptr, nullptr); + CloseServiceHandle(serviceManager); + return service; +} +// static +bool WindowsSplitTunnel::uninstallDriver() { + auto scm_rights = SC_MANAGER_ALL_ACCESS; + auto serviceManager = OpenSCManager(NULL, // local computer + NULL, // servicesActive database + scm_rights); + + auto servicehandle = + OpenService(serviceManager, DRIVER_SERVICE_NAME, GENERIC_READ); + auto result = DeleteService(servicehandle); + if (result) { + logger.debug() << "Split Tunnel Driver Removed"; + } + return result; +} +// static +bool WindowsSplitTunnel::isInstalled() { + // Check if the Drivers I/O File is present + auto symlink = QFileInfo(QString::fromWCharArray(DRIVER_SYMLINK)); + if (symlink.exists()) { + return true; + } + // If not check with SCM, if the kernel service exists + auto scm_rights = SC_MANAGER_ALL_ACCESS; + auto serviceManager = OpenSCManager(NULL, // local computer + NULL, // servicesActive database + scm_rights); + auto servicehandle = + OpenService(serviceManager, DRIVER_SERVICE_NAME, GENERIC_READ); + auto err = GetLastError(); + CloseServiceHandle(serviceManager); + CloseServiceHandle(servicehandle); + return err != ERROR_SERVICE_DOES_NOT_EXIST; +} + +QString WindowsSplitTunnel::convertPath(const QString& path) { + auto parts = path.split("/"); + QString driveLetter = parts.takeFirst(); + if (!driveLetter.contains(":") || parts.size() == 0) { + // device should contain : for e.g C: + return ""; + } + QByteArray buffer(2048, 0xFF); + auto ok = QueryDosDeviceW(qUtf16Printable(driveLetter), + (wchar_t*)buffer.data(), buffer.size() / 2); + + if (ok == ERROR_INSUFFICIENT_BUFFER) { + buffer.resize(buffer.size() * 2); + ok = QueryDosDeviceW(qUtf16Printable(driveLetter), (wchar_t*)buffer.data(), + buffer.size() / 2); + } + if (ok == 0) { + WindowsUtils::windowsLog("Err fetching dos path"); + return ""; + } + QString deviceName; + deviceName = QString::fromWCharArray((wchar_t*)buffer.data()); + parts.prepend(deviceName); + + return parts.join("\\"); +} + +// static +bool WindowsSplitTunnel::detectConflict() { + auto scm_rights = SC_MANAGER_ENUMERATE_SERVICE; + auto serviceManager = OpenSCManager(NULL, // local computer + NULL, // servicesActive database + scm_rights); + auto cleanup = qScopeGuard([&] { CloseServiceHandle(serviceManager); }); + // Query for Mullvad Service. + auto servicehandle = + OpenService(serviceManager, MV_SERVICE_NAME, GENERIC_READ); + auto err = GetLastError(); + CloseServiceHandle(servicehandle); + if (err != ERROR_SERVICE_DOES_NOT_EXIST) { + WindowsUtils::windowsLog("Mullvad Detected - Disabling SplitTunnel: "); + // Mullvad is installed, so we would certainly break things. + return true; + } + auto symlink = QFileInfo(QString::fromWCharArray(DRIVER_SYMLINK)); + if (!symlink.exists()) { + // The driver is not loaded / installed.. MV is not installed, all good! + logger.info() << "No Split-Tunnel Conflict detected, continue."; + return false; + } + // The driver exists, so let's check if it has been created by us. + // If our service is not present, it's has been created by + // someone else so we should not use that :) + servicehandle = + OpenService(serviceManager, DRIVER_SERVICE_NAME, GENERIC_READ); + err = GetLastError(); + CloseServiceHandle(servicehandle); + return err == ERROR_SERVICE_DOES_NOT_EXIST; +} diff --git a/client/platforms/windows/daemon/windowssplittunnel.h b/client/platforms/windows/daemon/windowssplittunnel.h new file mode 100644 index 00000000..10a96f11 --- /dev/null +++ b/client/platforms/windows/daemon/windowssplittunnel.h @@ -0,0 +1,180 @@ +/* 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 WINDOWSSPLITTUNNEL_H +#define WINDOWSSPLITTUNNEL_H + +#include +#include +#include + +// Note: the ws2tcpip.h import must come before the others. +// clang-format off +#include +// clang-format on +#include +#include +#include +#include + +// States for GetState +enum DRIVER_STATE { + STATE_UNKNOWN = -1, + STATE_NONE = 0, + STATE_STARTED = 1, + STATE_INITIALIZED = 2, + STATE_READY = 3, + STATE_RUNNING = 4, + STATE_ZOMBIE = 5, +}; + +#ifndef CTL_CODE + +# define FILE_ANY_ACCESS 0x0000 + +# define METHOD_BUFFERED 0 +# define METHOD_IN_DIRECT 1 +# define METHOD_NEITHER 3 + +# define CTL_CODE(DeviceType, Function, Method, Access) \ + (((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method)) +#endif + +// Known ControlCodes +#define IOCTL_INITIALIZE CTL_CODE(0x8000, 1, METHOD_NEITHER, FILE_ANY_ACCESS) + +#define IOCTL_DEQUEUE_EVENT \ + CTL_CODE(0x8000, 2, METHOD_BUFFERED, FILE_ANY_ACCESS) + +#define IOCTL_REGISTER_PROCESSES \ + CTL_CODE(0x8000, 3, METHOD_BUFFERED, FILE_ANY_ACCESS) + +#define IOCTL_REGISTER_IP_ADDRESSES \ + CTL_CODE(0x8000, 4, METHOD_BUFFERED, FILE_ANY_ACCESS) + +#define IOCTL_GET_IP_ADDRESSES \ + CTL_CODE(0x8000, 5, METHOD_BUFFERED, FILE_ANY_ACCESS) + +#define IOCTL_SET_CONFIGURATION \ + CTL_CODE(0x8000, 6, METHOD_BUFFERED, FILE_ANY_ACCESS) + +#define IOCTL_GET_CONFIGURATION \ + CTL_CODE(0x8000, 7, METHOD_BUFFERED, FILE_ANY_ACCESS) + +#define IOCTL_CLEAR_CONFIGURATION \ + CTL_CODE(0x8000, 8, METHOD_NEITHER, FILE_ANY_ACCESS) + +#define IOCTL_GET_STATE CTL_CODE(0x8000, 9, METHOD_BUFFERED, FILE_ANY_ACCESS) + +#define IOCTL_QUERY_PROCESS \ + CTL_CODE(0x8000, 10, METHOD_BUFFERED, FILE_ANY_ACCESS) + +#define IOCTL_ST_RESET CTL_CODE(0x8000, 11, METHOD_NEITHER, FILE_ANY_ACCESS) + +// Driver Configuration structures + +typedef struct { + // Offset into buffer region that follows all entries. + // The image name uses the device path. + SIZE_T ImageNameOffset; + // Length of the String + USHORT ImageNameLength; +} CONFIGURATION_ENTRY; + +typedef struct { + // Number of entries immediately following the header. + SIZE_T NumEntries; + + // Total byte length: header + entries + string buffer. + SIZE_T TotalLength; +} CONFIGURATION_HEADER; + +// Used to Configure Which IP is network/vpn +typedef struct { + IN_ADDR TunnelIpv4; + IN_ADDR InternetIpv4; + + IN6_ADDR TunnelIpv6; + IN6_ADDR InternetIpv6; +} IP_ADDRESSES_CONFIG; + +// Used to Define Which Processes are alive on activation +typedef struct { + SIZE_T NumEntries; + SIZE_T TotalLength; +} PROCESS_DISCOVERY_HEADER; + +typedef struct { + HANDLE ProcessId; + HANDLE ParentProcessId; + + SIZE_T ImageNameOffset; + USHORT ImageNameLength; +} PROCESS_DISCOVERY_ENTRY; + +typedef struct { + DWORD ProcessId; + DWORD ParentProcessId; + FILETIME CreationTime; + std::wstring DevicePath; +} ProcessInfo; + +class WindowsSplitTunnel final : public QObject { + Q_OBJECT + Q_DISABLE_COPY_MOVE(WindowsSplitTunnel) + public: + explicit WindowsSplitTunnel(QObject* parent); + ~WindowsSplitTunnel(); + + // void excludeApps(const QStringList& paths); + // Excludes an Application from the VPN + void setRules(const QStringList& appPaths); + + // Fetches and Pushed needed info to move to engaged mode + void start(int inetAdapterIndex); + // Deletes Rules and puts the driver into passive mode + void stop(); + // Resets the Whole Driver + void reset(); + + // Just close connection, leave state as is + void close(); + + // Installes the Kernel Driver as Driver Service + static SC_HANDLE installDriver(); + static bool uninstallDriver(); + static bool isInstalled(); + static bool detectConflict(); + + private slots: + void initDriver(); + + private: + HANDLE m_driver = INVALID_HANDLE_VALUE; + constexpr static const auto DRIVER_SYMLINK = L"\\\\.\\MULLVADSPLITTUNNEL"; + constexpr static const auto DRIVER_FILENAME = "mullvad-split-tunnel.sys"; + constexpr static const auto DRIVER_SERVICE_NAME = L"AmneziaVPNSplitTunnel"; + constexpr static const auto MV_SERVICE_NAME = L"MullvadVPN"; + DRIVER_STATE getState(); + + // Initializes the WFP Sublayer + bool initSublayer(); + + // Generates a Configuration for Each APP + std::vector generateAppConfiguration(const QStringList& appPaths); + // Generates a Configuration which IP's are VPN and which network + std::vector generateIPConfiguration(int inetAdapterIndex); + std::vector generateProcessBlob(); + + void getAddress(int adapterIndex, IN_ADDR* out_ipv4, IN6_ADDR* out_ipv6); + // Collects info about an Opened Process + ProcessInfo getProcessInfo(HANDLE process, + const PROCESSENTRY32W& processMeta); + + // Converts a path to a Dos Path: + // e.g C:/a.exe -> /harddisk0/a.exe + QString convertPath(const QString& path); +}; + +#endif // WINDOWSSPLITTUNNEL_H diff --git a/client/platforms/windows/daemon/windowstunnellogger.cpp b/client/platforms/windows/daemon/windowstunnellogger.cpp new file mode 100644 index 00000000..7194f0cd --- /dev/null +++ b/client/platforms/windows/daemon/windowstunnellogger.cpp @@ -0,0 +1,135 @@ +/* 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 "windowstunnellogger.h" + +#include + +#include "leakdetector.h" +#include "logger.h" + +/* The ring logger format used by the Wireguard DLL is as follows, assuming + * no padding: + * + * struct { + * uint32_t magic; + * uint32_t index; + * struct { + * uint64_t timestamp; + * char message[512]; + * } ring[2048]; + * }; + */ + +constexpr uint32_t RINGLOG_POLL_MSEC = 250; +constexpr uint32_t RINGLOG_MAGIC_HEADER = 0xbadbabe; +constexpr uint32_t RINGLOG_INDEX_OFFSET = 4; +constexpr uint32_t RINGLOG_HEADER_SIZE = 8; +constexpr uint32_t RINGLOG_MAX_ENTRIES = 2048; +constexpr uint32_t RINGLOG_MESSAGE_SIZE = 512; +constexpr uint32_t RINGLOG_TIMESTAMP_SIZE = 8; +constexpr uint32_t RINGLOG_FILE_SIZE = + RINGLOG_HEADER_SIZE + + ((RINGLOG_MESSAGE_SIZE + RINGLOG_TIMESTAMP_SIZE) * RINGLOG_MAX_ENTRIES); + +namespace { +Logger logger("tunnel.dll"); +} // namespace + +WindowsTunnelLogger::WindowsTunnelLogger(const QString& filename, + QObject* parent) + : QObject(parent), m_logfile(filename, this), m_timer(this) { + MZ_COUNT_CTOR(WindowsTunnelLogger); + + m_startTime = QDateTime::currentMSecsSinceEpoch() * 1000000; + m_logindex = -1; + + connect(&m_timer, SIGNAL(timeout()), this, SLOT(timeout())); + m_timer.start(RINGLOG_POLL_MSEC); +} + +WindowsTunnelLogger::~WindowsTunnelLogger() { + MZ_COUNT_DTOR(WindowsTunnelLogger); + if (m_logdata) { + timeout(); + + m_logfile.unmap(m_logdata); + m_logdata = nullptr; + } +} + +bool WindowsTunnelLogger::openLogData() { + if (m_logdata) { + return true; + } + if (!m_logfile.open(QIODevice::ReadOnly)) { + return false; + } + + m_logdata = m_logfile.map(0, RINGLOG_FILE_SIZE); + if (!m_logdata) { + m_logfile.close(); + return false; + } + + // Check for a valid magic header + uint32_t magic; + memcpy(&magic, m_logdata, 4); + logger.debug() << "Opening tunnel log file" << m_logfile.fileName(); + if (magic != RINGLOG_MAGIC_HEADER) { + logger.error() << "Unexpected magic header:" << QString::number(magic, 16); + m_logfile.unmap(m_logdata); + m_logfile.close(); + m_logdata = nullptr; + return false; + } + + return true; +} + +int WindowsTunnelLogger::nextIndex() { + qint32 value; + memcpy(&value, m_logdata + RINGLOG_INDEX_OFFSET, 4); + return value % RINGLOG_MAX_ENTRIES; +} + +void WindowsTunnelLogger::process(int index) { + Q_ASSERT(index >= 0); + Q_ASSERT(index < RINGLOG_MAX_ENTRIES); + size_t offset = static_cast(index) * + (RINGLOG_TIMESTAMP_SIZE + RINGLOG_MESSAGE_SIZE); + uchar* data = m_logdata + RINGLOG_HEADER_SIZE + offset; + + quint64 timestamp; + memcpy(×tamp, data, RINGLOG_TIMESTAMP_SIZE); + if (timestamp <= m_startTime) { + return; + } + QByteArray message((const char*)data + RINGLOG_TIMESTAMP_SIZE, + RINGLOG_MESSAGE_SIZE); + int nullIndex = message.indexOf(0x0); + if (nullIndex >= 0) { + message.truncate(nullIndex); + } + logger.info() << QString::fromUtf8(message); +} + +void WindowsTunnelLogger::timeout() { + if (!openLogData()) { + return; + } + + /* On the first pass, scan all log messages. */ + if (m_logindex < 0) { + m_logindex = nextIndex(); + process(m_logindex); + m_logindex = (m_logindex + 1) % RINGLOG_MAX_ENTRIES; + } + + /* Report new messages. */ + while (m_logindex != nextIndex()) { + process(m_logindex); + m_logindex = (m_logindex + 1) % RINGLOG_MAX_ENTRIES; + } +} diff --git a/client/platforms/windows/daemon/windowstunnellogger.h b/client/platforms/windows/daemon/windowstunnellogger.h new file mode 100644 index 00000000..ce7142b7 --- /dev/null +++ b/client/platforms/windows/daemon/windowstunnellogger.h @@ -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/. */ + +#ifndef WINDOWSTUNNELLOGGER_H +#define WINDOWSTUNNELLOGGER_H + +#include +#include +#include + +class WindowsTunnelLogger final : public QObject { + Q_OBJECT + Q_DISABLE_COPY_MOVE(WindowsTunnelLogger) + + public: + WindowsTunnelLogger(const QString& filename, QObject* parent = nullptr); + ~WindowsTunnelLogger(); + + private slots: + void timeout(); + + private: + bool openLogData(); + void process(int index); + int nextIndex(); + + private: + QTimer m_timer; + QFile m_logfile; + uchar* m_logdata = nullptr; + int m_logindex = -1; + quint64 m_startTime = 0; +}; + +#endif // WINDOWSTUNNELLOGGER_H diff --git a/client/platforms/windows/daemon/windowstunnelservice.cpp b/client/platforms/windows/daemon/windowstunnelservice.cpp new file mode 100644 index 00000000..37f81f26 --- /dev/null +++ b/client/platforms/windows/daemon/windowstunnelservice.cpp @@ -0,0 +1,345 @@ +/* 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 "WindowsTunnelService.h" + +#include + +#include +#include + +#include "leakdetector.h" +#include "logger.h" +#include "platforms/windows/windowscommons.h" +#include "platforms/windows/windowsutils.h" +#include "windowsdaemon.h" + +#define TUNNEL_NAMED_PIPE \ + "\\\\." \ + "\\pipe\\ProtectedPrefix\\Administrators\\WireGuard\\AmneziaVPN" + +constexpr uint32_t WINDOWS_TUNNEL_MONITOR_TIMEOUT_MSEC = 2000; + +namespace { +Logger logger("WindowsTunnelService"); +} // namespace + +static bool stopAndDeleteTunnelService(SC_HANDLE service); +static bool waitForServiceStatus(SC_HANDLE service, DWORD expectedStatus); + +WindowsTunnelService::WindowsTunnelService(QObject* parent) : QObject(parent) { + MZ_COUNT_CTOR(WindowsTunnelService); + + m_scm = OpenSCManager(nullptr, nullptr, SC_MANAGER_ALL_ACCESS); + if (m_scm == nullptr) { + WindowsUtils::windowsLog("Failed to open SCManager"); + } + + connect(&m_timer, &QTimer::timeout, this, &WindowsTunnelService::timeout); +} + +WindowsTunnelService::~WindowsTunnelService() { + MZ_COUNT_CTOR(WindowsTunnelService); + stop(); + CloseServiceHandle((SC_HANDLE)m_scm); +} + +void WindowsTunnelService::stop() { + SC_HANDLE service = (SC_HANDLE)m_service; + if (service) { + stopAndDeleteTunnelService(service); + CloseServiceHandle(service); + m_service = nullptr; + } + + m_timer.stop(); + + if (m_logworker) { + m_logthread.quit(); + m_logthread.wait(); + delete m_logworker; + m_logworker = nullptr; + } +} + +bool WindowsTunnelService::isRunning() { + if (m_service == nullptr) { + return false; + } + + SERVICE_STATUS status; + if (!QueryServiceStatus((SC_HANDLE)m_service, &status)) { + return false; + } + + return status.dwCurrentState == SERVICE_RUNNING; +} + +void WindowsTunnelService::timeout() { + if (m_service == nullptr) { + logger.error() << "The service doesn't exist"; + emit backendFailure(); + return; + } + + SERVICE_STATUS status; + if (!QueryServiceStatus((SC_HANDLE)m_service, &status)) { + WindowsUtils::windowsLog("Failed to retrieve the service status"); + emit backendFailure(); + return; + } + + if (status.dwCurrentState == SERVICE_RUNNING) { + // The service is active + return; + } + + logger.debug() << "The service is not active"; + emit backendFailure(); +} + +bool WindowsTunnelService::start(const QString& configData) { + logger.debug() << "Starting the tunnel service"; + + m_logworker = new WindowsTunnelLogger(WindowsCommons::tunnelLogFile()); + m_logworker->moveToThread(&m_logthread); + m_logthread.start(); + + SC_HANDLE scm = (SC_HANDLE)m_scm; + SC_HANDLE service = nullptr; + auto guard = qScopeGuard([&] { + if (service) { + CloseServiceHandle(service); + } + m_logthread.quit(); + m_logthread.wait(); + delete m_logworker; + m_logworker = nullptr; + }); + + // Let's see if we have to delete a previous instance. + service = OpenService(scm, TUNNEL_SERVICE_NAME, SERVICE_ALL_ACCESS); + if (service) { + logger.debug() << "An existing service has been detected. Let's close it."; + if (!stopAndDeleteTunnelService(service)) { + return false; + } + CloseServiceHandle(service); + service = nullptr; + } + + QString serviceCmdline; + { + QTextStream out(&serviceCmdline); + out << "\"" << qApp->applicationFilePath() << "\" tunneldaemon \"" + << configData << "\""; + } + + logger.debug() << "Service:" << qApp->applicationFilePath(); + + service = CreateService(scm, TUNNEL_SERVICE_NAME, L"Amezia VPN (tunnel)", + SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS, + SERVICE_DEMAND_START, SERVICE_ERROR_NORMAL, + (const wchar_t*)serviceCmdline.utf16(), nullptr, 0, + TEXT("Nsi\0TcpIp\0"), nullptr, nullptr); + if (!service) { + WindowsUtils::windowsLog("Failed to create the tunnel service"); + return false; + } + + SERVICE_DESCRIPTION sd = { + (wchar_t*)L"Manages the Amnezia VPN tunnel connection"}; + + if (!ChangeServiceConfig2(service, SERVICE_CONFIG_DESCRIPTION, &sd)) { + WindowsUtils::windowsLog( + "Failed to set the description to the tunnel service"); + return false; + } + + SERVICE_SID_INFO ssi; + ssi.dwServiceSidType = SERVICE_SID_TYPE_UNRESTRICTED; + if (!ChangeServiceConfig2(service, SERVICE_CONFIG_SERVICE_SID_INFO, &ssi)) { + WindowsUtils::windowsLog("Failed to set the SID to the tunnel service"); + return false; + } + + if (!StartService(service, 0, nullptr)) { + WindowsUtils::windowsLog("Failed to start the service"); + return false; + } + + if (waitForServiceStatus(service, SERVICE_RUNNING)) { + logger.debug() << "The tunnel service is up and running"; + guard.dismiss(); + m_service = service; + m_timer.start(WINDOWS_TUNNEL_MONITOR_TIMEOUT_MSEC); + return true; + } + + logger.error() << "Failed to run the tunnel service"; + + SERVICE_STATUS status; + if (!QueryServiceStatus(service, &status)) { + WindowsUtils::windowsLog("Failed to retrieve the service status"); + return false; + } + + logger.debug() << "The tunnel service exited with status:" + << status.dwWin32ExitCode << "-" << exitCodeToFailure(&status); + + emit backendFailure(); + return false; +} + +static bool stopAndDeleteTunnelService(SC_HANDLE service) { + SERVICE_STATUS status; + if (!QueryServiceStatus(service, &status)) { + WindowsUtils::windowsLog("Failed to retrieve the service status"); + return false; + } + + logger.debug() << "The current service is stopped:" + << (status.dwCurrentState == SERVICE_STOPPED); + + if (status.dwCurrentState != SERVICE_STOPPED) { + logger.debug() << "The service is not stopped yet."; + if (!ControlService(service, SERVICE_CONTROL_STOP, &status)) { + WindowsUtils::windowsLog("Failed to control the service"); + return false; + } + + if (!waitForServiceStatus(service, SERVICE_STOPPED)) { + logger.error() << "Unable to stop the service"; + return false; + } + } + + logger.debug() << "Proceeding with the deletion"; + + if (!DeleteService(service)) { + WindowsUtils::windowsLog("Failed to delete the service"); + return false; + } + + return true; +} + +QString WindowsTunnelService::uapiCommand(const QString& command) { + // Create a pipe to the tunnel service. + LPTSTR tunnelName = (LPTSTR)TEXT(TUNNEL_NAMED_PIPE); + HANDLE pipe = CreateFile(tunnelName, GENERIC_READ | GENERIC_WRITE, 0, nullptr, + OPEN_EXISTING, 0, nullptr); + if (pipe == INVALID_HANDLE_VALUE) { + return QString(); + } + + auto guard = qScopeGuard([&] { CloseHandle(pipe); }); + if (!WaitNamedPipe(tunnelName, 1000)) { + WindowsUtils::windowsLog("Failed to wait for named pipes"); + return QString(); + } + + DWORD mode = PIPE_READMODE_BYTE; + if (!SetNamedPipeHandleState(pipe, &mode, nullptr, nullptr)) { + WindowsUtils::windowsLog("Failed to set the read-mode on pipe"); + return QString(); + } + + // Write the UAPI command into the pipe. + QByteArray message = command.toLocal8Bit(); + DWORD written; + while (!message.endsWith("\n\n")) { + message.append('\n'); + } + if (!WriteFile(pipe, message.constData(), message.length(), &written, + nullptr)) { + WindowsUtils::windowsLog("Failed to write into the pipe"); + return QString(); + } + + // Receive the response from the pipe. + QByteArray reply; + while (!reply.contains("\n\n")) { + char buffer[512]; + DWORD read = 0; + if (!ReadFile(pipe, buffer, sizeof(buffer), &read, nullptr)) { + break; + } + + reply.append(buffer, read); + } + + return QString::fromUtf8(reply).trimmed(); +} + +// static +static bool waitForServiceStatus(SC_HANDLE service, DWORD expectedStatus) { + int tries = 0; + while (tries < 30) { + SERVICE_STATUS status; + if (!QueryServiceStatus(service, &status)) { + WindowsUtils::windowsLog("Failed to retrieve the service status"); + return false; + } + + if (status.dwCurrentState == expectedStatus) { + return true; + } + + logger.warning() << "The service is not in the right status yet."; + + Sleep(1000); + ++tries; + } + + return false; +} + +// static +QString WindowsTunnelService::exitCodeToFailure(const void* status) { + const SERVICE_STATUS* st = static_cast(status); + if (st->dwWin32ExitCode != ERROR_SERVICE_SPECIFIC_ERROR) { + return WindowsUtils::getErrorMessage(st->dwWin32ExitCode); + } + + // The order of this error code is taken from wireguard. + switch (st->dwServiceSpecificExitCode) { + case 0: + return "No error"; + case 1: + return "Error when opening the ringlogger log file"; + case 2: + return "Error while loading the WireGuard configuration file from " + "path."; + case 3: + return "Error while creating a WinTun device."; + case 4: + return "Error while listening on a named pipe."; + case 5: + return "Error while resolving DNS hostname endpoints."; + case 6: + return "Error while manipulating firewall rules."; + case 7: + return "Error while setting the device configuration."; + case 8: + return "Error while binding sockets to default routes."; + case 9: + return "Unable to set interface addresses, routes, dns, and/or " + "interface settings."; + case 10: + return "Error while determining current executable path."; + case 11: + return "Error while opening the NUL file."; + case 12: + return "Error while attempting to track tunnels."; + case 13: + return "Error while attempting to enumerate current sessions."; + case 14: + return "Error while dropping privileges."; + case 15: + return "Windows internal error."; + default: + return QString("Unknown error (%1)").arg(st->dwServiceSpecificExitCode); + } +} diff --git a/client/platforms/windows/daemon/windowstunnelservice.h b/client/platforms/windows/daemon/windowstunnelservice.h new file mode 100644 index 00000000..c6027a3b --- /dev/null +++ b/client/platforms/windows/daemon/windowstunnelservice.h @@ -0,0 +1,45 @@ +/* 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 WINDOWSTUNNELSERVICE_H +#define WINDOWSTUNNELSERVICE_H + +#include +#include +#include +#include + +#include "windowstunnellogger.h" + +class WindowsTunnelService final : public QObject { + Q_OBJECT + Q_DISABLE_COPY_MOVE(WindowsTunnelService) + + public: + WindowsTunnelService(QObject* parent = nullptr); + ~WindowsTunnelService(); + + bool start(const QString& configData); + void stop(); + bool isRunning(); + QString uapiCommand(const QString& command); + + signals: + void backendFailure(); + + private: + void timeout(); + static QString exitCodeToFailure(const void* status); + + private: + QTimer m_timer; + QThread m_logthread; + WindowsTunnelLogger* m_logworker = nullptr; + + // These are really SC_HANDLEs in disguise. + void* m_scm = nullptr; + void* m_service = nullptr; +}; + +#endif // WINDOWSTUNNELSERVICE_H diff --git a/client/platforms/windows/daemon/wireguardutilswindows.cpp b/client/platforms/windows/daemon/wireguardutilswindows.cpp new file mode 100644 index 00000000..1e0a4752 --- /dev/null +++ b/client/platforms/windows/daemon/wireguardutilswindows.cpp @@ -0,0 +1,275 @@ +/* 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 "wireguardutilswindows.h" + +#include +#include +#include +#include +#include + +#include + +#include "leakdetector.h" +#include "logger.h" +#include "platforms/windows/windowscommons.h" +#include "windowsdaemon.h" +#include "windowsfirewall.h" + +#pragma comment(lib, "iphlpapi.lib") + +namespace { +Logger logger("WireguardUtilsWindows"); +}; // namespace + +WireguardUtilsWindows::WireguardUtilsWindows(QObject* parent) + : WireguardUtils(parent), m_tunnel(this), m_routeMonitor(this) { + MZ_COUNT_CTOR(WireguardUtilsWindows); + logger.debug() << "WireguardUtilsWindows created."; + + connect(&m_tunnel, &WindowsTunnelService::backendFailure, this, + [&] { emit backendFailure(); }); +} + +WireguardUtilsWindows::~WireguardUtilsWindows() { + MZ_COUNT_DTOR(WireguardUtilsWindows); + logger.debug() << "WireguardUtilsWindows destroyed."; +} + +QList WireguardUtilsWindows::getPeerStatus() { + QString reply = m_tunnel.uapiCommand("get=1"); + PeerStatus status; + QList peerList; + for (const QString& line : reply.split('\n')) { + int eq = line.indexOf('='); + if (eq <= 0) { + continue; + } + QString name = line.left(eq); + QString value = line.mid(eq + 1); + + if (name == "public_key") { + if (!status.m_pubkey.isEmpty()) { + peerList.append(status); + } + QByteArray pubkey = QByteArray::fromHex(value.toUtf8()); + status = PeerStatus(pubkey.toBase64()); + } + + if (name == "tx_bytes") { + status.m_txBytes = value.toDouble(); + } + if (name == "rx_bytes") { + status.m_rxBytes = value.toDouble(); + } + if (name == "last_handshake_time_sec") { + status.m_handshake += value.toLongLong() * 1000; + } + if (name == "last_handshake_time_nsec") { + status.m_handshake += value.toLongLong() / 1000000; + } + } + if (!status.m_pubkey.isEmpty()) { + peerList.append(status); + } + + return peerList; +} + +bool WireguardUtilsWindows::addInterface(const InterfaceConfig& config) { + QStringList addresses; + for (const IPAddress& ip : config.m_allowedIPAddressRanges) { + addresses.append(ip.toString()); + } + + QMap extraConfig; + extraConfig["Table"] = "off"; + QString configString = config.toWgConf(extraConfig); + if (configString.isEmpty()) { + logger.error() << "Failed to create a config file"; + return false; + } + + // We don't want to pass a peer just yet, that will happen later with + // a UAPI command in WireguardUtilsWindows::updatePeer(), so truncate + // the config file to remove the [Peer] section. + qsizetype peerStart = configString.indexOf("[Peer]", 0, Qt::CaseSensitive); + if (peerStart >= 0) { + configString.truncate(peerStart); + } + + if (!m_tunnel.start(configString)) { + logger.error() << "Failed to activate the tunnel service"; + return false; + } + + // Determine the interface LUID + NET_LUID luid; + QString ifAlias = interfaceName(); + DWORD result = ConvertInterfaceAliasToLuid((wchar_t*)ifAlias.utf16(), &luid); + if (result != 0) { + logger.error() << "Failed to lookup LUID:" << result; + return false; + } + m_luid = luid.Value; + m_routeMonitor.setLuid(luid.Value); + + // Enable the windows firewall + NET_IFINDEX ifindex; + ConvertInterfaceLuidToIndex(&luid, &ifindex); + WindowsFirewall::instance()->enableKillSwitch(ifindex); + + logger.debug() << "Registration completed"; + return true; +} + +bool WireguardUtilsWindows::deleteInterface() { + WindowsFirewall::instance()->disableKillSwitch(); + m_tunnel.stop(); + return true; +} + +bool WireguardUtilsWindows::updatePeer(const InterfaceConfig& config) { + QByteArray publicKey = + QByteArray::fromBase64(qPrintable(config.m_serverPublicKey)); + QByteArray pskKey = + QByteArray::fromBase64(qPrintable(config.m_serverPskKey)); + + // Enable the windows firewall for this peer. + WindowsFirewall::instance()->enablePeerTraffic(config); + + logger.debug() << "Configuring peer" << publicKey.toHex() + << "via" << config.m_serverIpv4AddrIn; + + // Update/create the peer config + QString message; + QTextStream out(&message); + out << "set=1\n"; + out << "public_key=" << QString(publicKey.toHex()) << "\n"; + out << "preshared_key=" << QString(pskKey.toHex()) << "\n"; + if (!config.m_serverIpv4AddrIn.isNull()) { + out << "endpoint=" << config.m_serverIpv4AddrIn << ":"; + } else if (!config.m_serverIpv6AddrIn.isNull()) { + out << "endpoint=[" << config.m_serverIpv6AddrIn << "]:"; + } else { + logger.warning() << "Failed to create peer with no endpoints"; + return false; + } + out << config.m_serverPort << "\n"; + + out << "replace_allowed_ips=true\n"; + out << "persistent_keepalive_interval=" << WG_KEEPALIVE_PERIOD << "\n"; + for (const IPAddress& ip : config.m_allowedIPAddressRanges) { + out << "allowed_ip=" << ip.toString() << "\n"; + } + + // Exclude the server address, except for multihop exit servers. + if (config.m_hopType != InterfaceConfig::MultiHopExit) { + m_routeMonitor.addExclusionRoute(IPAddress(config.m_serverIpv4AddrIn)); + m_routeMonitor.addExclusionRoute(IPAddress(config.m_serverIpv6AddrIn)); + } + + QString reply = m_tunnel.uapiCommand(message); + logger.debug() << "DATA:" << reply; + return true; +} + +bool WireguardUtilsWindows::deletePeer(const InterfaceConfig& config) { + QByteArray publicKey = + QByteArray::fromBase64(qPrintable(config.m_serverPublicKey)); + + // Clear exclustion routes for this peer. + if (config.m_hopType != InterfaceConfig::MultiHopExit) { + m_routeMonitor.deleteExclusionRoute(IPAddress(config.m_serverIpv4AddrIn)); + m_routeMonitor.deleteExclusionRoute(IPAddress(config.m_serverIpv6AddrIn)); + } + + // Disable the windows firewall for this peer. + WindowsFirewall::instance()->disablePeerTraffic(config.m_serverPublicKey); + + QString message; + QTextStream out(&message); + out << "set=1\n"; + out << "public_key=" << QString(publicKey.toHex()) << "\n"; + out << "remove=true\n"; + + QString reply = m_tunnel.uapiCommand(message); + logger.debug() << "DATA:" << reply; + return true; +} + +void WireguardUtilsWindows::buildMibForwardRow(const IPAddress& prefix, + void* row) { + MIB_IPFORWARD_ROW2* entry = (MIB_IPFORWARD_ROW2*)row; + InitializeIpForwardEntry(entry); + + // Populate the next hop + if (prefix.type() == QAbstractSocket::IPv6Protocol) { + InetPtonA(AF_INET6, qPrintable(prefix.address().toString()), + &entry->DestinationPrefix.Prefix.Ipv6.sin6_addr); + entry->DestinationPrefix.Prefix.Ipv6.sin6_family = AF_INET6; + entry->DestinationPrefix.PrefixLength = prefix.prefixLength(); + } else { + InetPtonA(AF_INET, qPrintable(prefix.address().toString()), + &entry->DestinationPrefix.Prefix.Ipv4.sin_addr); + entry->DestinationPrefix.Prefix.Ipv4.sin_family = AF_INET; + entry->DestinationPrefix.PrefixLength = prefix.prefixLength(); + } + entry->InterfaceLuid.Value = m_luid; + entry->NextHop.si_family = entry->DestinationPrefix.Prefix.si_family; + + // Set the rest of the flags for a static route. + entry->ValidLifetime = 0xffffffff; + entry->PreferredLifetime = 0xffffffff; + entry->Metric = 0; + entry->Protocol = MIB_IPPROTO_NETMGMT; + entry->Loopback = false; + entry->AutoconfigureAddress = false; + entry->Publish = false; + entry->Immortal = false; + entry->Age = 0; +} + +bool WireguardUtilsWindows::updateRoutePrefix(const IPAddress& prefix) { + MIB_IPFORWARD_ROW2 entry; + buildMibForwardRow(prefix, &entry); + + // Install the route + DWORD result = CreateIpForwardEntry2(&entry); + if (result == ERROR_OBJECT_ALREADY_EXISTS) { + return true; + } + if (result != NO_ERROR) { + logger.error() << "Failed to create route to" + << logger.sensitive(prefix.toString()) + << "result:" << result; + } + return result == NO_ERROR; +} + +bool WireguardUtilsWindows::deleteRoutePrefix(const IPAddress& prefix) { + MIB_IPFORWARD_ROW2 entry; + buildMibForwardRow(prefix, &entry); + + // Install the route + DWORD result = DeleteIpForwardEntry2(&entry); + if (result == ERROR_NOT_FOUND) { + return true; + } + if (result != NO_ERROR) { + logger.error() << "Failed to delete route to" + << logger.sensitive(prefix.toString()) + << "result:" << result; + } + return result == NO_ERROR; +} + +bool WireguardUtilsWindows::addExclusionRoute(const IPAddress& prefix) { + return m_routeMonitor.addExclusionRoute(prefix); +} + +bool WireguardUtilsWindows::deleteExclusionRoute(const IPAddress& prefix) { + return m_routeMonitor.deleteExclusionRoute(prefix); +} diff --git a/client/platforms/windows/daemon/wireguardutilswindows.h b/client/platforms/windows/daemon/wireguardutilswindows.h new file mode 100644 index 00000000..4fd67fad --- /dev/null +++ b/client/platforms/windows/daemon/wireguardutilswindows.h @@ -0,0 +1,53 @@ +/* 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 WIREGUARDUTILSWINDOWS_H +#define WIREGUARDUTILSWINDOWS_H + +#include + +#include +#include + +#include "daemon/wireguardutils.h" +#include "windowsroutemonitor.h" +#include "windowstunnelservice.h" + +class WireguardUtilsWindows final : public WireguardUtils { + Q_OBJECT + + public: + WireguardUtilsWindows(QObject* parent); + ~WireguardUtilsWindows(); + + bool interfaceExists() override { return m_tunnel.isRunning(); } + QString interfaceName() override { + return WireguardUtilsWindows::s_interfaceName(); + } + static const QString s_interfaceName() { return "AmneziaVPN"; } + bool addInterface(const InterfaceConfig& config) override; + bool deleteInterface() override; + + bool updatePeer(const InterfaceConfig& config) override; + bool deletePeer(const InterfaceConfig& config) override; + QList getPeerStatus() override; + + bool updateRoutePrefix(const IPAddress& prefix) override; + bool deleteRoutePrefix(const IPAddress& prefix) override; + + bool addExclusionRoute(const IPAddress& prefix) override; + bool deleteExclusionRoute(const IPAddress& prefix) override; + + signals: + void backendFailure(); + + private: + void buildMibForwardRow(const IPAddress& prefix, void* row); + + quint64 m_luid = 0; + WindowsTunnelService m_tunnel; + WindowsRouteMonitor m_routeMonitor; +}; + +#endif // WIREGUARDUTILSWINDOWS_H diff --git a/client/platforms/windows/windowscommons.cpp b/client/platforms/windows/windowscommons.cpp new file mode 100644 index 00000000..dd9583d6 --- /dev/null +++ b/client/platforms/windows/windowscommons.cpp @@ -0,0 +1,186 @@ +/* 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 "windowscommons.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "logger.h" +#include "platforms/windows/windowsutils.h" + +#define TUNNEL_SERVICE_NAME L"WireGuardTunnel$amnvpn" + +constexpr const char* VPN_NAME = "AmneziaVPN"; + +namespace { +Logger logger("WindowsCommons"); +} + +QString WindowsCommons::tunnelConfigFile() { + QStringList paths = + QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation); + for (const QString& path : paths) { + QDir dir(path); + if (!dir.exists()) { + continue; + } + + QDir vpnDir(dir.filePath(VPN_NAME)); + if (!vpnDir.exists()) { + continue; + } + + QString wireguardFile(vpnDir.filePath(QString("%1.conf").arg(VPN_NAME))); + if (!QFileInfo::exists(wireguardFile)) { + continue; + } + + logger.debug() << "Found the current wireguard configuration:" + << wireguardFile; + return wireguardFile; + } + + for (const QString& path : paths) { + QDir dir(path); + + QDir vpnDir(dir.filePath(VPN_NAME)); + if (!vpnDir.exists() && !dir.mkdir(VPN_NAME)) { + logger.debug() << "Failed to create path Amnezia under" << path; + continue; + } + + return vpnDir.filePath(QString("%1.conf").arg(VPN_NAME)); + } + + logger.error() << "Failed to create the right paths"; + return QString(); +} + +QString WindowsCommons::tunnelLogFile() { + QStringList paths = + QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation); + + for (const QString& path : paths) { + QDir dir(path); + if (!dir.exists()) { + continue; + } + + QDir vpnDir(dir.filePath(VPN_NAME)); + if (!vpnDir.exists()) { + continue; + } + + return vpnDir.filePath("log.bin"); + } + + return QString(); +} + +// static +int WindowsCommons::AdapterIndexTo(const QHostAddress& dst) { + logger.debug() << "Getting Current Internet Adapter that routes to" + << logger.sensitive(dst.toString()); + quint32_be ipBigEndian; + quint32 ip = dst.toIPv4Address(); + qToBigEndian(ip, &ipBigEndian); + _MIB_IPFORWARDROW routeInfo; + auto result = GetBestRoute(ipBigEndian, 0, &routeInfo); + if (result != NO_ERROR) { + return -1; + } + auto adapter = + QNetworkInterface::interfaceFromIndex(routeInfo.dwForwardIfIndex); + logger.debug() << "Internet Adapter:" << adapter.name(); + return routeInfo.dwForwardIfIndex; +} + +// static +int WindowsCommons::VPNAdapterIndex() { + // For someReason QNetworkInterface::fromName(MozillaVPN) does not work >:( + auto adapterList = QNetworkInterface::allInterfaces(); + for (const auto& adapter : adapterList) { + if (adapter.humanReadableName().contains("AmneziaVPN")) { + return adapter.index(); + } + } + return -1; +} + +// Static +QString WindowsCommons::getCurrentPath() { + QByteArray buffer(2048, 0xFF); + auto ok = GetModuleFileNameA(NULL, buffer.data(), buffer.size()); + + if (ok == ERROR_INSUFFICIENT_BUFFER) { + buffer.resize(buffer.size() * 2); + ok = GetModuleFileNameA(NULL, buffer.data(), buffer.size()); + } + if (ok == 0) { + WindowsUtils::windowsLog("Err fetching dos path"); + return ""; + } + return QString::fromLocal8Bit(buffer); +} + +// Static +bool WindowsCommons::requireSoftwareRendering() { + /* Qt6 appears to require Direct3D shader level 5, and can result in rendering + * failures on some platforms. To workaround the issue, try to identify if + * this device can reliably run the shaders, and request fallback to software + * rendering if not. + * + * See: https://bugreports.qt.io/browse/QTBUG-100689 + */ + IDXGIFactory1* factory; + HRESULT result; + + result = CreateDXGIFactory1(__uuidof(IDXGIFactory1), (void**)(&factory)); + if (FAILED(result)) { + logger.error() << "Failed to create DXGI Factory:" << result; + return true; + } + auto guard = qScopeGuard([&] { factory->Release(); }); + + // Enumerate the graphics adapters to find the minimum D3D shader version + // that we can guarantee will render successfully. + UINT i = 0; + IDXGIAdapter1* adapter; + D3D_FEATURE_LEVEL minFeatureLevel = D3D_FEATURE_LEVEL_11_1; + while (factory->EnumAdapters1(i++, &adapter) != DXGI_ERROR_NOT_FOUND) { + auto adapterGuard = qScopeGuard([adapter] { adapter->Release(); }); + DXGI_ADAPTER_DESC1 desc; + adapter->GetDesc1(&desc); + QString gpuName = QString::fromWCharArray(desc.Description); + + // Try creating the driver to see what D3D feature level it supports. + D3D_FEATURE_LEVEL gpuFeatureLevel = D3D_FEATURE_LEVEL_9_1; + result = D3D11CreateDevice(adapter, D3D_DRIVER_TYPE_UNKNOWN, nullptr, 0, + nullptr, 0, D3D11_SDK_VERSION, nullptr, + &gpuFeatureLevel, nullptr); + if (FAILED(result)) { + logger.error() << "D3D Device" << gpuName + << "failed:" << QString::number((quint32)result, 16); + } else { + if (gpuFeatureLevel < minFeatureLevel) { + minFeatureLevel = gpuFeatureLevel; + } + logger.debug() << "D3D Device" << gpuName + << "supports D3D:" << QString::number(gpuFeatureLevel, 16); + } + } + + // D3D version 11.0 shader level 5, is required for GPU rendering. + return (minFeatureLevel < D3D_FEATURE_LEVEL_11_0); +} diff --git a/client/platforms/windows/windowscommons.h b/client/platforms/windows/windowscommons.h new file mode 100644 index 00000000..f529e375 --- /dev/null +++ b/client/platforms/windows/windowscommons.h @@ -0,0 +1,28 @@ +/* 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 WINDOWSCOMMONS_H +#define WINDOWSCOMMONS_H + +#include + +class QHostAddress; + +class WindowsCommons final { + public: + static QString tunnelConfigFile(); + static QString tunnelLogFile(); + + // Returns whether we need to fallback to software rendering. + static bool requireSoftwareRendering(); + + // Returns the Interface Index of the VPN Adapter + static int VPNAdapterIndex(); + // Returns the Interface Index that could Route to dst + static int AdapterIndexTo(const QHostAddress& dst); + // Returns the Path of the Current process + static QString getCurrentPath(); +}; + +#endif // WINDOWSCOMMONS_H diff --git a/client/platforms/windows/windowsnetworkwatcher.cpp b/client/platforms/windows/windowsnetworkwatcher.cpp new file mode 100644 index 00000000..2de5a726 --- /dev/null +++ b/client/platforms/windows/windowsnetworkwatcher.cpp @@ -0,0 +1,144 @@ +/* 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 "windowsnetworkwatcher.h" + +#include + +#include "leakdetector.h" +#include "logger.h" +#include "networkwatcherimpl.h" +#include "platforms/windows/windowsutils.h" + +#pragma comment(lib, "Wlanapi.lib") +#pragma comment(lib, "windowsapp.lib") + +namespace { +Logger logger("WindowsNetworkWatcher"); +} + +WindowsNetworkWatcher::WindowsNetworkWatcher(QObject* parent) + : NetworkWatcherImpl(parent) { + MZ_COUNT_CTOR(WindowsNetworkWatcher); +} + +WindowsNetworkWatcher::~WindowsNetworkWatcher() { + MZ_COUNT_DTOR(WindowsNetworkWatcher); + + if (m_wlanHandle) { + WlanCloseHandle(m_wlanHandle, nullptr); + } +} + +void WindowsNetworkWatcher::initialize() { + logger.debug() << "initialize"; + + DWORD negotiatedVersion; + if (WlanOpenHandle(2, nullptr, &negotiatedVersion, &m_wlanHandle) != + ERROR_SUCCESS) { + WindowsUtils::windowsLog("Failed to open the WLAN handle"); + return; + } + + DWORD prefNotifSource; + if (WlanRegisterNotification(m_wlanHandle, WLAN_NOTIFICATION_SOURCE_MSM, + true /* ignore duplicates */, + (WLAN_NOTIFICATION_CALLBACK)wlanCallback, this, + nullptr, &prefNotifSource) != ERROR_SUCCESS) { + WindowsUtils::windowsLog("Failed to register a wlan callback"); + return; + } + + logger.debug() << "callback registered"; +} + +// static +void WindowsNetworkWatcher::wlanCallback(PWLAN_NOTIFICATION_DATA data, + PVOID context) { + logger.debug() << "Callback"; + + WindowsNetworkWatcher* that = static_cast(context); + Q_ASSERT(that); + + that->processWlan(data); +} + +void WindowsNetworkWatcher::processWlan(PWLAN_NOTIFICATION_DATA data) { + logger.debug() << "Processing wlan data"; + + if (!isActive()) { + logger.debug() << "The watcher is off"; + return; + } + + if (data->NotificationSource != WLAN_NOTIFICATION_SOURCE_MSM) { + logger.debug() << "The wlan source is not MSM"; + return; + } + + if (data->NotificationCode != wlan_notification_msm_connected) { + logger.debug() << "Wlan unprocessed code: " << data->NotificationCode; + return; + } + + PWLAN_CONNECTION_ATTRIBUTES connectionInfo = nullptr; + DWORD connectionInfoSize = sizeof(WLAN_CONNECTION_ATTRIBUTES); + WLAN_OPCODE_VALUE_TYPE opCode = wlan_opcode_value_type_invalid; + + DWORD result = WlanQueryInterface( + m_wlanHandle, &data->InterfaceGuid, wlan_intf_opcode_current_connection, + nullptr, &connectionInfoSize, (PVOID*)&connectionInfo, &opCode); + if (result != ERROR_SUCCESS) { + WindowsUtils::windowsLog("Failed to query the interface"); + return; + } + + auto guard = qScopeGuard([&] { WlanFreeMemory(connectionInfo); }); + + QString bssid; + for (size_t i = 0; + i < sizeof(connectionInfo->wlanAssociationAttributes.dot11Bssid); ++i) { + if (i == 5) { + bssid.append(QString::asprintf( + "%.2X\n", connectionInfo->wlanAssociationAttributes.dot11Bssid[i])); + } else { + bssid.append(QString::asprintf( + "%.2X-", connectionInfo->wlanAssociationAttributes.dot11Bssid[i])); + } + } + if (bssid != m_lastBSSID) { + emit networkChanged(bssid); + m_lastBSSID = bssid; + } + + if (connectionInfo->wlanSecurityAttributes.dot11AuthAlgorithm != + DOT11_AUTH_ALGO_80211_OPEN && + connectionInfo->wlanSecurityAttributes.dot11CipherAlgorithm != + DOT11_CIPHER_ALGO_WEP && + connectionInfo->wlanSecurityAttributes.dot11CipherAlgorithm != + DOT11_CIPHER_ALGO_WEP40 && + connectionInfo->wlanSecurityAttributes.dot11CipherAlgorithm != + DOT11_CIPHER_ALGO_WEP104) { + logger.debug() << "The network is secure enough"; + return; + } + + QString ssid; + for (size_t i = 0; + i < connectionInfo->wlanAssociationAttributes.dot11Ssid.uSSIDLength; + ++i) { + ssid.append(QString::asprintf( + "%c", + (char)connectionInfo->wlanAssociationAttributes.dot11Ssid.ucSSID[i])); + } + + logger.debug() << "Unsecure network:" << logger.sensitive(ssid) + << "id:" << logger.sensitive(bssid); + emit unsecuredNetwork(ssid, bssid); +} + +NetworkWatcherImpl::TransportType WindowsNetworkWatcher::getTransportType() { + // TODO: Implement this once we update to Qt6.3 (VPN-3511) + return TransportType_Other; +} diff --git a/client/platforms/windows/windowsnetworkwatcher.h b/client/platforms/windows/windowsnetworkwatcher.h new file mode 100644 index 00000000..29b99808 --- /dev/null +++ b/client/platforms/windows/windowsnetworkwatcher.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 WINDOWSNETWORKWATCHER_H +#define WINDOWSNETWORKWATCHER_H + +#include +#include + +#include "networkwatcherimpl.h" + +class WindowsNetworkWatcher final : public NetworkWatcherImpl { + public: + WindowsNetworkWatcher(QObject* parent); + ~WindowsNetworkWatcher(); + + void initialize() override; + + NetworkWatcherImpl::TransportType getTransportType() override; + + private: + static void wlanCallback(PWLAN_NOTIFICATION_DATA data, PVOID context); + + void processWlan(PWLAN_NOTIFICATION_DATA data); + + private: + // The handle is set during the initialization. Windows calls processWlan() + // to inform about network changes. + HANDLE m_wlanHandle = nullptr; + QString m_lastBSSID; +}; + +#endif // WINDOWSNETWORKWATCHER_H diff --git a/client/platforms/windows/windowspingsender.cpp b/client/platforms/windows/windowspingsender.cpp new file mode 100644 index 00000000..ad78ad31 --- /dev/null +++ b/client/platforms/windows/windowspingsender.cpp @@ -0,0 +1,133 @@ +/* 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 "windowspingsender.h" + +#include +#include +#include +// Note: This important must come after the previous three. +// clang-format off +#include +// clang-format on + +#include + +#include "leakdetector.h" +#include "logger.h" +#include "windowscommons.h" +#include "platforms/windows/windowsutils.h" + +#pragma comment(lib, "Ws2_32") + +constexpr WORD WindowsPingPayloadSize = sizeof(quint16); + +struct WindowsPingSenderPrivate { + HANDLE m_handle; + HANDLE m_event; + unsigned char m_buffer[sizeof(ICMP_ECHO_REPLY) + WindowsPingPayloadSize + 8]; +}; + +namespace { +Logger logger("WindowsPingSender"); +} + +static DWORD icmpCleanupHelper(LPVOID data) { + struct WindowsPingSenderPrivate* p = (struct WindowsPingSenderPrivate*)data; + if (p->m_event != INVALID_HANDLE_VALUE) { + CloseHandle(p->m_event); + } + if (p->m_handle != INVALID_HANDLE_VALUE) { + IcmpCloseHandle(p->m_handle); + } + delete p; + return 0; +} + +WindowsPingSender::WindowsPingSender(const QHostAddress& source, + QObject* parent) + : PingSender(parent) { + MZ_COUNT_CTOR(WindowsPingSender); + m_source = source; + m_private = new struct WindowsPingSenderPrivate; + m_private->m_handle = IcmpCreateFile(); + m_private->m_event = CreateEventA(NULL, FALSE, FALSE, NULL); + + m_notifier = new QWinEventNotifier(m_private->m_event, this); + QObject::connect(m_notifier, &QWinEventNotifier::activated, this, + &WindowsPingSender::pingEventReady); + + memset(m_private->m_buffer, 0, sizeof(m_private->m_buffer)); +} + +WindowsPingSender::~WindowsPingSender() { + MZ_COUNT_DTOR(WindowsPingSender); + if (m_notifier) { + delete m_notifier; + } + // Closing the ICMP handle can hang if there are lost ping replies. Moving + // the cleanup into a separate thread avoids deadlocking the application. + HANDLE h = CreateThread(NULL, 0, icmpCleanupHelper, m_private, 0, NULL); + if (h == NULL) { + icmpCleanupHelper(m_private); + } else { + CloseHandle(h); + } +} + +void WindowsPingSender::sendPing(const QHostAddress& dest, quint16 sequence) { + if (m_private->m_handle == INVALID_HANDLE_VALUE) { + return; + } + if (m_private->m_event == INVALID_HANDLE_VALUE) { + return; + } + + quint32 v4dst = dest.toIPv4Address(); + if (m_source.isNull()) { + IcmpSendEcho2(m_private->m_handle, m_private->m_event, nullptr, nullptr, + qToBigEndian(v4dst), &sequence, sizeof(sequence), + nullptr, m_private->m_buffer, sizeof(m_private->m_buffer), + 10000); + } else { + quint32 v4src = m_source.toIPv4Address(); + IcmpSendEcho2Ex(m_private->m_handle, m_private->m_event, nullptr, nullptr, + qToBigEndian(v4src), qToBigEndian(v4dst), + &sequence, sizeof(sequence), nullptr, m_private->m_buffer, + sizeof(m_private->m_buffer), 10000); + } + + DWORD status = GetLastError(); + if (status != ERROR_IO_PENDING) { + QString errmsg = WindowsUtils::getErrorMessage(); + logger.error() << "failed to start Code: " << status + << " Message: " << errmsg + << " dest:" << logger.sensitive(dest.toString()); + } +} + +void WindowsPingSender::pingEventReady() { + DWORD replyCount = + IcmpParseReplies(m_private->m_buffer, sizeof(m_private->m_buffer)); + if (replyCount == 0) { + DWORD error = GetLastError(); + if (error == IP_REQ_TIMED_OUT) { + return; + } + QString errmsg = WindowsUtils::getErrorMessage(); + logger.error() << "No ping reply. Code: " << error + << " Message: " << errmsg; + return; + } + + const ICMP_ECHO_REPLY* replies = (const ICMP_ECHO_REPLY*)m_private->m_buffer; + for (DWORD i = 0; i < replyCount; i++) { + if (replies[i].DataSize < sizeof(quint16)) { + continue; + } + quint16 sequence; + memcpy(&sequence, replies[i].Data, sizeof(quint16)); + emit recvPing(sequence); + } +} diff --git a/client/platforms/windows/windowspingsender.h b/client/platforms/windows/windowspingsender.h new file mode 100644 index 00000000..d091cddb --- /dev/null +++ b/client/platforms/windows/windowspingsender.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 WINDOWSPINGSENDER_H +#define WINDOWSPINGSENDER_H + +#include +#include + +#include "pingsender.h" + +struct WindowsPingSenderPrivate; + +class WindowsPingSender final : public PingSender { + Q_OBJECT + Q_DISABLE_COPY_MOVE(WindowsPingSender) + + public: + WindowsPingSender(const QHostAddress& source, QObject* parent = nullptr); + ~WindowsPingSender(); + + void sendPing(const QHostAddress& destination, quint16 sequence) override; + + private slots: + void pingEventReady(); + + private: + QHostAddress m_source; + QWinEventNotifier* m_notifier = nullptr; + struct WindowsPingSenderPrivate* m_private = nullptr; +}; + +#endif // WINDOWSPINGSENDER_H diff --git a/client/platforms/windows/windowsservicemanager.cpp b/client/platforms/windows/windowsservicemanager.cpp new file mode 100644 index 00000000..3a334224 --- /dev/null +++ b/client/platforms/windows/windowsservicemanager.cpp @@ -0,0 +1,139 @@ +/* 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 "windowsservicemanager.h" + +#include + +#include "Windows.h" +#include "Winsvc.h" +#include "logger.h" +//#include "mozillavpn.h" +#include "platforms/windows/windowsutils.h" + +namespace { +Logger logger("WindowsServiceManager"); +} + +WindowsServiceManager::WindowsServiceManager(LPCWSTR serviceName) { + DWORD err = NULL; + auto scm_rights = SC_MANAGER_CONNECT | SC_MANAGER_ENUMERATE_SERVICE | + SC_MANAGER_QUERY_LOCK_STATUS | STANDARD_RIGHTS_READ; + m_serviceManager = OpenSCManager(NULL, // local computer + NULL, // servicesActive database + scm_rights); + err = GetLastError(); + if (err != NULL) { + logger.error() << " OpenSCManager failed code: " << err; + return; + } + logger.debug() << "OpenSCManager access given - " << err; + + logger.debug() << "Opening Service - " + << QString::fromWCharArray(serviceName); + // Try to get an elevated handle + m_service = OpenService(m_serviceManager, // SCM database + serviceName, // name of service + (GENERIC_READ | SERVICE_START | SERVICE_STOP)); + err = GetLastError(); + if (err != NULL) { + WindowsUtils::windowsLog("OpenService failed"); + return; + } + m_has_access = true; + m_timer.setSingleShot(false); + + logger.debug() << "Service manager execute access granted"; +} + +WindowsServiceManager::~WindowsServiceManager() { + if (m_service != NULL) { + CloseServiceHandle(m_service); + } + if (m_serviceManager != NULL) { + CloseServiceHandle(m_serviceManager); + } +} + +bool WindowsServiceManager::startPolling(DWORD goal_state, int max_wait_sec) { + int tries = 0; + while (tries < max_wait_sec) { + SERVICE_STATUS status; + if (!QueryServiceStatus(m_service, &status)) { + WindowsUtils::windowsLog("Failed to retrieve the service status"); + return false; + } + + if (status.dwCurrentState == goal_state) { + if (status.dwCurrentState == SERVICE_RUNNING) { + emit serviceStarted(); + } + if (status.dwCurrentState == SERVICE_STOPPED) { + emit serviceStopped(); + } + return true; + } + + logger.debug() << "Polling Status" << m_state_target + << "wanted, has: " << status.dwCurrentState; + Sleep(1000); + ++tries; + } + return false; +} + +SERVICE_STATUS_PROCESS WindowsServiceManager::getStatus() { + SERVICE_STATUS_PROCESS serviceStatus; + if (!m_has_access) { + logger.debug() << "Need read access to get service state"; + return serviceStatus; + } + DWORD dwBytesNeeded; // Contains missing bytes if struct is too small? + QueryServiceStatusEx(m_service, // handle to service + SC_STATUS_PROCESS_INFO, // information level + (LPBYTE)&serviceStatus, // address of structure + sizeof(SERVICE_STATUS_PROCESS), // size of structure + &dwBytesNeeded); + return serviceStatus; +} + +bool WindowsServiceManager::startService() { + auto state = getStatus().dwCurrentState; + if (state != SERVICE_STOPPED && state != SERVICE_STOP_PENDING) { + logger.warning() << ("Service start not possible, as its running"); + emit serviceStarted(); + return true; + } + + bool ok = StartService(m_service, // handle to service + 0, // number of arguments + NULL); // no arguments + if (ok) { + logger.debug() << ("Service start requested"); + startPolling(SERVICE_RUNNING, 30); + } else { + WindowsUtils::windowsLog("StartService failed"); + } + return ok; +} + +bool WindowsServiceManager::stopService() { + if (!m_has_access) { + logger.error() << "Need execute access to stop services"; + return false; + } + auto state = getStatus().dwCurrentState; + if (state != SERVICE_RUNNING && state != SERVICE_START_PENDING) { + logger.warning() << ("Service stop not possible, as its not running"); + } + + bool ok = ControlService(m_service, SERVICE_CONTROL_STOP, NULL); + if (ok) { + logger.debug() << ("Service stop requested"); + startPolling(SERVICE_STOPPED, 10); + } else { + WindowsUtils::windowsLog("StopService failed"); + } + return ok; +} diff --git a/client/platforms/windows/windowsservicemanager.h b/client/platforms/windows/windowsservicemanager.h new file mode 100644 index 00000000..e0709309 --- /dev/null +++ b/client/platforms/windows/windowsservicemanager.h @@ -0,0 +1,60 @@ +/* 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 WINDOWSSERVICEMANAGER +#define WINDOWSSERVICEMANAGER + +#include +#include + +#include "Windows.h" +#include "Winsvc.h" + +/** + * @brief The WindowsServiceManager provides controll over the MozillaVPNBroker + * service via SCM + */ +class WindowsServiceManager : public QObject { + Q_OBJECT + Q_DISABLE_COPY_MOVE(WindowsServiceManager) + + public: + WindowsServiceManager(LPCWSTR serviceName); + ~WindowsServiceManager(); + + // true if the Service is running + bool isRunning() { return getStatus().dwCurrentState == SERVICE_RUNNING; }; + + // Starts the service if execute rights are present + // Starts to poll for serviceStarted + bool startService(); + + // Stops the service if execute rights are present. + // Starts to poll for serviceStopped + bool stopService(); + + signals: + // Gets Emitted after the Service moved From SERVICE_START_PENDING to + // SERVICE_RUNNING + void serviceStarted(); + void serviceStopped(); + + private: + // Returns the State of the Process: + // See + // SERVICE_STOPPED,SERVICE_STOP_PENDING,SERVICE_START_PENDING,SERVICE_RUNNING + SERVICE_STATUS_PROCESS getStatus(); + bool m_has_access = false; + LPWSTR m_serviceName; + SC_HANDLE m_serviceManager; + SC_HANDLE m_service; // Service handle with r/w priv. + DWORD m_state_target; + int m_currentWaitTime; + int m_maxWaitTime; + QTimer m_timer; + + bool startPolling(DWORD goal_state, int maxS); +}; + +#endif // WINDOWSSERVICEMANAGER diff --git a/client/platforms/windows/windowsutils.cpp b/client/platforms/windows/windowsutils.cpp new file mode 100644 index 00000000..c593110b --- /dev/null +++ b/client/platforms/windows/windowsutils.cpp @@ -0,0 +1,62 @@ +/* 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 "windowsutils.h" + +#include +#include + +#include +#include + +#include "logger.h" + +namespace { +Logger logger("WindowsUtils"); +} // namespace + +constexpr const int WINDOWS_11_BUILD = + 22000; // Build Number of the first release win 11 iso + +QString WindowsUtils::getErrorMessage(quint32 code) { + LPSTR messageBuffer = nullptr; + size_t size = FormatMessageA( + FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, + nullptr, code, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + (LPSTR)&messageBuffer, 0, nullptr); + + std::string message(messageBuffer, size); + QString result(message.c_str()); + LocalFree(messageBuffer); + return result; +} + +QString WindowsUtils::getErrorMessage() { + return getErrorMessage(GetLastError()); +} + +// A simple function to log windows error messages. +void WindowsUtils::windowsLog(const QString& msg) { + QString errmsg = getErrorMessage(); + logger.error() << msg << "-" << errmsg; +} + +// Static +QString WindowsUtils::windowsVersion() { + QSettings regCurrentVersion( + "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", + QSettings::NativeFormat); + + int buildNr = regCurrentVersion.value("CurrentBuild").toInt(); + if (buildNr >= WINDOWS_11_BUILD) { + return "11"; + } + return QSysInfo::productVersion(); +} + +// static +void WindowsUtils::forceCrash() { + RaiseException(0x0000DEAD, EXCEPTION_NONCONTINUABLE, 0, NULL); +} diff --git a/client/platforms/windows/windowsutils.h b/client/platforms/windows/windowsutils.h new file mode 100644 index 00000000..d46b44cf --- /dev/null +++ b/client/platforms/windows/windowsutils.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 WINDOWSUTILS_H +#define WINDOWSUTILS_H + +#include + +class WindowsUtils final { + public: + static QString getErrorMessage(); + static QString getErrorMessage(quint32 code); + static void windowsLog(const QString& msg); + + // Returns the major version of Windows + static QString windowsVersion(); + + // Force an application crash for testing + static void forceCrash(); +}; + +#endif // WINDOWSUTILS_H diff --git a/client/protocols/wireguardprotocol.cpp b/client/protocols/wireguardprotocol.cpp index 2ba2b218..3091655d 100644 --- a/client/protocols/wireguardprotocol.cpp +++ b/client/protocols/wireguardprotocol.cpp @@ -18,7 +18,7 @@ WireguardProtocol::WireguardProtocol(const QJsonObject &configuration, QObject* // MZ #if defined(MZ_LINUX) //m_impl.reset(new LinuxController()); -#elif defined(MZ_MACOS) // || defined(MZ_WINDOWS) +#elif defined(Q_OS_MAC) || defined(Q_OS_WIN) m_impl.reset(new LocalSocketController()); connect(m_impl.get(), &ControllerImpl::connected, this, [this](const QString& pubkey, const QDateTime& connectionTimestamp) { emit connectionStateChanged(VpnProtocol::Connected); @@ -38,7 +38,7 @@ WireguardProtocol::~WireguardProtocol() void WireguardProtocol::stop() { -#ifdef Q_OS_MAC +#if defined(Q_OS_MAC) || defined(Q_OS_WIN) stopMzImpl(); return; #endif @@ -98,9 +98,11 @@ void WireguardProtocol::stop() setConnectionState(VpnProtocol::Disconnected); } -#ifdef Q_OS_MAC +#if defined(Q_OS_MAC) || defined(Q_OS_WIN) ErrorCode WireguardProtocol::startMzImpl() { + + qDebug() << "WireguardProtocol::startMzImpl():" << m_rawConfig; m_impl->activate(m_rawConfig); return ErrorCode::NoError; } @@ -169,7 +171,7 @@ ErrorCode WireguardProtocol::start() return lastError(); } -#ifdef Q_OS_MAC +#if defined(Q_OS_MAC) || defined(Q_OS_WIN) return startMzImpl(); #endif diff --git a/client/protocols/wireguardprotocol.h b/client/protocols/wireguardprotocol.h index 8f6bad9a..6f530758 100644 --- a/client/protocols/wireguardprotocol.h +++ b/client/protocols/wireguardprotocol.h @@ -23,7 +23,7 @@ public: ErrorCode start() override; void stop() override; -#ifdef Q_OS_MAC +#if defined(Q_OS_MAC) || defined(Q_OS_WIN) ErrorCode startMzImpl(); ErrorCode stopMzImpl(); #endif @@ -47,7 +47,7 @@ private: bool m_isConfigLoaded = false; -#ifdef Q_OS_MAC +#if defined(Q_OS_MAC) || defined(Q_OS_WIN) QScopedPointer m_impl; #endif }; diff --git a/service/CMakeLists.txt b/service/CMakeLists.txt index cfb3beb2..f05dbb23 100644 --- a/service/CMakeLists.txt +++ b/service/CMakeLists.txt @@ -9,7 +9,3 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) if(NOT IOS AND NOT ANDROID) add_subdirectory(server) endif() - -if(WIN32) - add_subdirectory(wireguard-service) -endif() diff --git a/service/server/CMakeLists.txt b/service/server/CMakeLists.txt index 5435b80e..72c5b09b 100644 --- a/service/server/CMakeLists.txt +++ b/service/server/CMakeLists.txt @@ -6,7 +6,7 @@ project(${PROJECT}) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) -find_package(Qt6 REQUIRED COMPONENTS Core Network RemoteObjects Core5Compat) +find_package(Qt6 REQUIRED COMPONENTS Core Network Widgets RemoteObjects Core5Compat) qt_standard_project_setup() configure_file(${CMAKE_SOURCE_DIR}/version.h.in ${CMAKE_CURRENT_BINARY_DIR}/version.h) @@ -50,7 +50,7 @@ set(HEADERS ${HEADERS} ${CMAKE_CURRENT_LIST_DIR}/../../client/mozilla/models/server.h ${CMAKE_CURRENT_LIST_DIR}/../../client/mozilla/controllerimpl.h - ${CMAKE_CURRENT_LIST_DIR}/../../client/mozilla/dnspingsender.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../client/mozilla/dnspingsender.h ${CMAKE_CURRENT_LIST_DIR}/../../client/mozilla/localsocketcontroller.h ${CMAKE_CURRENT_LIST_DIR}/../../client/mozilla/networkwatcher.h ${CMAKE_CURRENT_LIST_DIR}/../../client/mozilla/networkwatcherimpl.h @@ -69,7 +69,8 @@ set(SOURCES ${SOURCES} ${CMAKE_CURRENT_LIST_DIR}/../../client/mozilla/models/server.cpp ${CMAKE_CURRENT_LIST_DIR}/../../client/platforms/dummy/dummynetworkwatcher.cpp - + + ${CMAKE_CURRENT_LIST_DIR}/../../client/daemon/interfaceconfig.cpp ${CMAKE_CURRENT_LIST_DIR}/../../client/mozilla/shared/ipaddress.cpp ${CMAKE_CURRENT_LIST_DIR}/../../client/mozilla/shared/leakdetector.cpp @@ -107,11 +108,39 @@ if(WIN32) set(HEADERS ${HEADERS} ${CMAKE_CURRENT_LIST_DIR}/tapcontroller_win.h ${CMAKE_CURRENT_LIST_DIR}/router_win.h + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/windows/daemon/windowsdaemon.h + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/windows/daemon/windowsdaemontunnel.h + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/windows/daemon/windowsfirewall.h + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/windows/windowscommons.h + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/windows/windowsservicemanager.h + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/windows/daemon/dnsutilswindows.h + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/windows/daemon/windowssplittunnel.h + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/windows/daemon/windowstunnelservice.h + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/windows/daemon/wireguardutilswindows.h + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/windows/daemon/windowstunnellogger.h + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/windows/daemon/windowsroutemonitor.h + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/windows/windowsutils.h + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/windows/windowspingsender.h + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/windows/windowsnetworkwatcher.h ) set(SOURCES ${SOURCES} ${CMAKE_CURRENT_LIST_DIR}/tapcontroller_win.cpp ${CMAKE_CURRENT_LIST_DIR}/router_win.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/windows/daemon/windowsdaemon.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/windows/daemon/windowsdaemontunnel.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/windows/daemon/windowsfirewall.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/windows/windowscommons.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/windows/windowsservicemanager.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/windows/daemon/dnsutilswindows.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/windows/daemon/windowssplittunnel.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/windows/daemon/windowstunnelservice.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/windows/daemon/wireguardutilswindows.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/windows/daemon/windowstunnellogger.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/windows/daemon/windowsroutemonitor.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/windows/windowspingsender.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/windows/windowsnetworkwatcher.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/windows/windowsutils.cpp ) set(LIBS @@ -188,7 +217,7 @@ include_directories( ) add_executable(${PROJECT} ${SOURCES} ${HEADERS}) -target_link_libraries(${PROJECT} PRIVATE Qt6::Core Qt6::Network Qt6::RemoteObjects Qt6::Core5Compat ${LIBS}) +target_link_libraries(${PROJECT} PRIVATE Qt6::Core Qt6::Widgets Qt6::Network Qt6::RemoteObjects Qt6::Core5Compat ${LIBS}) target_compile_definitions(${PROJECT} PRIVATE "MZ_$") if(CMAKE_BUILD_TYPE STREQUAL "Debug") diff --git a/service/server/localserver.cpp b/service/server/localserver.cpp index 4b375d26..709ad693 100644 --- a/service/server/localserver.cpp +++ b/service/server/localserver.cpp @@ -15,7 +15,7 @@ #endif namespace { -Logger logger("MacOSDaemonServer"); +Logger logger("WgDaemonServer"); } LocalServer::LocalServer(QObject *parent) : QObject(parent), @@ -40,17 +40,25 @@ LocalServer::LocalServer(QObject *parent) : QObject(parent), } }); +#if defined(Q_OS_MAC) || defined(Q_OS_WIN) // Init Mozilla Wireguard Daemon -#ifdef Q_OS_MAC if (!server.initialize()) { logger.error() << "Failed to initialize the server"; return; } +#endif +#ifdef Q_OS_MAC // Signal handling for a proper shutdown. QObject::connect(qApp, &QCoreApplication::aboutToQuit, []() { MacOSDaemon::instance()->deactivate(); }); #endif + +#ifdef Q_OS_WIN + // Signal handling for a proper shutdown. + QObject::connect(qApp, &QCoreApplication::aboutToQuit, + []() { WindowsDaemon::instance()->deactivate(); }); +#endif } LocalServer::~LocalServer() diff --git a/service/server/localserver.h b/service/server/localserver.h index e4217cc0..b5264120 100644 --- a/service/server/localserver.h +++ b/service/server/localserver.h @@ -10,9 +10,14 @@ #include "ipcserver.h" -#ifdef Q_OS_MAC -#include "macos/daemon/macosdaemon.h" +#ifdef Q_OS_WIN #include "../../client/daemon/daemonlocalserver.h" +#include "windows/daemon/windowsdaemon.h" +#endif + +#ifdef Q_OS_MAC +#include "../../client/daemon/daemonlocalserver.h" +#include "macos/daemon/macosdaemon.h" #endif class QLocalServer; @@ -33,9 +38,13 @@ public: QRemoteObjectHost m_serverNode; bool m_isRemotingEnabled = false; -#ifdef Q_OS_MAC - MacOSDaemon daemon; +#ifdef Q_OS_WIN DaemonLocalServer server{qApp}; + WindowsDaemon daemon; +#endif +#ifdef Q_OS_MAC + DaemonLocalServer server{qApp}; + MacOSDaemon daemon; #endif }; diff --git a/service/server/main.cpp b/service/server/main.cpp index 8834b088..495192c3 100644 --- a/service/server/main.cpp +++ b/service/server/main.cpp @@ -6,13 +6,39 @@ #include "systemservice.h" #include "utilities.h" +#ifdef Q_OS_WIN +#include "platforms/windows/daemon/windowsdaemontunnel.h" + +namespace { +int s_argc = 0; +char** s_argv = nullptr; +} // namespace + +#endif int runApplication(int argc, char** argv) { QCoreApplication app(argc,argv); - LocalServer localServer; +#ifdef Q_OS_WIN + if(argc > 2){ + s_argc = argc; + s_argv = argv; + QStringList tokens; + for (int i = 1; i < argc; ++i) { + tokens.append(QString(argv[i])); + } + + if (!tokens.empty() && tokens[0] == "tunneldaemon") { + WindowsDaemonTunnel *daemon = new WindowsDaemonTunnel(); + daemon->run(tokens); + } + } +#endif + + LocalServer localServer; return app.exec(); + } @@ -22,7 +48,7 @@ int main(int argc, char **argv) Logger::init(); - if (argc == 2) { + if (argc >= 2) { qInfo() << "Started as console application"; return runApplication(argc, argv); } diff --git a/service/server/systemservice.cpp b/service/server/systemservice.cpp index 8af7dcb8..43a93d8f 100644 --- a/service/server/systemservice.cpp +++ b/service/server/systemservice.cpp @@ -2,10 +2,39 @@ #include "localserver.h" #include "systemservice.h" + +#ifdef Q_OS_WIN +#include "platforms/windows/daemon/windowsdaemontunnel.h" + +namespace { +int s_argc = 0; +char** s_argv = nullptr; +} // namespace +#endif + SystemService::SystemService(int argc, char **argv) : QtService(argc, argv, SERVICE_NAME) { setServiceDescription("Service for AmneziaVPN"); + +#ifdef Q_OS_WIN + if(argc > 2){ + s_argc = argc; + s_argv = argv; + QStringList tokens; + + for (int i = 1; i < argc; ++i) { + tokens.append(QString(argv[i])); + } + + if (!tokens.empty() && tokens[0] == "tunneldaemon") { + WindowsDaemonTunnel *daemon = new WindowsDaemonTunnel(); + daemon->run(tokens); + } + + } +#endif + } void SystemService::start() diff --git a/service/wireguard-service/CMakeLists.txt b/service/wireguard-service/CMakeLists.txt deleted file mode 100644 index 33a3d584..00000000 --- a/service/wireguard-service/CMakeLists.txt +++ /dev/null @@ -1,34 +0,0 @@ -cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR) - -set(PROJECT wireguard-service) -project(${PROJECT} LANGUAGES CXX) - -set(CMAKE_CXX_STANDARD 20) -set(CMAKE_CXX_STANDARD_REQUIRED ON) - -find_package(Qt6 REQUIRED COMPONENTS Core) -qt_standard_project_setup() - -set(SOURCES - ${CMAKE_CURRENT_LIST_DIR}/main.cpp - ${CMAKE_CURRENT_LIST_DIR}/wireguardtunnelservice.cpp -) - -set(HEADERS - ${CMAKE_CURRENT_LIST_DIR}/wireguardtunnelservice.h -) - -set(LIBS - user32 - rasapi32 - shlwapi - iphlpapi - ws2_32 - iphlpapi - gdi32 - Advapi32 - Kernel32 -) - -add_executable(${PROJECT} ${SOURCES} ${HEADERS}) -target_link_libraries(${PROJECT} PRIVATE Qt6::Core ${LIBS}) diff --git a/service/wireguard-service/main.cpp b/service/wireguard-service/main.cpp deleted file mode 100644 index 8e5f231e..00000000 --- a/service/wireguard-service/main.cpp +++ /dev/null @@ -1,31 +0,0 @@ -#include "wireguardtunnelservice.h" -#include -#include - -int wmain(int argc, wchar_t** argv) -{ - if (argc != 3) { - debug_log(L"Wrong argument provided"); - return 1; - } - TCHAR option[20]; - TCHAR configFile[5000]; - - StringCchCopy(option, 20, argv[1]); - StringCchCopy(configFile, 5000, argv[2]); - - WireguardTunnelService tunnel(configFile); - - if (lstrcmpi(option, TEXT("--run")) == 0) { - debug_log(L"start tunnel"); - tunnel.startTunnel(); - } else if (lstrcmpi(option, TEXT("--add")) == 0) { - tunnel.addService(); - } else if (lstrcmpi(option, TEXT("--remove")) == 0) { - tunnel.removeService(); - } else { - debug_log(L"Wrong argument provided"); - return 1; - } - return 0; -} diff --git a/service/wireguard-service/wireguardtunnelservice.cpp b/service/wireguard-service/wireguardtunnelservice.cpp deleted file mode 100644 index 9864038e..00000000 --- a/service/wireguard-service/wireguardtunnelservice.cpp +++ /dev/null @@ -1,160 +0,0 @@ -#include "wireguardtunnelservice.h" -#include -#include -#include -#include -#include -#include -#include - - -void debug_log(const std::wstring& msg) -{ - std::wcerr << msg << std::endl; -} - -WireguardTunnelService::WireguardTunnelService(const std::wstring& configFile): - m_configFile{configFile} -{ -} - -void WireguardTunnelService::addService() -{ - SC_HANDLE scm; - SC_HANDLE service; - scm = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS); - if (NULL == scm) { - debug_log(L"OpenSCManager failed"); - return; - } - WCHAR szFileName[MAX_PATH]; - - GetModuleFileNameW(NULL, szFileName, MAX_PATH); - std::wstring runCommand = szFileName; - runCommand += TEXT(" --run "); - runCommand += m_configFile; - - debug_log(runCommand); - // check if service is already running - service = OpenServiceW( - scm, - SVCNAME, - SERVICE_ALL_ACCESS - ); - if (NULL != service) { - //service is already running, remove it before add new service - debug_log(L"service is already running, remove it before add new service"); - CloseServiceHandle(service); - removeService(); - } - service = CreateServiceW( - scm, - SVCNAME, - SVCNAME, - SERVICE_ALL_ACCESS, - SERVICE_WIN32_OWN_PROCESS, - SERVICE_DEMAND_START, - SERVICE_ERROR_NORMAL, - runCommand.c_str(), - NULL, - NULL, - TEXT("Nsi\0TcpIp"), - NULL, - NULL); - if (NULL == service) { - debug_log(L"CreateServiceW failed"); - CloseServiceHandle(scm); - return; - } - SERVICE_SID_INFO info; - info.dwServiceSidType = SERVICE_SID_TYPE_UNRESTRICTED; - if (ChangeServiceConfig2W(service, - SERVICE_CONFIG_SERVICE_SID_INFO, - &info) == 0) { - debug_log(L"ChangeServiceConfig2 failed"); - CloseServiceHandle(service); - CloseServiceHandle(scm); - return; - } - if (StartServiceW(service, 0, NULL) == 0) { - debug_log(L"StartServiceW failed"); - CloseServiceHandle(service); - CloseServiceHandle(scm); - return; - } - if (DeleteService(service) == 0) { - debug_log(L"DeleteService failed"); - CloseServiceHandle(service); - CloseServiceHandle(scm); - return; - } - CloseServiceHandle(service); - CloseServiceHandle(scm); -} - -void WireguardTunnelService::removeService() -{ - SC_HANDLE scm; - SC_HANDLE service; - scm = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS); - if (NULL == scm) { - debug_log(L"OpenSCManager failed"); - return; - } - service = OpenServiceW( - scm, - SVCNAME, - SERVICE_ALL_ACCESS - ); - if (NULL == service) { - debug_log(L"OpenServiceW failed"); - CloseServiceHandle(scm); - return; - } - SERVICE_STATUS stt; - if (ControlService(service, SERVICE_CONTROL_STOP, &stt) == 0) { - debug_log(L"ControlService failed"); - DeleteService(service); - CloseServiceHandle(service); - CloseServiceHandle(scm); - return; - } - for (int i = 0; - i < 180 && QueryServiceStatus(scm, &stt) && stt.dwCurrentState != SERVICE_STOPPED; - ++i) { - std::this_thread::sleep_for(std::chrono::seconds{1}); - } - DeleteService(service); - CloseServiceHandle(service); - CloseServiceHandle(scm); -} - - -int WireguardTunnelService::startTunnel() -{ - debug_log(TEXT(__FUNCTION__)); - - HMODULE tunnelLib = LoadLibrary(TEXT("tunnel.dll")); - if (!tunnelLib) { - debug_log(L"Failed to load tunnel.dll"); - return 1; - } - - typedef bool WireGuardTunnelService(const LPCWSTR settings); - - WireGuardTunnelService* tunnelProc = (WireGuardTunnelService*)GetProcAddress( - tunnelLib, "WireGuardTunnelService"); - if (!tunnelProc) { - debug_log(L"Failed to get WireGuardTunnelService function"); - return 1; - } - - debug_log(m_configFile.c_str()); - - if (!tunnelProc(m_configFile.c_str())) { - debug_log(L"Failed to activate the tunnel service"); - return 1; - } - return 0; -} - diff --git a/service/wireguard-service/wireguardtunnelservice.h b/service/wireguard-service/wireguardtunnelservice.h deleted file mode 100644 index 3afd64c0..00000000 --- a/service/wireguard-service/wireguardtunnelservice.h +++ /dev/null @@ -1,22 +0,0 @@ -#ifndef WIREGUARDTUNNELSERVICE_H -#define WIREGUARDTUNNELSERVICE_H - -#include -#include - -#define SVCNAME TEXT("AmneziaVPNWireGuardService") - -class WireguardTunnelService -{ -public: - WireguardTunnelService(const std::wstring& configFile); - void addService(); - void removeService(); - int startTunnel(); -private: - std::wstring m_configFile; -}; - -void debug_log(const std::wstring& msg); - -#endif // WIREGUARDTUNNELSERVICE_H From 96ffd7e1478abc79436a03a0080b18d430832252 Mon Sep 17 00:00:00 2001 From: Mykola Baibuz Date: Thu, 14 Sep 2023 23:44:57 +0300 Subject: [PATCH 2/9] Update Android icons --- client/android/res/drawable-hdpi/icon.png | Bin 8193 -> 3338 bytes client/android/res/drawable-ldpi/icon.png | Bin 3609 -> 1984 bytes client/android/res/drawable-mdpi/icon.png | Bin 5216 -> 3517 bytes client/android/res/drawable-xhdpi/icon.png | Bin 11628 -> 9958 bytes client/android/res/drawable-xxhdpi/icon.png | Bin 19686 -> 19030 bytes client/android/res/drawable-xxxhdpi/icon.png | Bin 28856 -> 30603 bytes 6 files changed, 0 insertions(+), 0 deletions(-) diff --git a/client/android/res/drawable-hdpi/icon.png b/client/android/res/drawable-hdpi/icon.png index 657c547cc8e270a9f357bf8b2b7dfd1df3a2df47..d0f203630e5ec2ec3b1985288cefe1b86bb047e7 100644 GIT binary patch delta 3327 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 diff --git a/client/android/res/drawable-ldpi/icon.png b/client/android/res/drawable-ldpi/icon.png index 2b0f2ad80cd6bfa921a9c02fc05da42af3b047a3..00b978f5ec255ca0c3fe63be100c481d11b850eb 100644 GIT binary patch delta 1963 zcmV;c2UPf(9Ka8dD}M_U000XU0RWnu7ytkO0drDELIAGL9O(c600d`2O+f$vv5yP< zVFdsH2UST#K~#7FZC7nY%(yV+D6;K=rp0x zPGy_=XO+4i{jsUrwEjW+wU0lWkhZB68`Zj1EJf-%w8A8W2!EnsX_EpWq=bgV4*8%- zoH$?hdwaj%`zXltP`(9n zC;w$l?BUaTPR(rar0~0f#gQm&ws1bL1ZkmE^$ENOcn&HRuHW)?K1#j9ZDaKuRn1jY zp76EjULkTF*MCBwLG?L$rGSL!v*jyo7~kiU==(HWq||y>Yq6c!Rz~8rsBdlOGaN1%A&a5w1L%40J(Na+ffoE7=ZYi53zWD5QUJuHCD#*vfk>FPruUMjUAxiT zxD&x}#Dh{QUV*Nv5GE$GIR2BDF_F!B5VWm_OK?ISeRP|K15#t{GpZDj6Ni6y0+B=l zAm-lZD3gG0&{L?RYdC-DA}(FMD*WG_-iJ-hiX-V(w)+7NH2d*v(>xMk;O;!gYC&bf5^|WF zz<;Bs9D-@>(DRQgQMlQ~8S?;-DlBr!}Q*8o^)3X>ja~29VvbB95)|QI6 zJ8;#Y_WGND!;g<2!>g~nKqfL%IDYg=(tl800XjF1;%o+EBP00H>%+(`>wE&`215M& z$wO9#zlc%s?C9Kqn#l*a_SV}>U|#4J4#yEKM3C*jhK1QlVeun%btDpt;hhVY(7AIL zUO42#!c+!t{Ph};&(UMM1YX(pOWx?1m}^u{2)5dZB>k z`Y)rdrj69Zwi-)k=SMLx(u3vIIewTfWavvzZy|?wRO5uY4yD=b;e19S#iM`;m z-nbL0VlrWwLw`JZJ3UaOu&!yB2gD^IS0ogSqj%sh!qUxK+L2CwA3Hmr zMm|4>K#1kD2csrh6bR7SL!)wfu*d}a@#jPG=#za8GC8we#lKrW_;sPd2Qzbbq*-h@ zF@BxixbZ&udiOr|CV@z375(fmRZ^Kfc9F_kHs_ku$H;fGuVB#9Wq%vbvQRN!7LUhY z<4CIPWY+TW z&6UKEOj_&U!kr?{4m*@7K~fDq{QjE(xJ8Yp&^2{|-|J6mm0>b5ZLe9!$n9~lequIH zCT4-2>o6BeLzG}~jgW;^M+uO>Od&EPkYIdTFp0;-SQjG6WPcO2{BB^O(rixMT%@>o~HquR`Un}=hk85oXloX8A z^U(>DY@k0*JWxW#we=~6Qn#x08-1)&pI&TRW~EtI2B`IosMknY)*e_WZKSOIttd+} x8@!Fl=*8M=i6mTRA?UHr5~c;JMC52x=;uLpcWDY&wJZPt002ovPDHLkV1k|N%%A`O literal 3609 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 diff --git a/client/android/res/drawable-mdpi/icon.png b/client/android/res/drawable-mdpi/icon.png index eb9f49adf29339c9891efa4faa40cf3b24416d2b..e23a94a990c8f398f80ef8b89f89b5c83ccacf42 100644 GIT binary patch delta 3508 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 i+CmzlK!au!r29V@+S6#l)yYo)0000@ literal 5216 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 diff --git a/client/android/res/drawable-xhdpi/icon.png b/client/android/res/drawable-xhdpi/icon.png index bd1cf1afd3a9ce3d4bc007f4b19356e906140c1d..0c5408f8dcb9c8603615103e29e3688f37417b68 100644 GIT binary patch literal 9958 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 literal 11628 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 Ee>$RcApigX diff --git a/client/android/res/drawable-xxhdpi/icon.png b/client/android/res/drawable-xxhdpi/icon.png index dc07319f1d3bbbf51675e77770ba7931fe3d04d8..0f94f39ef69eaf8098290b3d9992d9c648f8b0f1 100644 GIT binary patch literal 19030 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� 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 UuOkI@`v3p{07*qoM6N<$g6 Date: Fri, 15 Sep 2023 21:42:04 +0300 Subject: [PATCH 3/9] Android 8 support (#321) --- client/3rd-prebuilt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/3rd-prebuilt b/client/3rd-prebuilt index 66d39ba0..bc450df2 160000 --- a/client/3rd-prebuilt +++ b/client/3rd-prebuilt @@ -1 +1 @@ -Subproject commit 66d39ba0e294f65ed4933f01f9eedd56032610d3 +Subproject commit bc450df2418540869e97a31c4dd76839989b1fa6 From 279692afea9e8929d1364f0f6515de4dd68acd17 Mon Sep 17 00:00:00 2001 From: Mykola Baibuz Date: Sun, 17 Sep 2023 17:06:24 -0400 Subject: [PATCH 4/9] WireGuard rework for Linux --- client/daemon/daemonlocalserver.cpp | 11 +- client/mozilla/networkwatcher.cpp | 4 +- client/mozilla/shared/ipaddress.cpp | 2 +- .../platforms/linux/daemon/dbustypeslinux.h | 170 ++++++++ .../platforms/linux/daemon/dnsutilslinux.cpp | 212 ++++++++++ client/platforms/linux/daemon/dnsutilslinux.h | 41 ++ .../platforms/linux/daemon/iputilslinux.cpp | 150 +++++++ client/platforms/linux/daemon/iputilslinux.h | 31 ++ client/platforms/linux/daemon/linuxdaemon.cpp | 52 +++ client/platforms/linux/daemon/linuxdaemon.h | 36 ++ .../linux/daemon/linuxroutemonitor.cpp | 133 +++++++ .../linux/daemon/linuxroutemonitor.h | 53 +++ client/platforms/linux/daemon/pidtracker.cpp | 228 +++++++++++ client/platforms/linux/daemon/pidtracker.h | 72 ++++ .../linux/daemon/wireguardutilslinux.cpp | 369 ++++++++++++++++++ .../linux/daemon/wireguardutilslinux.h | 55 +++ client/platforms/linux/interfaceconfig.h | 47 +++ client/platforms/linux/linuxdependencies.cpp | 139 +++++++ client/platforms/linux/linuxdependencies.h | 25 ++ .../platforms/linux/linuxnetworkwatcher.cpp | 57 +++ client/platforms/linux/linuxnetworkwatcher.h | 38 ++ .../linux/linuxnetworkwatcherworker.cpp | 177 +++++++++ .../linux/linuxnetworkwatcherworker.h | 41 ++ client/protocols/wireguardprotocol.cpp | 12 +- client/protocols/wireguardprotocol.h | 4 +- service/server/CMakeLists.txt | 27 +- service/server/localserver.cpp | 6 +- service/server/localserver.h | 16 +- service/server/router_linux.cpp | 146 ++++++- service/server/router_linux.h | 1 + 30 files changed, 2319 insertions(+), 36 deletions(-) create mode 100644 client/platforms/linux/daemon/dbustypeslinux.h create mode 100644 client/platforms/linux/daemon/dnsutilslinux.cpp create mode 100644 client/platforms/linux/daemon/dnsutilslinux.h create mode 100644 client/platforms/linux/daemon/iputilslinux.cpp create mode 100644 client/platforms/linux/daemon/iputilslinux.h create mode 100644 client/platforms/linux/daemon/linuxdaemon.cpp create mode 100644 client/platforms/linux/daemon/linuxdaemon.h create mode 100644 client/platforms/linux/daemon/linuxroutemonitor.cpp create mode 100644 client/platforms/linux/daemon/linuxroutemonitor.h create mode 100644 client/platforms/linux/daemon/pidtracker.cpp create mode 100644 client/platforms/linux/daemon/pidtracker.h create mode 100644 client/platforms/linux/daemon/wireguardutilslinux.cpp create mode 100644 client/platforms/linux/daemon/wireguardutilslinux.h create mode 100644 client/platforms/linux/interfaceconfig.h create mode 100644 client/platforms/linux/linuxdependencies.cpp create mode 100644 client/platforms/linux/linuxdependencies.h create mode 100644 client/platforms/linux/linuxnetworkwatcher.cpp create mode 100644 client/platforms/linux/linuxnetworkwatcher.h create mode 100644 client/platforms/linux/linuxnetworkwatcherworker.cpp create mode 100644 client/platforms/linux/linuxnetworkwatcherworker.h diff --git a/client/daemon/daemonlocalserver.cpp b/client/daemon/daemonlocalserver.cpp index 02a12cb9..9d8feb68 100644 --- a/client/daemon/daemonlocalserver.cpp +++ b/client/daemon/daemonlocalserver.cpp @@ -12,7 +12,7 @@ #include "leakdetector.h" #include "logger.h" -#ifdef MZ_MACOS +#if defined(MZ_MACOS) || defined(MZ_LINUX) # include # include # include @@ -68,7 +68,8 @@ bool DaemonLocalServer::initialize() { QString DaemonLocalServer::daemonPath() const { #if defined(MZ_WINDOWS) return "\\\\.\\pipe\\amneziavpn"; -#elif defined(MZ_MACOS) +#endif +#if defined(MZ_MACOS) || defined(MZ_LINUX) QDir dir("/var/run"); if (!dir.exists()) { logger.warning() << "/var/run doesn't exist. Fallback /tmp."; @@ -76,12 +77,12 @@ QString DaemonLocalServer::daemonPath() const { } if (dir.exists("amneziavpn")) { - logger.debug() << "/var/run/amneziavpn seems to be usable"; + logger.debug() << "/var/run/amnezia seems to be usable"; return VAR_PATH; } if (!dir.mkdir("amneziavpn")) { - logger.warning() << "Failed to create /var/run/amneziavpn"; + logger.warning() << "Failed to create /var/run/amnezia"; return TMP_PATH; } @@ -92,7 +93,5 @@ QString DaemonLocalServer::daemonPath() const { } return VAR_PATH; -#else -# error Unsupported platform #endif } diff --git a/client/mozilla/networkwatcher.cpp b/client/mozilla/networkwatcher.cpp index 54beb11c..47fdb622 100644 --- a/client/mozilla/networkwatcher.cpp +++ b/client/mozilla/networkwatcher.cpp @@ -19,7 +19,7 @@ #endif #ifdef MZ_LINUX -//# include "platforms/linux/linuxnetworkwatcher.h" +# include "platforms/linux/linuxnetworkwatcher.h" #endif #ifdef MZ_MACOS @@ -56,7 +56,7 @@ void NetworkWatcher::initialize() { #if defined(MZ_WINDOWS) m_impl = new WindowsNetworkWatcher(this); #elif defined(MZ_LINUX) -// m_impl = new LinuxNetworkWatcher(this); + m_impl = new LinuxNetworkWatcher(this); #elif defined(MZ_MACOS) m_impl = new MacOSNetworkWatcher(this); #elif defined(MZ_WASM) diff --git a/client/mozilla/shared/ipaddress.cpp b/client/mozilla/shared/ipaddress.cpp index 7c337755..1f84ad07 100644 --- a/client/mozilla/shared/ipaddress.cpp +++ b/client/mozilla/shared/ipaddress.cpp @@ -30,7 +30,7 @@ IPAddress::IPAddress(const QString& ip) { m_prefixLength = 128; } } else { - Q_ASSERT(false); + // Q_ASSERT(false); } } diff --git a/client/platforms/linux/daemon/dbustypeslinux.h b/client/platforms/linux/daemon/dbustypeslinux.h new file mode 100644 index 00000000..1a5e44e2 --- /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 00000000..cc47202b --- /dev/null +++ b/client/platforms/linux/daemon/dnsutilslinux.cpp @@ -0,0 +1,212 @@ +/* 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 + +#include +#include + +#include "leakdetector.h" +#include "logger.h" + +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("DnsUtilsLinux"); +} + +DnsUtilsLinux::DnsUtilsLinux(QObject* parent) : DnsUtils(parent) { + MZ_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() { + MZ_COUNT_DTOR(DnsUtilsLinux); + + for (auto iterator = m_linkDomains.constBegin(); + iterator != m_linkDomains.constEnd(); ++iterator) { + QList argumentList; + argumentList << QVariant::fromValue(iterator.key()); + argumentList << QVariant::fromValue(iterator.value()); + 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 iterator = m_linkDomains.constBegin(); + iterator != m_linkDomains.constEnd(); ++iterator) { + setLinkDomains(iterator.key(), iterator.value()); + } + 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 (const 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 (const auto& d : domains) { + // The DNS search domains often winds up revealing user's ISP which + // can correlate back to their location. + logger.debug() << "Setting DNS domain:" << logger.sensitive(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 (const 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 iterator = m_linkDomains.constBegin(); + iterator != m_linkDomains.constEnd(); ++iterator) { + if (!iterator.value().contains(root)) { + continue; + } + QList newlist = iterator.value(); + newlist.removeAll(root); + setLinkDomains(iterator.key(), 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 00000000..e4bbd273 --- /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 +#include + +#include "daemon/dnsutils.h" +#include "dbustypeslinux.h" + +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 00000000..9a51caad --- /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 +#include +#include +#include + +#include +#include + +#include "daemon/wireguardutils.h" +#include "leakdetector.h" +#include "logger.h" + +constexpr uint32_t ETH_MTU = 1500; +constexpr uint32_t WG_MTU_OVERHEAD = 80; + +namespace { +Logger logger("IPUtilsLinux"); +} + +IPUtilsLinux::IPUtilsLinux(QObject* parent) : IPUtils(parent) { + MZ_COUNT_CTOR(IPUtilsLinux); + logger.debug() << "IPUtilsLinux created."; +} + +IPUtilsLinux::~IPUtilsLinux() { + MZ_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: " << logger.sensitive(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: " << logger.sensitive(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 00000000..38edf177 --- /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 + +#include "daemon/iputils.h" + +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 00000000..7c2d95db --- /dev/null +++ b/client/platforms/linux/daemon/linuxdaemon.cpp @@ -0,0 +1,52 @@ +/* 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 "linuxdaemon.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "leakdetector.h" +#include "logger.h" + +namespace { +Logger logger("LinuxDaemon"); +LinuxDaemon* s_daemon = nullptr; +} // namespace + +LinuxDaemon::LinuxDaemon() : Daemon(nullptr) { + MZ_COUNT_CTOR(LinuxDaemon); + + logger.debug() << "Daemon created"; + + m_wgutils = new WireguardUtilsLinux(this); + m_dnsutils = new DnsUtilsLinux(this); + m_iputils = new IPUtilsLinux(this); + + Q_ASSERT(s_daemon == nullptr); + s_daemon = this; +} + +LinuxDaemon::~LinuxDaemon() { + MZ_COUNT_DTOR(LinuxDaemon); + + logger.debug() << "Daemon released"; + + Q_ASSERT(s_daemon == this); + s_daemon = nullptr; +} + +// static +LinuxDaemon* LinuxDaemon::instance() { + Q_ASSERT(s_daemon); + return s_daemon; +} diff --git a/client/platforms/linux/daemon/linuxdaemon.h b/client/platforms/linux/daemon/linuxdaemon.h new file mode 100644 index 00000000..7f5d27b7 --- /dev/null +++ b/client/platforms/linux/daemon/linuxdaemon.h @@ -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/. */ + +#ifndef LINUXDAEMON_H +#define LINUXDAEMON_H + + +#include "daemon/daemon.h" +#include "dnsutilslinux.h" +#include "iputilslinux.h" +#include "wireguardutilslinux.h" + +class LinuxDaemon final : public Daemon { + friend class IPUtilsMacos; + + public: + LinuxDaemon(); + ~LinuxDaemon(); + + static LinuxDaemon* instance(); + + protected: + WireguardUtils* wgutils() const override { return m_wgutils; } + bool supportDnsUtils() const override { return true; } + DnsUtils* dnsutils() override { return m_dnsutils; } + bool supportIPUtils() const override { return true; } + IPUtils* iputils() override { return m_iputils; } + + private: + WireguardUtilsLinux* m_wgutils = nullptr; + DnsUtilsLinux* m_dnsutils = nullptr; + IPUtilsLinux* m_iputils = nullptr; +}; + +#endif // LINUXDAEMON_H diff --git a/client/platforms/linux/daemon/linuxroutemonitor.cpp b/client/platforms/linux/daemon/linuxroutemonitor.cpp new file mode 100644 index 00000000..80f510b7 --- /dev/null +++ b/client/platforms/linux/daemon/linuxroutemonitor.cpp @@ -0,0 +1,133 @@ +/* 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 "linuxroutemonitor.h" + +#include "router_linux.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include +#include +#include +#include + +#include "leakdetector.h" +#include "logger.h" + +namespace { +Logger logger("LinuxRouteMonitor"); +} // namespace + +LinuxRouteMonitor::LinuxRouteMonitor(const QString& ifname, QObject* parent) + : QObject(parent), m_ifname(ifname) { + MZ_COUNT_CTOR(LinuxRouteMonitor); + logger.debug() << "LinuxRouteMonitor created."; + + m_rtsock = socket(PF_ROUTE, SOCK_RAW, 0); + if (m_rtsock < 0) { + logger.error() << "Failed to create routing socket:" << strerror(errno); + return; + } + + RouterLinux &router = RouterLinux::Instance(); + m_defaultGatewayIpv4 = router.getgatewayandiface().toUtf8(); + + m_ifindex = if_nametoindex(qPrintable(ifname)); + m_notifier = new QSocketNotifier(m_rtsock, QSocketNotifier::Read, this); +} + +LinuxRouteMonitor::~LinuxRouteMonitor() { + MZ_COUNT_DTOR(LinuxRouteMonitor); + flushExclusionRoutes(); + if (m_rtsock >= 0) { + close(m_rtsock); + } + logger.debug() << "LinuxRouteMonitor destroyed."; +} + +// Compare memory against zero. +static int memcmpzero(const void* data, size_t len) { + const quint8* ptr = static_cast(data); + while (len--) { + if (*ptr++) return 1; + } + return 0; +} + +bool LinuxRouteMonitor::insertRoute(const IPAddress& prefix) { + int temp_sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); + + struct ifreq ifc; + int res; + + if(temp_sock < 0) + return -1; + strcpy(ifc.ifr_name, m_ifname.toUtf8()); + + res = ioctl(temp_sock, SIOCGIFADDR, &ifc); + if(res < 0) + return -1; + + RouterLinux &router = RouterLinux::Instance(); + logger.debug() << "prefix.toString() " << prefix.toString() << " m_ifname " << inet_ntoa(((struct sockaddr_in*)&ifc.ifr_addr)->sin_addr); + return router.routeAdd(prefix.toString(), inet_ntoa(((struct sockaddr_in*)&ifc.ifr_addr)->sin_addr), temp_sock); +} + +bool LinuxRouteMonitor::deleteRoute(const IPAddress& prefix) { + int temp_sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); + + struct ifreq ifc; + int res; + if(temp_sock < 0) + temp_sock -1; + strcpy(ifc.ifr_name, m_ifname.toUtf8()); + + res = ioctl(temp_sock, SIOCGIFADDR, &ifc); + if(res < 0) + return -1; + + RouterLinux &router = RouterLinux::Instance(); + logger.debug() << "prefix.toString() " << prefix.toString() << " m_ifname " << inet_ntoa(((struct sockaddr_in*)&ifc.ifr_addr)->sin_addr); + return router.routeDelete(prefix.toString(), inet_ntoa(((struct sockaddr_in*)&ifc.ifr_addr)->sin_addr), temp_sock); +} + +bool LinuxRouteMonitor::addExclusionRoute(const IPAddress& prefix) { + logger.debug() << "Adding exclusion route for" + << logger.sensitive(prefix.toString()); + + int temp_sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); + RouterLinux &router = RouterLinux::Instance(); + logger.debug() << "prefix.toString() " << prefix.toString() << " m_defaultGatewayIpv4 " << m_defaultGatewayIpv4; + return router.routeAdd(prefix.toString(), m_defaultGatewayIpv4, temp_sock); + // Otherwise, the default route isn't known yet. Do nothing. + return true; +} + +bool LinuxRouteMonitor::deleteExclusionRoute(const IPAddress& prefix) { + logger.debug() << "Deleting exclusion route for" + << logger.sensitive(prefix.toString()); + + int temp_sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); + RouterLinux &router = RouterLinux::Instance(); + logger.debug() << "prefix.toString() " << prefix.toString() << " m_defaultGatewayIpv4 " << m_defaultGatewayIpv4; + return router.routeDelete(prefix.toString(), m_defaultGatewayIpv4, temp_sock); +} + +void LinuxRouteMonitor::flushExclusionRoutes() { + RouterLinux &router = RouterLinux::Instance(); + router.clearSavedRoutes(); +} diff --git a/client/platforms/linux/daemon/linuxroutemonitor.h b/client/platforms/linux/daemon/linuxroutemonitor.h new file mode 100644 index 00000000..872fcb7e --- /dev/null +++ b/client/platforms/linux/daemon/linuxroutemonitor.h @@ -0,0 +1,53 @@ +/* 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 LINUXROUTEMONITOR_H +#define LINUXROUTEMONITOR_H + +#include +#include +#include +#include +#include + +#include "ipaddress.h" + +struct if_msghdr; +struct rt_msghdr; +struct sockaddr; + +class LinuxRouteMonitor final : public QObject { + Q_OBJECT + + public: + LinuxRouteMonitor(const QString& ifname, QObject* parent = nullptr); + ~LinuxRouteMonitor(); + + bool insertRoute(const IPAddress& prefix); + bool deleteRoute(const IPAddress& prefix); + int interfaceFlags() { return m_ifflags; } + + bool addExclusionRoute(const IPAddress& prefix); + bool deleteExclusionRoute(const IPAddress& prefix); + void flushExclusionRoutes(); + + private: + static QString addrToString(const struct sockaddr* sa); + static QString addrToString(const QByteArray& data); + + QList m_exclusionRoutes; + QByteArray m_defaultGatewayIpv4; + QByteArray m_defaultGatewayIpv6; + unsigned int m_defaultIfindexIpv4 = 0; + unsigned int m_defaultIfindexIpv6 = 0; + + QString m_ifname; + unsigned int m_ifindex = 0; + int m_ifflags = 0; + int m_rtsock = -1; + int m_rtseq = 0; + QSocketNotifier* m_notifier = nullptr; +}; + +#endif // LINUXROUTEMONITOR_H diff --git a/client/platforms/linux/daemon/pidtracker.cpp b/client/platforms/linux/daemon/pidtracker.cpp new file mode 100644 index 00000000..76d53219 --- /dev/null +++ b/client/platforms/linux/daemon/pidtracker.cpp @@ -0,0 +1,228 @@ +/* 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "leakdetector.h" +#include "logger.h" + +constexpr size_t CN_MCAST_MSG_SIZE = + sizeof(struct cn_msg) + sizeof(enum proc_cn_mcast_op); + +namespace { +Logger logger("PidTracker"); +} + +PidTracker::PidTracker(QObject* parent) : QObject(parent) { + MZ_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() { + MZ_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 iterator = kthreads.constBegin(); iterator != kthreads.constEnd(); + ++iterator) { + fprintf(fp, "%d\n", iterator.key()); + 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 00000000..dc632b8b --- /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") { + MZ_COUNT_CTOR(ProcessGroup); + name = groupName; + rootpid = groupRootPid; + state = groupState; + refcount = 0; + } + ~ProcessGroup() { MZ_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/wireguardutilslinux.cpp b/client/platforms/linux/daemon/wireguardutilslinux.cpp new file mode 100644 index 00000000..32150ad5 --- /dev/null +++ b/client/platforms/linux/daemon/wireguardutilslinux.cpp @@ -0,0 +1,369 @@ +/* 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 + +#include +#include +#include +#include +#include + +#include "leakdetector.h" +#include "logger.h" + +constexpr const int WG_TUN_PROC_TIMEOUT = 5000; +constexpr const char* WG_RUNTIME_DIR = "/var/run/wireguard"; + +namespace { +Logger logger("WireguardUtilsLinux"); +Logger logwireguard("WireguardGo"); +}; // namespace + +WireguardUtilsLinux::WireguardUtilsLinux(QObject* parent) + : WireguardUtils(parent), m_tunnel(this) { + MZ_COUNT_CTOR(WireguardUtilsLinux); + logger.debug() << "WireguardUtilsLinux created."; + + connect(&m_tunnel, SIGNAL(readyReadStandardOutput()), this, + SLOT(tunnelStdoutReady())); + connect(&m_tunnel, SIGNAL(errorOccurred(QProcess::ProcessError)), this, + SLOT(tunnelErrorOccurred(QProcess::ProcessError))); +} + +WireguardUtilsLinux::~WireguardUtilsLinux() { + MZ_COUNT_DTOR(WireguardUtilsLinux); + logger.debug() << "WireguardUtilsLinux destroyed."; +} + +void WireguardUtilsLinux::tunnelStdoutReady() { + for (;;) { + QByteArray line = m_tunnel.readLine(); + if (line.length() <= 0) { + break; + } + logwireguard.debug() << QString::fromUtf8(line); + } +} + +void WireguardUtilsLinux::tunnelErrorOccurred(QProcess::ProcessError error) { + logger.warning() << "Tunnel process encountered an error:" << error; + emit backendFailure(); +} + +bool WireguardUtilsLinux::addInterface(const InterfaceConfig& config) { + Q_UNUSED(config); + if (m_tunnel.state() != QProcess::NotRunning) { + logger.warning() << "Unable to start: tunnel process already running"; + return false; + } + + QDir wgRuntimeDir(WG_RUNTIME_DIR); + if (!wgRuntimeDir.exists()) { + wgRuntimeDir.mkpath("."); + } + + QProcessEnvironment pe = QProcessEnvironment::systemEnvironment(); + QString wgNameFile = wgRuntimeDir.filePath(QString(WG_INTERFACE) + ".sock"); + pe.insert("WG_TUN_NAME_FILE", wgNameFile); +#ifdef MZ_DEBUG + pe.insert("LOG_LEVEL", "debug"); +#endif + m_tunnel.setProcessEnvironment(pe); + + QDir appPath(QCoreApplication::applicationDirPath()); + QStringList wgArgs = {"-f", "amn0"}; + m_tunnel.start(appPath.filePath("wireguard-go"), wgArgs); + if (!m_tunnel.waitForStarted(WG_TUN_PROC_TIMEOUT)) { + logger.error() << "Unable to start tunnel process due to timeout"; + m_tunnel.kill(); + return false; + } + + m_ifname = waitForTunnelName(wgNameFile); + if (m_ifname.isNull()) { + logger.error() << "Unable to read tunnel interface name"; + m_tunnel.kill(); + return false; + } + logger.debug() << "Created wireguard interface" << m_ifname; + + // Start the routing table monitor. + m_rtmonitor = new LinuxRouteMonitor(m_ifname, this); + + // Send a UAPI command to configure the interface + QString message("set=1\n"); + QByteArray privateKey = QByteArray::fromBase64(config.m_privateKey.toUtf8()); + QTextStream out(&message); + out << "private_key=" << QString(privateKey.toHex()) << "\n"; + out << "replace_peers=true\n"; + int err = uapiErrno(uapiCommand(message)); + if (err != 0) { + logger.error() << "Interface configuration failed:" << strerror(err); + } + return (err == 0); +} + +bool WireguardUtilsLinux::deleteInterface() { + if (m_rtmonitor) { + delete m_rtmonitor; + m_rtmonitor = nullptr; + } + + if (m_tunnel.state() == QProcess::NotRunning) { + return false; + } + + // Attempt to terminate gracefully. + m_tunnel.terminate(); + if (!m_tunnel.waitForFinished(WG_TUN_PROC_TIMEOUT)) { + m_tunnel.kill(); + m_tunnel.waitForFinished(WG_TUN_PROC_TIMEOUT); + } + + // Garbage collect. + QDir wgRuntimeDir(WG_RUNTIME_DIR); + QFile::remove(wgRuntimeDir.filePath(QString(WG_INTERFACE) + ".name")); + return true; +} + +// dummy implementations for now +bool WireguardUtilsLinux::updatePeer(const InterfaceConfig& config) { + QByteArray publicKey = + QByteArray::fromBase64(qPrintable(config.m_serverPublicKey)); + + QByteArray pskKey = QByteArray::fromBase64(qPrintable(config.m_serverPskKey)); + + logger.debug() << "Configuring peer" << config.m_serverPublicKey << "via" << config.m_serverIpv4AddrIn; + + // Update/create the peer config + QString message; + QTextStream out(&message); + out << "set=1\n"; + out << "public_key=" << QString(publicKey.toHex()) << "\n"; + out << "preshared_key=" << QString(pskKey.toHex()) << "\n"; + if (!config.m_serverIpv4AddrIn.isNull()) { + out << "endpoint=" << config.m_serverIpv4AddrIn << ":"; + } else if (!config.m_serverIpv6AddrIn.isNull()) { + out << "endpoint=[" << config.m_serverIpv6AddrIn << "]:"; + } else { + logger.warning() << "Failed to create peer with no endpoints"; + return false; + } + out << config.m_serverPort << "\n"; + + out << "replace_allowed_ips=true\n"; + out << "persistent_keepalive_interval=" << WG_KEEPALIVE_PERIOD << "\n"; + for (const IPAddress& ip : config.m_allowedIPAddressRanges) { + out << "allowed_ip=" << ip.toString() << "\n"; + } + + // Exclude the server address, except for multihop exit servers. + if ((config.m_hopType != InterfaceConfig::MultiHopExit) && + (m_rtmonitor != nullptr)) { + m_rtmonitor->addExclusionRoute(IPAddress(config.m_serverIpv4AddrIn)); + m_rtmonitor->addExclusionRoute(IPAddress(config.m_serverIpv6AddrIn)); + } + + int err = uapiErrno(uapiCommand(message)); + if (err != 0) { + logger.error() << "Peer configuration failed:" << strerror(err); + } + return (err == 0); +} + +bool WireguardUtilsLinux::deletePeer(const InterfaceConfig& config) { + QByteArray publicKey = + QByteArray::fromBase64(qPrintable(config.m_serverPublicKey)); + + // Clear exclustion routes for this peer. + if ((config.m_hopType != InterfaceConfig::MultiHopExit) && + (m_rtmonitor != nullptr)) { + m_rtmonitor->deleteExclusionRoute(IPAddress(config.m_serverIpv4AddrIn)); + m_rtmonitor->deleteExclusionRoute(IPAddress(config.m_serverIpv6AddrIn)); + } + + QString message; + QTextStream out(&message); + out << "set=1\n"; + out << "public_key=" << QString(publicKey.toHex()) << "\n"; + out << "remove=true\n"; + + int err = uapiErrno(uapiCommand(message)); + if (err != 0) { + logger.error() << "Peer deletion failed:" << strerror(err); + } + return (err == 0); +} + +QList WireguardUtilsLinux::getPeerStatus() { + QString reply = uapiCommand("get=1"); + PeerStatus status; + QList peerList; + for (const QString& line : reply.split('\n')) { + int eq = line.indexOf('='); + if (eq <= 0) { + continue; + } + QString name = line.left(eq); + QString value = line.mid(eq + 1); + + if (name == "public_key") { + if (!status.m_pubkey.isEmpty()) { + peerList.append(status); + } + QByteArray pubkey = QByteArray::fromHex(value.toUtf8()); + status = PeerStatus(pubkey.toBase64()); + } + + if (name == "tx_bytes") { + status.m_txBytes = value.toDouble(); + } + if (name == "rx_bytes") { + status.m_rxBytes = value.toDouble(); + } + if (name == "last_handshake_time_sec") { + status.m_handshake += value.toLongLong() * 1000; + } + if (name == "last_handshake_time_nsec") { + status.m_handshake += value.toLongLong() / 1000000; + } + } + if (!status.m_pubkey.isEmpty()) { + peerList.append(status); + } + + return peerList; +} + +bool WireguardUtilsLinux::updateRoutePrefix(const IPAddress& prefix) { + if (!m_rtmonitor) { + return false; + } + if (prefix.prefixLength() > 0) { + return m_rtmonitor->insertRoute(prefix); + } + + // Ensure that we do not replace the default route. + if (prefix.type() == QAbstractSocket::IPv4Protocol) { + return m_rtmonitor->insertRoute(IPAddress("0.0.0.0/1")) && + m_rtmonitor->insertRoute(IPAddress("128.0.0.0/1")); + } + if (prefix.type() == QAbstractSocket::IPv6Protocol) { + return m_rtmonitor->insertRoute(IPAddress("::/1")) && + m_rtmonitor->insertRoute(IPAddress("8000::/1")); + } + + return false; +} + +bool WireguardUtilsLinux::deleteRoutePrefix(const IPAddress& prefix) { + if (!m_rtmonitor) { + return false; + } + if (prefix.prefixLength() > 0) { + return m_rtmonitor->insertRoute(prefix); + } + + // Ensure that we do not replace the default route. + if (prefix.type() == QAbstractSocket::IPv4Protocol) { + return m_rtmonitor->deleteRoute(IPAddress("0.0.0.0/1")) && + m_rtmonitor->deleteRoute(IPAddress("128.0.0.0/1")); + } else if (prefix.type() == QAbstractSocket::IPv6Protocol) { + return m_rtmonitor->deleteRoute(IPAddress("::/1")) && + m_rtmonitor->deleteRoute(IPAddress("8000::/1")); + } else { + return false; + } +} + +bool WireguardUtilsLinux::addExclusionRoute(const IPAddress& prefix) { + if (!m_rtmonitor) { + return false; + } + return m_rtmonitor->addExclusionRoute(prefix); +} + +bool WireguardUtilsLinux::deleteExclusionRoute(const IPAddress& prefix) { + if (!m_rtmonitor) { + return false; + } + return m_rtmonitor->deleteExclusionRoute(prefix); +} + +QString WireguardUtilsLinux::uapiCommand(const QString& command) { + QLocalSocket socket; + QTimer uapiTimeout; + QDir wgRuntimeDir(WG_RUNTIME_DIR); + QString wgSocketFile = wgRuntimeDir.filePath(m_ifname + ".sock"); + + uapiTimeout.setSingleShot(true); + uapiTimeout.start(WG_TUN_PROC_TIMEOUT); + + socket.connectToServer(wgSocketFile, QIODevice::ReadWrite); + if (!socket.waitForConnected(WG_TUN_PROC_TIMEOUT)) { + logger.error() << "QLocalSocket::waitForConnected() failed:" + << socket.errorString(); + return QString(); + } + + // Send the message to the UAPI socket. + QByteArray message = command.toLocal8Bit(); + while (!message.endsWith("\n\n")) { + message.append('\n'); + } + socket.write(message); + + QByteArray reply; + while (!reply.contains("\n\n")) { + if (!uapiTimeout.isActive()) { + logger.error() << "UAPI command timed out"; + return QString(); + } + QCoreApplication::processEvents(QEventLoop::AllEvents, 100); + reply.append(socket.readAll()); + } + + return QString::fromUtf8(reply).trimmed(); +} + +// static +int WireguardUtilsLinux::uapiErrno(const QString& reply) { + for (const QString& line : reply.split("\n")) { + int eq = line.indexOf('='); + if (eq <= 0) { + continue; + } + if (line.left(eq) == "errno") { + return line.mid(eq + 1).toInt(); + } + } + return EINVAL; +} + +QString WireguardUtilsLinux::waitForTunnelName(const QString& filename) { + QTimer timeout; + timeout.setSingleShot(true); + timeout.start(WG_TUN_PROC_TIMEOUT); + + QFile file(filename); + + while ((m_tunnel.state() == QProcess::Running) && timeout.isActive()) { + QCoreApplication::processEvents(QEventLoop::AllEvents, 100); + QString ifname = "amn0"; + + // Test-connect to the UAPI socket. + QLocalSocket sock; + QDir wgRuntimeDir(WG_RUNTIME_DIR); + QString sockName = wgRuntimeDir.filePath(ifname + ".sock"); + sock.connectToServer(sockName, QIODevice::ReadWrite); + if (sock.waitForConnected(100)) { + return ifname; + } + } + + return QString(); +} diff --git a/client/platforms/linux/daemon/wireguardutilslinux.h b/client/platforms/linux/daemon/wireguardutilslinux.h new file mode 100644 index 00000000..a8320c95 --- /dev/null +++ b/client/platforms/linux/daemon/wireguardutilslinux.h @@ -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/. */ + +#ifndef WIREGUARDUTILSLINUX_H +#define WIREGUARDUTILSLINUX_H + +#include +#include + +#include "daemon/wireguardutils.h" +#include "linuxroutemonitor.h" + +class WireguardUtilsLinux final : public WireguardUtils { + Q_OBJECT + +public: + WireguardUtilsLinux(QObject* parent); + ~WireguardUtilsLinux(); + + bool interfaceExists() override { + return m_tunnel.state() == QProcess::Running; + } + QString interfaceName() override { return m_ifname; } + bool addInterface(const InterfaceConfig& config) override; + bool deleteInterface() override; + + bool updatePeer(const InterfaceConfig& config) override; + bool deletePeer(const InterfaceConfig& config) override; + QList getPeerStatus() override; + + bool updateRoutePrefix(const IPAddress& prefix) override; + bool deleteRoutePrefix(const IPAddress& prefix) override; + + bool addExclusionRoute(const IPAddress& prefix) override; + bool deleteExclusionRoute(const IPAddress& prefix) override; + +signals: + void backendFailure(); + +private slots: + void tunnelStdoutReady(); + void tunnelErrorOccurred(QProcess::ProcessError error); + +private: + QString uapiCommand(const QString& command); + static int uapiErrno(const QString& command); + QString waitForTunnelName(const QString& filename); + + QString m_ifname; + QProcess m_tunnel; + LinuxRouteMonitor* m_rtmonitor = nullptr; +}; + +#endif // WIREGUARDUTILSLINUX_H diff --git a/client/platforms/linux/interfaceconfig.h b/client/platforms/linux/interfaceconfig.h new file mode 100644 index 00000000..bd4e383a --- /dev/null +++ b/client/platforms/linux/interfaceconfig.h @@ -0,0 +1,47 @@ +/* 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 INTERFACECONFIG_H +#define INTERFACECONFIG_H + +#include +#include + +#include "ipaddress.h" + +class QJsonObject; + +class InterfaceConfig { + Q_GADGET + + public: + InterfaceConfig() {} + + enum HopType { SingleHop, MultiHopEntry, MultiHopExit }; + Q_ENUM(HopType) + + HopType m_hopType; + QString m_privateKey; + QString m_deviceIpv4Address; + QString m_deviceIpv6Address; + QString m_serverIpv4Gateway; + QString m_serverIpv6Gateway; + QString m_serverPublicKey; + QString m_serverIpv4AddrIn; + QString m_serverIpv6AddrIn; + QString m_dnsServer; + int m_serverPort = 0; + QList m_allowedIPAddressRanges; + QStringList m_excludedAddresses; + QStringList m_vpnDisabledApps; +#if defined(MZ_ANDROID) || defined(MZ_IOS) + QString m_installationId; +#endif + + QJsonObject toJson() const; + QString toWgConf( + const QMap& extra = QMap()) const; +}; + +#endif // INTERFACECONFIG_H diff --git a/client/platforms/linux/linuxdependencies.cpp b/client/platforms/linux/linuxdependencies.cpp new file mode 100644 index 00000000..3a4cb259 --- /dev/null +++ b/client/platforms/linux/linuxdependencies.cpp @@ -0,0 +1,139 @@ +/* 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 + +#include +#include +#include +#include +#include +#include +#include + +//#include "dbusclient.h" +#include "logger.h" + +namespace { + +Logger logger("LinuxDependencies"); + +void showAlert(const QString& message) { + logger.debug() << "Show alert:" << message; + + QMessageBox alert; + alert.setText(message); + alert.exec(); +} + +bool checkDaemonVersion() { + logger.debug() << "Check Daemon Version"; + + bool completed = false; + bool value = false; + + while (!completed) { + QCoreApplication::processEvents(); + } + + return value; +} + +} // namespace + +// static +bool LinuxDependencies::checkDependencies() { + char* path = getenv("PATH"); + if (!path) { + showAlert("No PATH env found."); + 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(); +} + +// static +QString LinuxDependencies::findCgroup2Path() { + 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, "cgroup2") != 0) { + continue; + } + return QString(entry.mnt_dir); + } + fclose(fp); + + return QString(); +} + +// static +QString LinuxDependencies::gnomeShellVersion() { + QDBusInterface iface("org.gnome.Shell", "/org/gnome/Shell", + "org.gnome.Shell"); + if (!iface.isValid()) { + return QString(); + } + + QVariant shellVersion = iface.property("ShellVersion"); + if (!shellVersion.isValid()) { + return QString(); + } + return shellVersion.toString(); +} + +// static +QString LinuxDependencies::kdeFrameworkVersion() { + QProcess proc; + proc.start("kf5-config", QStringList{"--version"}, QIODeviceBase::ReadOnly); + if (!proc.waitForFinished()) { + return QString(); + } + + QByteArray result = proc.readAllStandardOutput(); + for (const QByteArray& line : result.split('\n')) { + if (line.startsWith("KDE Frameworks: ")) { + return QString::fromUtf8(line.last(line.size() - 16)); + } + } + + return QString(); +} diff --git a/client/platforms/linux/linuxdependencies.h b/client/platforms/linux/linuxdependencies.h new file mode 100644 index 00000000..70ff6f18 --- /dev/null +++ b/client/platforms/linux/linuxdependencies.h @@ -0,0 +1,25 @@ +/* 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); + static QString findCgroup2Path(); + static QString gnomeShellVersion(); + static QString kdeFrameworkVersion(); + + 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 00000000..c8ae0fea --- /dev/null +++ b/client/platforms/linux/linuxnetworkwatcher.cpp @@ -0,0 +1,57 @@ +/* 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 + +#include "leakdetector.h" +#include "linuxnetworkwatcherworker.h" +#include "logger.h" + +namespace { +Logger logger("LinuxNetworkWatcher"); +} + +LinuxNetworkWatcher::LinuxNetworkWatcher(QObject* parent) + : NetworkWatcherImpl(parent) { + MZ_COUNT_CTOR(LinuxNetworkWatcher); + + m_thread.start(); +} + +LinuxNetworkWatcher::~LinuxNetworkWatcher() { + MZ_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. + QTimer::singleShot(2000, this, [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 00000000..ed8c76ba --- /dev/null +++ b/client/platforms/linux/linuxnetworkwatcher.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 LINUXNETWORKWATCHER_H +#define LINUXNETWORKWATCHER_H + +#include + +#include "networkwatcherimpl.h" + +class LinuxNetworkWatcherWorker; + +class LinuxNetworkWatcher final : public NetworkWatcherImpl { + Q_OBJECT + + public: + explicit LinuxNetworkWatcher(QObject* parent); + ~LinuxNetworkWatcher(); + + void initialize() override; + + void start() override; + + NetworkWatcherImpl::TransportType getTransportType() { + // TODO: Find out how to do that on linux generally. (VPN-2382) + return NetworkWatcherImpl::TransportType_Unknown; + }; + + 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 00000000..19ed3251 --- /dev/null +++ b/client/platforms/linux/linuxnetworkwatcherworker.cpp @@ -0,0 +1,177 @@ +/* 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 + +#include "leakdetector.h" +#include "logger.h" + +// 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("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) { + MZ_COUNT_CTOR(LinuxNetworkWatcherWorker); + moveToThread(thread); +} + +LinuxNetworkWatcherWorker::~LinuxNetworkWatcherWorker() { + MZ_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:" << logger.sensitive(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 00000000..cc4c6a36 --- /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/protocols/wireguardprotocol.cpp b/client/protocols/wireguardprotocol.cpp index 3091655d..91903b2b 100644 --- a/client/protocols/wireguardprotocol.cpp +++ b/client/protocols/wireguardprotocol.cpp @@ -16,9 +16,7 @@ WireguardProtocol::WireguardProtocol(const QJsonObject &configuration, QObject* writeWireguardConfiguration(configuration); // MZ -#if defined(MZ_LINUX) - //m_impl.reset(new LinuxController()); -#elif defined(Q_OS_MAC) || defined(Q_OS_WIN) +#if defined(Q_OS_MAC) || defined(Q_OS_WIN) || defined(Q_OS_LINUX) m_impl.reset(new LocalSocketController()); connect(m_impl.get(), &ControllerImpl::connected, this, [this](const QString& pubkey, const QDateTime& connectionTimestamp) { emit connectionStateChanged(VpnProtocol::Connected); @@ -38,7 +36,7 @@ WireguardProtocol::~WireguardProtocol() void WireguardProtocol::stop() { -#if defined(Q_OS_MAC) || defined(Q_OS_WIN) +#if defined(Q_OS_MAC) || defined(Q_OS_WIN) || defined(Q_OS_LINUX) stopMzImpl(); return; #endif @@ -98,7 +96,7 @@ void WireguardProtocol::stop() setConnectionState(VpnProtocol::Disconnected); } -#if defined(Q_OS_MAC) || defined(Q_OS_WIN) +#if defined(Q_OS_MAC) || defined(Q_OS_WIN) || defined(Q_OS_LINUX) ErrorCode WireguardProtocol::startMzImpl() { @@ -126,7 +124,7 @@ void WireguardProtocol::writeWireguardConfiguration(const QJsonObject &configura m_configFile.write(jConfig.value(config_key::config).toString().toUtf8()); m_configFile.close(); -#ifdef Q_OS_LINUX +#if 0 if (IpcClient::Interface()) { QRemoteObjectPendingReply result = IpcClient::Interface()->copyWireguardConfig(m_configFile.fileName()); if (result.returnValue()) { @@ -171,7 +169,7 @@ ErrorCode WireguardProtocol::start() return lastError(); } -#if defined(Q_OS_MAC) || defined(Q_OS_WIN) +#if defined(Q_OS_MAC) || defined(Q_OS_WIN) || defined(Q_OS_LINUX) return startMzImpl(); #endif diff --git a/client/protocols/wireguardprotocol.h b/client/protocols/wireguardprotocol.h index 6f530758..dea8d6d9 100644 --- a/client/protocols/wireguardprotocol.h +++ b/client/protocols/wireguardprotocol.h @@ -23,7 +23,7 @@ public: ErrorCode start() override; void stop() override; -#if defined(Q_OS_MAC) || defined(Q_OS_WIN) +#if defined(Q_OS_MAC) || defined(Q_OS_WIN) || defined(Q_OS_LINUX) ErrorCode startMzImpl(); ErrorCode stopMzImpl(); #endif @@ -47,7 +47,7 @@ private: bool m_isConfigLoaded = false; -#if defined(Q_OS_MAC) || defined(Q_OS_WIN) +#if defined(Q_OS_MAC) || defined(Q_OS_WIN) || defined(Q_OS_LINUX) QScopedPointer m_impl; #endif }; diff --git a/service/server/CMakeLists.txt b/service/server/CMakeLists.txt index 72c5b09b..20ed8cb6 100644 --- a/service/server/CMakeLists.txt +++ b/service/server/CMakeLists.txt @@ -6,9 +6,10 @@ project(${PROJECT}) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) -find_package(Qt6 REQUIRED COMPONENTS Core Network Widgets RemoteObjects Core5Compat) +find_package(Qt6 REQUIRED COMPONENTS DBus Core Network Widgets RemoteObjects Core5Compat) qt_standard_project_setup() + configure_file(${CMAKE_SOURCE_DIR}/version.h.in ${CMAKE_CURRENT_BINARY_DIR}/version.h) set(HEADERS @@ -91,7 +92,7 @@ if(UNIX) ) endif() -if (WIN32 OR APPLE) +if (WIN32 OR APPLE OR LINUX) set(HEADERS ${HEADERS} ${CMAKE_CURRENT_LIST_DIR}/../../client/daemon/daemon.h ${CMAKE_CURRENT_LIST_DIR}/../../client/daemon/daemonlocalserver.h @@ -198,12 +199,32 @@ if(APPLE) endif() if(LINUX) + set(HEADERS ${HEADERS} ${CMAKE_CURRENT_LIST_DIR}/router_linux.h + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/linuxnetworkwatcher.h + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/linuxnetworkwatcherworker.h + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/linuxdependencies.h + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/daemon/iputilslinux.h + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/daemon/dbustypeslinux.h + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/daemon/linuxdaemon.h + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/daemon/dnsutilslinux.h + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/daemon/pidtracker.h + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/daemon/wireguardutilslinux.h + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/daemon/linuxroutemonitor.h ) set(SOURCES ${SOURCES} ${CMAKE_CURRENT_LIST_DIR}/router_linux.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/linuxnetworkwatcher.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/linuxnetworkwatcherworker.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/linuxdependencies.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/daemon/dnsutilslinux.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/daemon/pidtracker.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/daemon/iputilslinux.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/daemon/linuxdaemon.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/daemon/wireguardutilslinux.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/daemon/linuxroutemonitor.cpp ) endif() @@ -217,7 +238,7 @@ include_directories( ) add_executable(${PROJECT} ${SOURCES} ${HEADERS}) -target_link_libraries(${PROJECT} PRIVATE Qt6::Core Qt6::Widgets Qt6::Network Qt6::RemoteObjects Qt6::Core5Compat ${LIBS}) +target_link_libraries(${PROJECT} PRIVATE Qt6::Core Qt6::Widgets Qt6::Network Qt6::RemoteObjects Qt6::Core5Compat Qt6::DBus ${LIBS}) target_compile_definitions(${PROJECT} PRIVATE "MZ_$") if(CMAKE_BUILD_TYPE STREQUAL "Debug") diff --git a/service/server/localserver.cpp b/service/server/localserver.cpp index 709ad693..3e1b0954 100644 --- a/service/server/localserver.cpp +++ b/service/server/localserver.cpp @@ -40,12 +40,16 @@ LocalServer::LocalServer(QObject *parent) : QObject(parent), } }); -#if defined(Q_OS_MAC) || defined(Q_OS_WIN) // Init Mozilla Wireguard Daemon if (!server.initialize()) { logger.error() << "Failed to initialize the server"; return; } + +#ifdef Q_OS_LINUX + // Signal handling for a proper shutdown. + QObject::connect(qApp, &QCoreApplication::aboutToQuit, + []() { LinuxDaemon::instance()->deactivate(); }); #endif #ifdef Q_OS_MAC diff --git a/service/server/localserver.h b/service/server/localserver.h index b5264120..4a6648a5 100644 --- a/service/server/localserver.h +++ b/service/server/localserver.h @@ -10,13 +10,18 @@ #include "ipcserver.h" -#ifdef Q_OS_WIN #include "../../client/daemon/daemonlocalserver.h" + + +#ifdef Q_OS_WIN #include "windows/daemon/windowsdaemon.h" #endif +#ifdef Q_OS_LINUX +#include "linux/daemon/linuxdaemon.h" +#endif + #ifdef Q_OS_MAC -#include "../../client/daemon/daemonlocalserver.h" #include "macos/daemon/macosdaemon.h" #endif @@ -31,13 +36,14 @@ class LocalServer : public QObject public: explicit LocalServer(QObject* parent = nullptr); ~LocalServer(); - QSharedPointer m_server; - IpcServer m_ipcServer; QRemoteObjectHost m_serverNode; bool m_isRemotingEnabled = false; - +#ifdef Q_OS_LINUX + DaemonLocalServer server{qApp}; + LinuxDaemon daemon; +#endif #ifdef Q_OS_WIN DaemonLocalServer server{qApp}; WindowsDaemon daemon; diff --git a/service/server/router_linux.cpp b/service/server/router_linux.cpp index 3517f8e6..d717ce9c 100644 --- a/service/server/router_linux.cpp +++ b/service/server/router_linux.cpp @@ -14,6 +14,17 @@ #include #include #include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + #include RouterLinux &RouterLinux::Instance() @@ -22,6 +33,123 @@ RouterLinux &RouterLinux::Instance() return s; } +#define BUFFER_SIZE 4096 + +QString RouterLinux::getgatewayandiface() +{ + int received_bytes = 0, msg_len = 0, route_attribute_len = 0; + int sock = -1, msgseq = 0; + struct nlmsghdr *nlh, *nlmsg; + struct rtmsg *route_entry; + // This struct contain route attributes (route type) + struct rtattr *route_attribute; + char gateway_address[INET_ADDRSTRLEN], interface[IF_NAMESIZE]; + char msgbuf[BUFFER_SIZE], buffer[BUFFER_SIZE]; + char *ptr = buffer; + struct timeval tv; + + if ((sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE)) < 0) { + perror("socket failed"); + return ""; + } + + memset(msgbuf, 0, sizeof(msgbuf)); + memset(gateway_address, 0, sizeof(gateway_address)); + memset(interface, 0, sizeof(interface)); + memset(buffer, 0, sizeof(buffer)); + + /* point the header and the msg structure pointers into the buffer */ + nlmsg = (struct nlmsghdr *)msgbuf; + + /* Fill in the nlmsg header*/ + nlmsg->nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg)); + nlmsg->nlmsg_type = RTM_GETROUTE; // Get the routes from kernel routing table . + nlmsg->nlmsg_flags = NLM_F_DUMP | NLM_F_REQUEST; // The message is a request for dump. + nlmsg->nlmsg_seq = msgseq++; // Sequence of the message packet. + nlmsg->nlmsg_pid = getpid(); // PID of process sending the request. + + /* 1 Sec Timeout to avoid stall */ + tv.tv_sec = 1; + setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (struct timeval *)&tv, sizeof(struct timeval)); + /* send msg */ + if (send(sock, nlmsg, nlmsg->nlmsg_len, 0) < 0) { + perror("send failed"); + return ""; + } + + /* receive response */ + do + { + received_bytes = recv(sock, ptr, sizeof(buffer) - msg_len, 0); + if (received_bytes < 0) { + perror("Error in recv"); + return ""; + } + + nlh = (struct nlmsghdr *) ptr; + + /* Check if the header is valid */ + if((NLMSG_OK(nlmsg, received_bytes) == 0) || + (nlmsg->nlmsg_type == NLMSG_ERROR)) + { + perror("Error in received packet"); + return ""; + } + + /* If we received all data break */ + if (nlh->nlmsg_type == NLMSG_DONE) + break; + else { + ptr += received_bytes; + msg_len += received_bytes; + } + + /* Break if its not a multi part message */ + if ((nlmsg->nlmsg_flags & NLM_F_MULTI) == 0) + break; + } + while ((nlmsg->nlmsg_seq != msgseq) || (nlmsg->nlmsg_pid != getpid())); + + /* parse response */ + for ( ; NLMSG_OK(nlh, received_bytes); nlh = NLMSG_NEXT(nlh, received_bytes)) + { + /* Get the route data */ + route_entry = (struct rtmsg *) NLMSG_DATA(nlh); + + /* We are just interested in main routing table */ + if (route_entry->rtm_table != RT_TABLE_MAIN) + continue; + + route_attribute = (struct rtattr *) RTM_RTA(route_entry); + route_attribute_len = RTM_PAYLOAD(nlh); + + /* Loop through all attributes */ + for ( ; RTA_OK(route_attribute, route_attribute_len); + route_attribute = RTA_NEXT(route_attribute, route_attribute_len)) + { + switch(route_attribute->rta_type) { + case RTA_OIF: + if_indextoname(*(int *)RTA_DATA(route_attribute), interface); + break; + case RTA_GATEWAY: + inet_ntop(AF_INET, RTA_DATA(route_attribute), + gateway_address, sizeof(gateway_address)); + break; + default: + break; + } + } + + if ((*gateway_address) && (*interface)) { + qDebug().noquote() << "Gateway " << gateway_address << " for interface " << interface; + break; + } + } + close(sock); + return gateway_address; +} + + bool RouterLinux::routeAdd(const QString &ipWithSubnet, const QString &gw, const int &sock) { QString ip = Utils::ipAddressFromIpWithSubnet(ipWithSubnet); @@ -29,7 +157,7 @@ bool RouterLinux::routeAdd(const QString &ipWithSubnet, const QString &gw, const if (!Utils::checkIPv4Format(ip) || !Utils::checkIPv4Format(gw)) { qCritical().noquote() << "Critical, trying to add invalid route: " << ip << gw; - return false; + return true; } struct rtentry route; @@ -53,11 +181,11 @@ bool RouterLinux::routeAdd(const QString &ipWithSubnet, const QString &gw, const if (int err = ioctl(sock, SIOCADDRT, &route) < 0) { - qDebug().noquote() << "route add error: gw " - << ((struct sockaddr_in *)&route.rt_gateway)->sin_addr.s_addr - << " ip " << ((struct sockaddr_in *)&route.rt_dst)->sin_addr.s_addr - << " mask " << ((struct sockaddr_in *)&route.rt_genmask)->sin_addr.s_addr << " " << err; - return false; + // qDebug().noquote() << "route add error: gw " + // << ((struct sockaddr_in *)&route.rt_gateway)->sin_addr.s_addr + // << " ip " << ((struct sockaddr_in *)&route.rt_dst)->sin_addr.s_addr + // << " mask " << ((struct sockaddr_in *)&route.rt_genmask)->sin_addr.s_addr << " " << err; + // return false; } m_addedRoutes.append({ipWithSubnet, gw}); @@ -99,7 +227,7 @@ bool RouterLinux::routeDelete(const QString &ipWithSubnet, const QString &gw, co if (!Utils::checkIPv4Format(ip) || !Utils::checkIPv4Format(gw)) { qCritical().noquote() << "Critical, trying to remove invalid route: " << ip << gw; - return false; + return true; } if (ip == "0.0.0.0") { @@ -129,8 +257,8 @@ bool RouterLinux::routeDelete(const QString &ipWithSubnet, const QString &gw, co if (ioctl(sock, SIOCDELRT, &route) < 0) { - qDebug().noquote() << "route delete error: gw " << gw << " ip " << ip << " mask " << mask; - return false; + // qDebug().noquote() << "route delete error: gw " << gw << " ip " << ip << " mask " << mask; + // return false; } return true; } diff --git a/service/server/router_linux.h b/service/server/router_linux.h index 6da20b7d..5b4897bd 100644 --- a/service/server/router_linux.h +++ b/service/server/router_linux.h @@ -27,6 +27,7 @@ public: bool clearSavedRoutes(); bool routeDelete(const QString &ip, const QString &gw, const int &sock); bool routeDeleteList(const QString &gw, const QStringList &ips); + QString getgatewayandiface(); void flushDns(); public slots: From ff41b26e94892687bc71be911b4725c24397cc50 Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Tue, 19 Sep 2023 18:45:06 +0500 Subject: [PATCH 5/9] added parsing of wireguard config parameters when importing native configs --- client/ui/pages_logic/StartPageLogic.cpp | 166 ++++++++++++++--------- 1 file changed, 104 insertions(+), 62 deletions(-) diff --git a/client/ui/pages_logic/StartPageLogic.cpp b/client/ui/pages_logic/StartPageLogic.cpp index c1670b70..79bd24c0 100644 --- a/client/ui/pages_logic/StartPageLogic.cpp +++ b/client/ui/pages_logic/StartPageLogic.cpp @@ -1,72 +1,73 @@ #include "StartPageLogic.h" #include "ViewConfigLogic.h" -#include "core/errorstrings.h" +#include "../uilogic.h" #include "configurators/ssh_configurator.h" #include "configurators/vpn_configurator.h" -#include "../uilogic.h" -#include "utilities.h" +#include "core/errorstrings.h" #include "core/servercontroller.h" +#include "utilities.h" +#include #include #include -#include #ifdef Q_OS_ANDROID -#include -#include "../../platforms/android/androidutils.h" -#include "../../platforms/android/android_controller.h" + #include "../../platforms/android/android_controller.h" + #include "../../platforms/android/androidutils.h" + #include #endif #ifdef Q_OS_IOS -#include + #include #endif -namespace { -enum class ConfigTypes { - Amnezia, - OpenVpn, - WireGuard -}; - -ConfigTypes checkConfigFormat(const QString &config) +namespace { - const QString openVpnConfigPatternCli = "client"; - const QString openVpnConfigPatternProto1 = "proto tcp"; - const QString openVpnConfigPatternProto2 = "proto udp"; - const QString openVpnConfigPatternDriver1 = "dev tun"; - const QString openVpnConfigPatternDriver2 = "dev tap"; + enum class ConfigTypes { + Amnezia, + OpenVpn, + WireGuard + }; - const QString wireguardConfigPatternSectionInterface = "[Interface]"; - const QString wireguardConfigPatternSectionPeer = "[Peer]"; + ConfigTypes checkConfigFormat(const QString &config) + { + const QString openVpnConfigPatternCli = "client"; + const QString openVpnConfigPatternProto1 = "proto tcp"; + const QString openVpnConfigPatternProto2 = "proto udp"; + const QString openVpnConfigPatternDriver1 = "dev tun"; + const QString openVpnConfigPatternDriver2 = "dev tap"; - if (config.contains(openVpnConfigPatternCli) && - (config.contains(openVpnConfigPatternProto1) || config.contains(openVpnConfigPatternProto2)) && - (config.contains(openVpnConfigPatternDriver1) || config.contains(openVpnConfigPatternDriver2))) { - return ConfigTypes::OpenVpn; - } else if (config.contains(wireguardConfigPatternSectionInterface) && - config.contains(wireguardConfigPatternSectionPeer)) - return ConfigTypes::WireGuard; - return ConfigTypes::Amnezia; -} + const QString wireguardConfigPatternSectionInterface = "[Interface]"; + const QString wireguardConfigPatternSectionPeer = "[Peer]"; + + if (config.contains(openVpnConfigPatternCli) + && (config.contains(openVpnConfigPatternProto1) || config.contains(openVpnConfigPatternProto2)) + && (config.contains(openVpnConfigPatternDriver1) || config.contains(openVpnConfigPatternDriver2))) { + return ConfigTypes::OpenVpn; + } else if (config.contains(wireguardConfigPatternSectionInterface) + && config.contains(wireguardConfigPatternSectionPeer)) + return ConfigTypes::WireGuard; + return ConfigTypes::Amnezia; + } } -StartPageLogic::StartPageLogic(UiLogic *logic, QObject *parent): - PageLogicBase(logic, parent), - m_pushButtonConnectEnabled{true}, - m_pushButtonConnectText{tr("Connect")}, - m_pushButtonConnectKeyChecked{false}, - m_labelWaitInfoVisible{true}, - m_pushButtonBackFromStartVisible{true}, - m_ipAddressPortRegex{Utils::ipAddressPortRegExp()} +StartPageLogic::StartPageLogic(UiLogic *logic, QObject *parent) + : PageLogicBase(logic, parent), + m_pushButtonConnectEnabled { true }, + m_pushButtonConnectText { tr("Connect") }, + m_pushButtonConnectKeyChecked { false }, + m_labelWaitInfoVisible { true }, + m_pushButtonBackFromStartVisible { true }, + m_ipAddressPortRegex { Utils::ipAddressPortRegExp() } { #ifdef Q_OS_ANDROID // Set security screen for Android app AndroidUtils::runOnAndroidThreadSync([]() { QJniObject activity = AndroidUtils::getActivity(); QJniObject window = activity.callObjectMethod("getWindow", "()Landroid/view/Window;"); - if (window.isValid()){ + if (window.isValid()) { const int FLAG_SECURE = 8192; window.callMethod("addFlags", "(I)V", FLAG_SECURE); } @@ -93,17 +94,13 @@ void StartPageLogic::onUpdatePage() void StartPageLogic::onPushButtonConnect() { - if (pushButtonConnectKeyChecked()){ - if (lineEditIpText().isEmpty() || - lineEditLoginText().isEmpty() || - textEditSshKeyText().isEmpty() ) { + if (pushButtonConnectKeyChecked()) { + if (lineEditIpText().isEmpty() || lineEditLoginText().isEmpty() || textEditSshKeyText().isEmpty()) { set_labelWaitInfoText(tr("Please fill in all fields")); return; } } else { - if (lineEditIpText().isEmpty() || - lineEditLoginText().isEmpty() || - lineEditPasswordText().isEmpty() ) { + if (lineEditIpText().isEmpty() || lineEditLoginText().isEmpty() || lineEditPasswordText().isEmpty()) { set_labelWaitInfoText(tr("Please fill in all fields")); return; } @@ -178,7 +175,8 @@ void StartPageLogic::onPushButtonConnect() set_pushButtonConnectText(tr("Connect")); uiLogic()->m_installCredentials = serverCredentials; - if (ok) emit uiLogic()->goToPage(Page::NewServer); + if (ok) + emit uiLogic()->goToPage(Page::NewServer); } void StartPageLogic::onPushButtonImport() @@ -189,22 +187,24 @@ void StartPageLogic::onPushButtonImport() void StartPageLogic::onPushButtonImportOpenFile() { QString fileName = UiLogic::getOpenFileName(Q_NULLPTR, tr("Open config file"), - QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation), "*.vpn *.ovpn *.conf"); - if (fileName.isEmpty()) return; + QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation), + "*.vpn *.ovpn *.conf"); + if (fileName.isEmpty()) + return; QFile file(fileName); - + #ifdef Q_OS_IOS CFURLRef url = CFURLCreateWithFileSystemPath( - kCFAllocatorDefault, CFStringCreateWithCharacters(0, reinterpret_cast(fileName.unicode()), - fileName.length()), - kCFURLPOSIXPathStyle, 0); - + kCFAllocatorDefault, + CFStringCreateWithCharacters(0, reinterpret_cast(fileName.unicode()), fileName.length()), + kCFURLPOSIXPathStyle, 0); + if (!CFURLStartAccessingSecurityScopedResource(url)) { qDebug() << "Could not access path " << QUrl::fromLocalFile(fileName).toString(); } #endif - + file.open(QIODevice::ReadOnly); QByteArray data = file.readAll(); @@ -242,8 +242,7 @@ bool StartPageLogic::importConnection(const QJsonObject &profile) // check config uiLogic()->pageLogic()->set_configJson(profile); emit uiLogic()->goToPage(Page::ViewConfig); - } - else { + } else { qDebug() << "Failed to import profile"; qDebug().noquote() << QJsonDocument(profile).toJson(); return false; @@ -314,7 +313,6 @@ bool StartPageLogic::importConnectionFromOpenVpnConfig(const QString &config) o[config_key::defaultContainer] = "amnezia-openvpn"; o[config_key::description] = m_settings->nextAvailableServerName(); - const static QRegularExpression dnsRegExp("dhcp-option DNS (\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\b)"); QRegularExpressionMatchIterator dnsMatch = dnsRegExp.globalMatch(config); if (dnsMatch.hasNext()) { @@ -338,9 +336,51 @@ bool StartPageLogic::importConnectionFromWireguardConfig(const QString &config) QRegularExpressionMatch hostNameAndPortMatch = hostNameAndPortRegExp.match(config); QString hostName; QString port; - if (hostNameAndPortMatch.hasMatch()) { + if (hostNameAndPortMatch.hasCaptured(1)) { hostName = hostNameAndPortMatch.captured(1); + } else { + return importConnection(QJsonObject()); + } + + if (hostNameAndPortMatch.hasCaptured(2)) { port = hostNameAndPortMatch.captured(2); + } else { + port = protocols::wireguard::defaultPort; + } + + lastConfig[config_key::hostName] = hostName; + lastConfig[config_key::port] = port.toInt(); + + const static QRegularExpression clientPrivKeyRegExp("PrivateKey = (.*)"); + QRegularExpressionMatch clientPrivKeyMatch = clientPrivKeyRegExp.match(config); + if (clientPrivKeyMatch.hasMatch()) { + lastConfig[config_key::client_priv_key] = clientPrivKeyMatch.captured(1); + } else { + return importConnection(QJsonObject()); + } + + const static QRegularExpression clientIpRegExp("Address = (\\d+\\.\\d+\\.\\d+\\.\\d+)"); + QRegularExpressionMatch clientIpMatch = clientIpRegExp.match(config); + if (clientIpMatch.hasMatch()) { + lastConfig[config_key::client_ip] = clientIpMatch.captured(1); + } else { + return importConnection(QJsonObject()); + } + + const static QRegularExpression pskKeyRegExp("PresharedKey = (.*)"); + QRegularExpressionMatch pskKeyMatch = pskKeyRegExp.match(config); + if (pskKeyMatch.hasMatch()) { + lastConfig[config_key::psk_key] = pskKeyMatch.captured(1); + } else { + return importConnection(QJsonObject()); + } + + const static QRegularExpression serverPubKeyRegExp("PublicKey = (.*)"); + QRegularExpressionMatch serverPubKeyMatch = serverPubKeyRegExp.match(config); + if (serverPubKeyMatch.hasMatch()) { + lastConfig[config_key::server_pub_key] = serverPubKeyMatch.captured(1); + } else { + return importConnection(QJsonObject()); } QJsonObject wireguardConfig; @@ -361,7 +401,9 @@ bool StartPageLogic::importConnectionFromWireguardConfig(const QString &config) o[config_key::defaultContainer] = "amnezia-wireguard"; o[config_key::description] = m_settings->nextAvailableServerName(); - const static QRegularExpression dnsRegExp("DNS = (\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\b).*(\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\b)"); + const static QRegularExpression dnsRegExp( + "DNS = " + "(\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\b).*(\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\b)"); QRegularExpressionMatch dnsMatch = dnsRegExp.match(config); if (dnsMatch.hasMatch()) { o[config_key::dns1] = dnsMatch.captured(1); From f5ab034aeb6b88ca6362b22ccb3986fd2d13a729 Mon Sep 17 00:00:00 2001 From: Mykola Baibuz Date: Tue, 19 Sep 2023 17:59:04 -0400 Subject: [PATCH 6/9] WG routing rework for Linux --- .../linux/daemon/linuxroutemonitor.cpp | 371 ++++++++++++++---- .../linux/daemon/linuxroutemonitor.h | 25 +- client/platforms/linux/daemon/pidtracker.cpp | 228 ----------- client/platforms/linux/daemon/pidtracker.h | 72 ---- service/server/CMakeLists.txt | 2 - service/server/router_linux.cpp | 146 +------ 6 files changed, 314 insertions(+), 530 deletions(-) delete mode 100644 client/platforms/linux/daemon/pidtracker.cpp delete mode 100644 client/platforms/linux/daemon/pidtracker.h diff --git a/client/platforms/linux/daemon/linuxroutemonitor.cpp b/client/platforms/linux/daemon/linuxroutemonitor.cpp index 80f510b7..f0c49eb6 100644 --- a/client/platforms/linux/daemon/linuxroutemonitor.cpp +++ b/client/platforms/linux/daemon/linuxroutemonitor.cpp @@ -4,27 +4,21 @@ #include "linuxroutemonitor.h" -#include "router_linux.h" - -#include -#include -#include -#include -#include -#include #include - -#include -#include - -#include -#include - #include #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include + #include "leakdetector.h" #include "logger.h" @@ -32,31 +26,57 @@ namespace { Logger logger("LinuxRouteMonitor"); } // namespace + +typedef struct wg_allowedip { + uint16_t family; + union { + struct in_addr ip4; + struct in6_addr ip6; + }; + uint8_t cidr; + struct wg_allowedip *next_allowedip; +} wg_allowedip; + +constexpr const char* WG_INTERFACE = "amn0"; + +static void nlmsg_append_attr(struct nlmsghdr* nlmsg, size_t maxlen, + int attrtype, const void* attrdata, + size_t attrlen); +static void nlmsg_append_attr32(struct nlmsghdr* nlmsg, size_t maxlen, + int attrtype, uint32_t value); + +static bool buildAllowedIp(wg_allowedip* ip, const IPAddress& prefix); + + LinuxRouteMonitor::LinuxRouteMonitor(const QString& ifname, QObject* parent) : QObject(parent), m_ifname(ifname) { MZ_COUNT_CTOR(LinuxRouteMonitor); logger.debug() << "LinuxRouteMonitor created."; - m_rtsock = socket(PF_ROUTE, SOCK_RAW, 0); - if (m_rtsock < 0) { - logger.error() << "Failed to create routing socket:" << strerror(errno); - return; + m_nlsock = socket(AF_NETLINK, SOCK_DGRAM, NETLINK_ROUTE); + if (m_nlsock < 0) { + logger.warning() << "Failed to create netlink socket:" << strerror(errno); } - RouterLinux &router = RouterLinux::Instance(); - m_defaultGatewayIpv4 = router.getgatewayandiface().toUtf8(); + 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_ifindex = if_nametoindex(qPrintable(ifname)); - m_notifier = new QSocketNotifier(m_rtsock, QSocketNotifier::Read, this); + m_notifier = new QSocketNotifier(m_nlsock, QSocketNotifier::Read, this); + connect(m_notifier, &QSocketNotifier::activated, this, + &LinuxRouteMonitor::nlsockReady); } LinuxRouteMonitor::~LinuxRouteMonitor() { MZ_COUNT_DTOR(LinuxRouteMonitor); - flushExclusionRoutes(); - if (m_rtsock >= 0) { - close(m_rtsock); + if (m_nlsock >= 0) { + close(m_nlsock); } - logger.debug() << "LinuxRouteMonitor destroyed."; + logger.debug() << "WireguardUtilsLinux destroyed."; } // Compare memory against zero. @@ -69,65 +89,266 @@ static int memcmpzero(const void* data, size_t len) { } bool LinuxRouteMonitor::insertRoute(const IPAddress& prefix) { - int temp_sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); + logger.debug() << "Adding route to" << prefix.toString(); - struct ifreq ifc; - int res; - - if(temp_sock < 0) - return -1; - strcpy(ifc.ifr_name, m_ifname.toUtf8()); - - res = ioctl(temp_sock, SIOCGIFADDR, &ifc); - if(res < 0) - return -1; - - RouterLinux &router = RouterLinux::Instance(); - logger.debug() << "prefix.toString() " << prefix.toString() << " m_ifname " << inet_ntoa(((struct sockaddr_in*)&ifc.ifr_addr)->sin_addr); - return router.routeAdd(prefix.toString(), inet_ntoa(((struct sockaddr_in*)&ifc.ifr_addr)->sin_addr), temp_sock); + const int flags = NLM_F_REQUEST | NLM_F_CREATE | NLM_F_REPLACE | NLM_F_ACK; + return rtmSendRoute(RTM_NEWROUTE, flags, RTN_UNICAST, prefix); } bool LinuxRouteMonitor::deleteRoute(const IPAddress& prefix) { - int temp_sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); + logger.debug() << "Removing route to" << prefix.toString(); - struct ifreq ifc; - int res; - if(temp_sock < 0) - temp_sock -1; - strcpy(ifc.ifr_name, m_ifname.toUtf8()); - - res = ioctl(temp_sock, SIOCGIFADDR, &ifc); - if(res < 0) - return -1; - - RouterLinux &router = RouterLinux::Instance(); - logger.debug() << "prefix.toString() " << prefix.toString() << " m_ifname " << inet_ntoa(((struct sockaddr_in*)&ifc.ifr_addr)->sin_addr); - return router.routeDelete(prefix.toString(), inet_ntoa(((struct sockaddr_in*)&ifc.ifr_addr)->sin_addr), temp_sock); + const int flags = NLM_F_REQUEST | NLM_F_ACK; + return rtmSendRoute(RTM_DELROUTE, flags, RTN_UNICAST, prefix); } bool LinuxRouteMonitor::addExclusionRoute(const IPAddress& prefix) { - logger.debug() << "Adding exclusion route for" - << logger.sensitive(prefix.toString()); - - int temp_sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); - RouterLinux &router = RouterLinux::Instance(); - logger.debug() << "prefix.toString() " << prefix.toString() << " m_defaultGatewayIpv4 " << m_defaultGatewayIpv4; - return router.routeAdd(prefix.toString(), m_defaultGatewayIpv4, temp_sock); - // Otherwise, the default route isn't known yet. Do nothing. - return true; + logger.debug() << "Adding exclusion route for" + << prefix.toString(); + const int flags = NLM_F_REQUEST | NLM_F_CREATE | NLM_F_REPLACE | NLM_F_ACK; + return rtmSendRoute(RTM_NEWROUTE, flags, RTN_THROW, prefix); } bool LinuxRouteMonitor::deleteExclusionRoute(const IPAddress& prefix) { - logger.debug() << "Deleting exclusion route for" - << logger.sensitive(prefix.toString()); - - int temp_sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); - RouterLinux &router = RouterLinux::Instance(); - logger.debug() << "prefix.toString() " << prefix.toString() << " m_defaultGatewayIpv4 " << m_defaultGatewayIpv4; - return router.routeDelete(prefix.toString(), m_defaultGatewayIpv4, temp_sock); + logger.debug() << "Removing exclusion route for" + << prefix.toString(); + const int flags = NLM_F_REQUEST | NLM_F_ACK; + return rtmSendRoute(RTM_DELROUTE, flags, RTN_THROW, prefix); } -void LinuxRouteMonitor::flushExclusionRoutes() { - RouterLinux &router = RouterLinux::Instance(); - router.clearSavedRoutes(); +bool LinuxRouteMonitor::rtmSendRoute(int action, int flags, int type, + const IPAddress& prefix) { + constexpr size_t rtm_max_size = sizeof(struct rtmsg) + + 2 * RTA_SPACE(sizeof(uint32_t)) + + RTA_SPACE(sizeof(struct in6_addr)); + wg_allowedip ip; + if (!buildAllowedIp(&ip, prefix)) { + logger.warning() << "Invalid destination prefix"; + return false; + } + + char buf[NLMSG_SPACE(rtm_max_size)]; + struct nlmsghdr* nlmsg = reinterpret_cast(buf); + struct rtmsg* rtm = static_cast(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 = type; + rtm->rtm_table = RT_TABLE_UNSPEC; + rtm->rtm_protocol = RTPROT_BOOT; + rtm->rtm_scope = RT_SCOPE_UNIVERSE; + + if (rtm->rtm_family == AF_INET6) { + nlmsg_append_attr(nlmsg, sizeof(buf), RTA_DST, &ip.ip6, sizeof(ip.ip6)); + } else { + nlmsg_append_attr(nlmsg, sizeof(buf), RTA_DST, &ip.ip4, sizeof(ip.ip4)); + } + + if (rtm->rtm_type == RTN_UNICAST) { + int index = if_nametoindex(WG_INTERFACE); + + if (index <= 0) { + logger.error() << "if_nametoindex() failed:" << strerror(errno); + return false; + } + nlmsg_append_attr32(nlmsg, sizeof(buf), RTA_OIF, index); + } + + if (rtm->rtm_type == RTN_THROW) { + int index = if_nametoindex(getgatewayandiface().toUtf8()); + if (index <= 0) { + logger.error() << "if_nametoindex() failed:" << strerror(errno); + return false; + } + nlmsg_append_attr32(nlmsg, 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); +} + +static void nlmsg_append_attr(struct nlmsghdr* nlmsg, size_t maxlen, + int attrtype, const void* attrdata, + size_t attrlen) { + size_t newlen = NLMSG_ALIGN(nlmsg->nlmsg_len) + RTA_SPACE(attrlen); + if (newlen <= maxlen) { + char* buf = reinterpret_cast(nlmsg) + NLMSG_ALIGN(nlmsg->nlmsg_len); + struct rtattr* attr = reinterpret_cast(buf); + 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(struct nlmsghdr* nlmsg, size_t maxlen, + int attrtype, uint32_t value) { + nlmsg_append_attr(nlmsg, maxlen, attrtype, &value, sizeof(value)); +} + +void LinuxRouteMonitor::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 = static_cast(NLMSG_DATA(nlmsg)); + if (err->error != 0) { + logger.debug() << "Netlink request failed:" << strerror(-err->error); + } + nlmsg = NLMSG_NEXT(nlmsg, len); + } +} + +#define BUFFER_SIZE 4096 + +QString LinuxRouteMonitor::getgatewayandiface() +{ + int received_bytes = 0, msg_len = 0, route_attribute_len = 0; + int sock = -1, msgseq = 0; + struct nlmsghdr *nlh, *nlmsg; + struct rtmsg *route_entry; + // This struct contain route attributes (route type) + struct rtattr *route_attribute; + char gateway_address[INET_ADDRSTRLEN], interface[IF_NAMESIZE]; + char msgbuf[BUFFER_SIZE], buffer[BUFFER_SIZE]; + char *ptr = buffer; + struct timeval tv; + + if ((sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE)) < 0) { + perror("socket failed"); + return ""; + } + + memset(msgbuf, 0, sizeof(msgbuf)); + memset(gateway_address, 0, sizeof(gateway_address)); + memset(interface, 0, sizeof(interface)); + memset(buffer, 0, sizeof(buffer)); + + /* point the header and the msg structure pointers into the buffer */ + nlmsg = (struct nlmsghdr *)msgbuf; + + /* Fill in the nlmsg header*/ + nlmsg->nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg)); + nlmsg->nlmsg_type = RTM_GETROUTE; // Get the routes from kernel routing table . + nlmsg->nlmsg_flags = NLM_F_DUMP | NLM_F_REQUEST; // The message is a request for dump. + nlmsg->nlmsg_seq = msgseq++; // Sequence of the message packet. + nlmsg->nlmsg_pid = getpid(); // PID of process sending the request. + + /* 1 Sec Timeout to avoid stall */ + tv.tv_sec = 1; + setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (struct timeval *)&tv, sizeof(struct timeval)); + /* send msg */ + if (send(sock, nlmsg, nlmsg->nlmsg_len, 0) < 0) { + perror("send failed"); + return ""; + } + + /* receive response */ + do + { + received_bytes = recv(sock, ptr, sizeof(buffer) - msg_len, 0); + if (received_bytes < 0) { + perror("Error in recv"); + return ""; + } + + nlh = (struct nlmsghdr *) ptr; + + /* Check if the header is valid */ + if((NLMSG_OK(nlmsg, received_bytes) == 0) || + (nlmsg->nlmsg_type == NLMSG_ERROR)) + { + perror("Error in received packet"); + return ""; + } + + /* If we received all data break */ + if (nlh->nlmsg_type == NLMSG_DONE) + break; + else { + ptr += received_bytes; + msg_len += received_bytes; + } + + /* Break if its not a multi part message */ + if ((nlmsg->nlmsg_flags & NLM_F_MULTI) == 0) + break; + } + while ((nlmsg->nlmsg_seq != msgseq) || (nlmsg->nlmsg_pid != getpid())); + + /* parse response */ + for ( ; NLMSG_OK(nlh, received_bytes); nlh = NLMSG_NEXT(nlh, received_bytes)) + { + /* Get the route data */ + route_entry = (struct rtmsg *) NLMSG_DATA(nlh); + + /* We are just interested in main routing table */ + if (route_entry->rtm_table != RT_TABLE_MAIN) + continue; + + route_attribute = (struct rtattr *) RTM_RTA(route_entry); + route_attribute_len = RTM_PAYLOAD(nlh); + + /* Loop through all attributes */ + for ( ; RTA_OK(route_attribute, route_attribute_len); + route_attribute = RTA_NEXT(route_attribute, route_attribute_len)) + { + switch(route_attribute->rta_type) { + case RTA_OIF: + if_indextoname(*(int *)RTA_DATA(route_attribute), interface); + break; + case RTA_GATEWAY: + inet_ntop(AF_INET, RTA_DATA(route_attribute), + gateway_address, sizeof(gateway_address)); + break; + default: + break; + } + } + + if ((*gateway_address) && (*interface)) { + logger.debug() << "Gateway " << gateway_address << " for interface " << interface; + break; + } + } + close(sock); + return interface; +} + +static bool buildAllowedIp(wg_allowedip* ip, + const IPAddress& prefix) { + const char* addrString = qPrintable(prefix.address().toString()); + if (prefix.type() == QAbstractSocket::IPv4Protocol) { + ip->family = AF_INET; + ip->cidr = prefix.prefixLength(); + return inet_pton(AF_INET, addrString, &ip->ip4) == 1; + } + if (prefix.type() == QAbstractSocket::IPv6Protocol) { + ip->family = AF_INET6; + ip->cidr = prefix.prefixLength(); + return inet_pton(AF_INET6, addrString, &ip->ip6) == 1; + } + return false; } diff --git a/client/platforms/linux/daemon/linuxroutemonitor.h b/client/platforms/linux/daemon/linuxroutemonitor.h index 872fcb7e..f1c3ac1d 100644 --- a/client/platforms/linux/daemon/linuxroutemonitor.h +++ b/client/platforms/linux/daemon/linuxroutemonitor.h @@ -13,9 +13,6 @@ #include "ipaddress.h" -struct if_msghdr; -struct rt_msghdr; -struct sockaddr; class LinuxRouteMonitor final : public QObject { Q_OBJECT @@ -26,28 +23,24 @@ class LinuxRouteMonitor final : public QObject { bool insertRoute(const IPAddress& prefix); bool deleteRoute(const IPAddress& prefix); - int interfaceFlags() { return m_ifflags; } bool addExclusionRoute(const IPAddress& prefix); bool deleteExclusionRoute(const IPAddress& prefix); - void flushExclusionRoutes(); - private: static QString addrToString(const struct sockaddr* sa); static QString addrToString(const QByteArray& data); - - QList m_exclusionRoutes; - QByteArray m_defaultGatewayIpv4; - QByteArray m_defaultGatewayIpv6; - unsigned int m_defaultIfindexIpv4 = 0; - unsigned int m_defaultIfindexIpv6 = 0; - + bool rtmSendRoute(int action, int flags, int type, + const IPAddress& prefix); + QString getgatewayandiface(); QString m_ifname; unsigned int m_ifindex = 0; - int m_ifflags = 0; - int m_rtsock = -1; - int m_rtseq = 0; + int m_nlsock = -1; + int m_nlseq = 0; QSocketNotifier* m_notifier = nullptr; + + private slots: + void nlsockReady(); + }; #endif // LINUXROUTEMONITOR_H diff --git a/client/platforms/linux/daemon/pidtracker.cpp b/client/platforms/linux/daemon/pidtracker.cpp deleted file mode 100644 index 76d53219..00000000 --- a/client/platforms/linux/daemon/pidtracker.cpp +++ /dev/null @@ -1,228 +0,0 @@ -/* 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 -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "leakdetector.h" -#include "logger.h" - -constexpr size_t CN_MCAST_MSG_SIZE = - sizeof(struct cn_msg) + sizeof(enum proc_cn_mcast_op); - -namespace { -Logger logger("PidTracker"); -} - -PidTracker::PidTracker(QObject* parent) : QObject(parent) { - MZ_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() { - MZ_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 iterator = kthreads.constBegin(); iterator != kthreads.constEnd(); - ++iterator) { - fprintf(fp, "%d\n", iterator.key()); - fflush(fp); - } - fclose(fp); - return true; -} diff --git a/client/platforms/linux/daemon/pidtracker.h b/client/platforms/linux/daemon/pidtracker.h deleted file mode 100644 index dc632b8b..00000000 --- a/client/platforms/linux/daemon/pidtracker.h +++ /dev/null @@ -1,72 +0,0 @@ -/* 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") { - MZ_COUNT_CTOR(ProcessGroup); - name = groupName; - rootpid = groupRootPid; - state = groupState; - refcount = 0; - } - ~ProcessGroup() { MZ_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/service/server/CMakeLists.txt b/service/server/CMakeLists.txt index 20ed8cb6..d2629a0b 100644 --- a/service/server/CMakeLists.txt +++ b/service/server/CMakeLists.txt @@ -209,7 +209,6 @@ if(LINUX) ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/daemon/dbustypeslinux.h ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/daemon/linuxdaemon.h ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/daemon/dnsutilslinux.h - ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/daemon/pidtracker.h ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/daemon/wireguardutilslinux.h ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/daemon/linuxroutemonitor.h ) @@ -220,7 +219,6 @@ if(LINUX) ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/linuxnetworkwatcherworker.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/linuxdependencies.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/daemon/dnsutilslinux.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/daemon/pidtracker.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/daemon/iputilslinux.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/daemon/linuxdaemon.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/daemon/wireguardutilslinux.cpp diff --git a/service/server/router_linux.cpp b/service/server/router_linux.cpp index d717ce9c..9410b146 100644 --- a/service/server/router_linux.cpp +++ b/service/server/router_linux.cpp @@ -14,17 +14,6 @@ #include #include #include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - #include RouterLinux &RouterLinux::Instance() @@ -33,123 +22,6 @@ RouterLinux &RouterLinux::Instance() return s; } -#define BUFFER_SIZE 4096 - -QString RouterLinux::getgatewayandiface() -{ - int received_bytes = 0, msg_len = 0, route_attribute_len = 0; - int sock = -1, msgseq = 0; - struct nlmsghdr *nlh, *nlmsg; - struct rtmsg *route_entry; - // This struct contain route attributes (route type) - struct rtattr *route_attribute; - char gateway_address[INET_ADDRSTRLEN], interface[IF_NAMESIZE]; - char msgbuf[BUFFER_SIZE], buffer[BUFFER_SIZE]; - char *ptr = buffer; - struct timeval tv; - - if ((sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE)) < 0) { - perror("socket failed"); - return ""; - } - - memset(msgbuf, 0, sizeof(msgbuf)); - memset(gateway_address, 0, sizeof(gateway_address)); - memset(interface, 0, sizeof(interface)); - memset(buffer, 0, sizeof(buffer)); - - /* point the header and the msg structure pointers into the buffer */ - nlmsg = (struct nlmsghdr *)msgbuf; - - /* Fill in the nlmsg header*/ - nlmsg->nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg)); - nlmsg->nlmsg_type = RTM_GETROUTE; // Get the routes from kernel routing table . - nlmsg->nlmsg_flags = NLM_F_DUMP | NLM_F_REQUEST; // The message is a request for dump. - nlmsg->nlmsg_seq = msgseq++; // Sequence of the message packet. - nlmsg->nlmsg_pid = getpid(); // PID of process sending the request. - - /* 1 Sec Timeout to avoid stall */ - tv.tv_sec = 1; - setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (struct timeval *)&tv, sizeof(struct timeval)); - /* send msg */ - if (send(sock, nlmsg, nlmsg->nlmsg_len, 0) < 0) { - perror("send failed"); - return ""; - } - - /* receive response */ - do - { - received_bytes = recv(sock, ptr, sizeof(buffer) - msg_len, 0); - if (received_bytes < 0) { - perror("Error in recv"); - return ""; - } - - nlh = (struct nlmsghdr *) ptr; - - /* Check if the header is valid */ - if((NLMSG_OK(nlmsg, received_bytes) == 0) || - (nlmsg->nlmsg_type == NLMSG_ERROR)) - { - perror("Error in received packet"); - return ""; - } - - /* If we received all data break */ - if (nlh->nlmsg_type == NLMSG_DONE) - break; - else { - ptr += received_bytes; - msg_len += received_bytes; - } - - /* Break if its not a multi part message */ - if ((nlmsg->nlmsg_flags & NLM_F_MULTI) == 0) - break; - } - while ((nlmsg->nlmsg_seq != msgseq) || (nlmsg->nlmsg_pid != getpid())); - - /* parse response */ - for ( ; NLMSG_OK(nlh, received_bytes); nlh = NLMSG_NEXT(nlh, received_bytes)) - { - /* Get the route data */ - route_entry = (struct rtmsg *) NLMSG_DATA(nlh); - - /* We are just interested in main routing table */ - if (route_entry->rtm_table != RT_TABLE_MAIN) - continue; - - route_attribute = (struct rtattr *) RTM_RTA(route_entry); - route_attribute_len = RTM_PAYLOAD(nlh); - - /* Loop through all attributes */ - for ( ; RTA_OK(route_attribute, route_attribute_len); - route_attribute = RTA_NEXT(route_attribute, route_attribute_len)) - { - switch(route_attribute->rta_type) { - case RTA_OIF: - if_indextoname(*(int *)RTA_DATA(route_attribute), interface); - break; - case RTA_GATEWAY: - inet_ntop(AF_INET, RTA_DATA(route_attribute), - gateway_address, sizeof(gateway_address)); - break; - default: - break; - } - } - - if ((*gateway_address) && (*interface)) { - qDebug().noquote() << "Gateway " << gateway_address << " for interface " << interface; - break; - } - } - close(sock); - return gateway_address; -} - - bool RouterLinux::routeAdd(const QString &ipWithSubnet, const QString &gw, const int &sock) { QString ip = Utils::ipAddressFromIpWithSubnet(ipWithSubnet); @@ -157,7 +29,7 @@ bool RouterLinux::routeAdd(const QString &ipWithSubnet, const QString &gw, const if (!Utils::checkIPv4Format(ip) || !Utils::checkIPv4Format(gw)) { qCritical().noquote() << "Critical, trying to add invalid route: " << ip << gw; - return true; + return false; } struct rtentry route; @@ -181,11 +53,11 @@ bool RouterLinux::routeAdd(const QString &ipWithSubnet, const QString &gw, const if (int err = ioctl(sock, SIOCADDRT, &route) < 0) { - // qDebug().noquote() << "route add error: gw " - // << ((struct sockaddr_in *)&route.rt_gateway)->sin_addr.s_addr - // << " ip " << ((struct sockaddr_in *)&route.rt_dst)->sin_addr.s_addr - // << " mask " << ((struct sockaddr_in *)&route.rt_genmask)->sin_addr.s_addr << " " << err; - // return false; + qDebug().noquote() << "route add error: gw " + << ((struct sockaddr_in *)&route.rt_gateway)->sin_addr.s_addr + << " ip " << ((struct sockaddr_in *)&route.rt_dst)->sin_addr.s_addr + << " mask " << ((struct sockaddr_in *)&route.rt_genmask)->sin_addr.s_addr << " " << err; + return false; } m_addedRoutes.append({ipWithSubnet, gw}); @@ -227,7 +99,7 @@ bool RouterLinux::routeDelete(const QString &ipWithSubnet, const QString &gw, co if (!Utils::checkIPv4Format(ip) || !Utils::checkIPv4Format(gw)) { qCritical().noquote() << "Critical, trying to remove invalid route: " << ip << gw; - return true; + return false; } if (ip == "0.0.0.0") { @@ -257,8 +129,8 @@ bool RouterLinux::routeDelete(const QString &ipWithSubnet, const QString &gw, co if (ioctl(sock, SIOCDELRT, &route) < 0) { - // qDebug().noquote() << "route delete error: gw " << gw << " ip " << ip << " mask " << mask; - // return false; + qDebug().noquote() << "route delete error: gw " << gw << " ip " << ip << " mask " << mask; + return false; } return true; } From 52e5453d56f8a024c2a9b54a4cef75ac99494ddc Mon Sep 17 00:00:00 2001 From: Mykola Baibuz Date: Wed, 20 Sep 2023 14:27:28 -0400 Subject: [PATCH 7/9] Upload AWG binary --- client/3rd-prebuilt | 2 +- client/daemon/daemonlocalserver.cpp | 4 ++-- client/mozilla/shared/ipaddress.cpp | 2 -- client/platforms/linux/daemon/wireguardutilslinux.cpp | 2 +- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/client/3rd-prebuilt b/client/3rd-prebuilt index bc450df2..e8795854 160000 --- a/client/3rd-prebuilt +++ b/client/3rd-prebuilt @@ -1 +1 @@ -Subproject commit bc450df2418540869e97a31c4dd76839989b1fa6 +Subproject commit e8795854a5cf27004fe78caecc90a961688d1d41 diff --git a/client/daemon/daemonlocalserver.cpp b/client/daemon/daemonlocalserver.cpp index 9d8feb68..9b8e9504 100644 --- a/client/daemon/daemonlocalserver.cpp +++ b/client/daemon/daemonlocalserver.cpp @@ -77,12 +77,12 @@ QString DaemonLocalServer::daemonPath() const { } if (dir.exists("amneziavpn")) { - logger.debug() << "/var/run/amnezia seems to be usable"; + logger.debug() << "/var/run/amneziavpn seems to be usable"; return VAR_PATH; } if (!dir.mkdir("amneziavpn")) { - logger.warning() << "Failed to create /var/run/amnezia"; + logger.warning() << "Failed to create /var/run/amneziavpn"; return TMP_PATH; } diff --git a/client/mozilla/shared/ipaddress.cpp b/client/mozilla/shared/ipaddress.cpp index 1f84ad07..95a2f2d0 100644 --- a/client/mozilla/shared/ipaddress.cpp +++ b/client/mozilla/shared/ipaddress.cpp @@ -29,8 +29,6 @@ IPAddress::IPAddress(const QString& ip) { if (m_prefixLength >= 128) { m_prefixLength = 128; } - } else { - // Q_ASSERT(false); } } diff --git a/client/platforms/linux/daemon/wireguardutilslinux.cpp b/client/platforms/linux/daemon/wireguardutilslinux.cpp index 32150ad5..a8b7b04a 100644 --- a/client/platforms/linux/daemon/wireguardutilslinux.cpp +++ b/client/platforms/linux/daemon/wireguardutilslinux.cpp @@ -76,7 +76,7 @@ bool WireguardUtilsLinux::addInterface(const InterfaceConfig& config) { QDir appPath(QCoreApplication::applicationDirPath()); QStringList wgArgs = {"-f", "amn0"}; - m_tunnel.start(appPath.filePath("wireguard-go"), wgArgs); + m_tunnel.start(appPath.filePath("../../client/bin/wireguard-go"), wgArgs); if (!m_tunnel.waitForStarted(WG_TUN_PROC_TIMEOUT)) { logger.error() << "Unable to start tunnel process due to timeout"; m_tunnel.kill(); From 97a72a9ee2d2041cb858b036909e12a3f206fa19 Mon Sep 17 00:00:00 2001 From: pokamest Date: Thu, 21 Sep 2023 11:28:18 +0100 Subject: [PATCH 8/9] build_windows.bat fix --- deploy/PrivacyTechAppleCertDeveloperId.p12 | Bin 3336 -> 0 bytes deploy/PrivacyTechAppleCertInstallerId.p12 | Bin 3332 -> 0 bytes deploy/PrivacyTechWindowsCert.pfx | Bin 9546 -> 0 bytes deploy/WWDRCA.cer | Bin 1062 -> 0 bytes deploy/build_windows.bat | 36 ++++++++------------- 5 files changed, 13 insertions(+), 23 deletions(-) delete mode 100755 deploy/PrivacyTechAppleCertDeveloperId.p12 delete mode 100755 deploy/PrivacyTechAppleCertInstallerId.p12 delete mode 100644 deploy/PrivacyTechWindowsCert.pfx delete mode 100644 deploy/WWDRCA.cer diff --git a/deploy/PrivacyTechAppleCertDeveloperId.p12 b/deploy/PrivacyTechAppleCertDeveloperId.p12 deleted file mode 100755 index a04ec85a02ac7e68cd42ccd951d286abb33fe7ae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3336 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 diff --git a/deploy/PrivacyTechWindowsCert.pfx b/deploy/PrivacyTechWindowsCert.pfx deleted file mode 100644 index 42c49a339f02c9f69fbfcad55f92bf99830662b7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9546 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 D2`7-6 diff --git a/deploy/build_windows.bat b/deploy/build_windows.bat index b6892f3d..7ae3e9f6 100644 --- a/deploy/build_windows.bat +++ b/deploy/build_windows.bat @@ -14,41 +14,31 @@ set PROJECT_DIR=%cd% set SCRIPT_DIR=%PROJECT_DIR:"=%\deploy set WORK_DIR=%SCRIPT_DIR:"=%\build_%BUILD_ARCH:"=% -rmdir /Q /S %WORK_DIR% -mkdir %WORK_DIR% - - set APP_NAME=AmneziaVPN set APP_FILENAME=%APP_NAME:"=%.exe set APP_DOMAIN=org.amneziavpn.package -set RELEASE_DIR=%WORK_DIR:"=% -set OUT_APP_DIR=%RELEASE_DIR:"=%\client\release -set PREBILT_DEPLOY_DATA_DIR=%SCRIPT_DIR:"=%\data\deploy-prebuilt\windows\x%BUILD_ARCH:"=% +set OUT_APP_DIR=%WORK_DIR:"=%\client\release +set PREBILT_DEPLOY_DATA_DIR=%PROJECT_DIR:"=%\client\3rd-prebuilt\deploy-prebuilt\windows\x%BUILD_ARCH:"=% set DEPLOY_DATA_DIR=%SCRIPT_DIR:"=%\data\windows\x%BUILD_ARCH:"=% -set INSTALLER_DATA_DIR=%RELEASE_DIR:"=%\installer\packages\%APP_DOMAIN:"=%\data +set INSTALLER_DATA_DIR=%WORK_DIR:"=%\installer\packages\%APP_DOMAIN:"=%\data set TARGET_FILENAME=%PROJECT_DIR:"=%\%APP_NAME:"=%_x%BUILD_ARCH:"=%.exe echo "Environment:" +echo "WORK_DIR: %WORK_DIR%" echo "APP_FILENAME: %APP_FILENAME%" echo "PROJECT_DIR: %PROJECT_DIR%" echo "SCRIPT_DIR: %SCRIPT_DIR%" -echo "RELEASE_DIR: %RELEASE_DIR%" echo "OUT_APP_DIR: %OUT_APP_DIR%" echo "DEPLOY_DATA_DIR: %DEPLOY_DATA_DIR%" echo "INSTALLER_DATA_DIR: %INSTALLER_DATA_DIR%" -echo "QMAKE_STASH_FILE: %QMAKE_STASH_FILE%" echo "TARGET_FILENAME: %TARGET_FILENAME%" -rem Signing staff -powershell Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Scope LocalMachine -powershell Get-ExecutionPolicy -List - -powershell Import-PfxCertificate -FilePath %SCRIPT_DIR:"=%\PrivacyTechWindowsCert.pfx -CertStoreLocation Cert:\LocalMachine\My -Password $(ConvertTo-SecureString -String $Env:WIN_CERT_PW -AsPlainText -Force) - echo "Cleanup..." -Rmdir /Q /S %RELEASE_DIR% +rmdir /Q /S %WORK_DIR% Del %TARGET_FILENAME% +mkdir %WORK_DIR% + call "%QT_BIN_DIR:"=%\qt-cmake" --version "%QT_BIN_DIR:"=%\windeployqt" -v cmake --version @@ -69,11 +59,11 @@ copy "%WORK_DIR:"=%\service\server\release\%APP_NAME:"=%-service.exe" %OUT_APP_D echo "Signing exe" cd %OUT_APP_DIR% -signtool sign /v /sm /s My /n "Privacy Technologies OU" /fd sha256 /tr http://timestamp.comodoca.com/?td=sha256 /td sha256 *.exe - +signtool sign /v /n "Privacy Technologies OU" /fd sha256 /tr http://timestamp.comodoca.com/?td=sha256 /td sha256 *.exe + "%QT_BIN_DIR:"=%\windeployqt" --release --qmldir "%PROJECT_DIR:"=%\client" --force --no-translations "%OUT_APP_DIR:"=%\%APP_FILENAME:"=%" -signtool sign /v /sm /s My /n "Privacy Technologies OU" /fd sha256 /tr http://timestamp.comodoca.com/?td=sha256 /td sha256 *.dll +signtool sign /v /n "Privacy Technologies OU" /fd sha256 /tr http://timestamp.comodoca.com/?td=sha256 /td sha256 *.dll echo "Copying deploy data..." xcopy %DEPLOY_DATA_DIR% %OUT_APP_DIR% /s /e /y /i /f @@ -81,7 +71,7 @@ xcopy %PREBILT_DEPLOY_DATA_DIR% %OUT_APP_DIR% /s /e /y /i /f copy "%WORK_DIR:"=%\service\wireguard-service\release\wireguard-service.exe" %OUT_APP_DIR%\wireguard\ cd %SCRIPT_DIR% -xcopy %SCRIPT_DIR:"=%\installer %RELEASE_DIR:"=%\installer /s /e /y /i /f +xcopy %SCRIPT_DIR:"=%\installer %WORK_DIR:"=%\installer /s /e /y /i /f mkdir %INSTALLER_DATA_DIR% echo "Deploy finished, content:" @@ -91,14 +81,14 @@ cd %OUT_APP_DIR% echo "Compressing data..." "%QIF_BIN_DIR:"=%\archivegen" -c 9 %INSTALLER_DATA_DIR:"=%\%APP_NAME:"=%.7z . -cd "%RELEASE_DIR:"=%\installer" +cd "%WORK_DIR:"=%\installer" echo "Creating installer..." "%QIF_BIN_DIR:"=%\binarycreator" --offline-only -v -c config\windows.xml -p packages -f %TARGET_FILENAME% timeout 5 cd %PROJECT_DIR% -signtool sign /v /sm /s My /n "Privacy Technologies OU" /fd sha256 /tr http://timestamp.comodoca.com/?td=sha256 /td sha256 "%TARGET_FILENAME%" +signtool sign /v /n "Privacy Technologies OU" /fd sha256 /tr http://timestamp.comodoca.com/?td=sha256 /td sha256 "%TARGET_FILENAME%" echo "Finished, see %TARGET_FILENAME%" exit 0 From 665f2412f1fdab94f18e3d21e01c0f73802517c1 Mon Sep 17 00:00:00 2001 From: pokamest Date: Thu, 21 Sep 2023 05:14:15 -0700 Subject: [PATCH 9/9] Version bump, macos/ios build fix [no ci] --- CMakeLists.txt | 4 ++-- deploy/build_ios.sh | 2 +- deploy/build_macos.sh | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 5ecde0cd..f6f94a23 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,11 +2,11 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR) set(PROJECT AmneziaVPN) -project(${PROJECT} VERSION 3.1.0.0 +project(${PROJECT} VERSION 3.1.0.1 DESCRIPTION "AmneziaVPN" HOMEPAGE_URL "https://amnezia.org/" ) -set(RELEASE_DATE "2023-08-28") +set(RELEASE_DATE "2023-09-21") set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH}) if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") diff --git a/deploy/build_ios.sh b/deploy/build_ios.sh index c4c0691a..7f16b916 100755 --- a/deploy/build_ios.sh +++ b/deploy/build_ios.sh @@ -34,7 +34,7 @@ clang -v # Generate XCodeProj $QT_BIN_DIR/qt-cmake . -B $BUILD_DIR -GXcode -DQT_HOST_PATH=$QT_MACOS_ROOT_DIR -KEYCHAIN=amnezia.build.keychain +KEYCHAIN=amnezia.build.ios.keychain KEYCHAIN_FILE=$HOME/Library/Keychains/${KEYCHAIN}-db # Setup keychain diff --git a/deploy/build_macos.sh b/deploy/build_macos.sh index 54b6dbe3..700198e7 100755 --- a/deploy/build_macos.sh +++ b/deploy/build_macos.sh @@ -79,7 +79,7 @@ if [ "${MAC_CERT_PW+x}" ]; then CERTIFICATE_P12=$DEPLOY_DIR/PrivacyTechAppleCertDeveloperId.p12 WWDRCA=$DEPLOY_DIR/WWDRCA.cer - KEYCHAIN=amnezia.build.keychain + KEYCHAIN=amnezia.build.macos.keychain TEMP_PASS=tmp_pass security create-keychain -p $TEMP_PASS $KEYCHAIN || true