diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..04e9030a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,42 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Log files** +Attach log files to help explain your problem. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. Windows 10] + - Version [e.g. 2.1.2] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Version [e.g. 2.1.2] + +**Server (please complete the following information):** + - OS: [e.g. Ubuntu 22.04] + +**Additional context** +Add any other context about the problem here. diff --git a/client/3rd-prebuilt b/client/3rd-prebuilt index 75ab7e24..bc450df2 160000 --- a/client/3rd-prebuilt +++ b/client/3rd-prebuilt @@ -1 +1 @@ -Subproject commit 75ab7e2418b83af7f8ed0a448ec5081b37b54442 +Subproject commit bc450df2418540869e97a31c4dd76839989b1fa6 diff --git a/client/android/res/drawable-hdpi/icon.png b/client/android/res/drawable-hdpi/icon.png index 657c547c..d0f20363 100644 Binary files a/client/android/res/drawable-hdpi/icon.png and b/client/android/res/drawable-hdpi/icon.png differ diff --git a/client/android/res/drawable-ldpi/icon.png b/client/android/res/drawable-ldpi/icon.png index 2b0f2ad8..00b978f5 100644 Binary files a/client/android/res/drawable-ldpi/icon.png and b/client/android/res/drawable-ldpi/icon.png differ diff --git a/client/android/res/drawable-mdpi/icon.png b/client/android/res/drawable-mdpi/icon.png index eb9f49ad..e23a94a9 100644 Binary files a/client/android/res/drawable-mdpi/icon.png and b/client/android/res/drawable-mdpi/icon.png differ diff --git a/client/android/res/drawable-xhdpi/icon.png b/client/android/res/drawable-xhdpi/icon.png index bd1cf1af..0c5408f8 100644 Binary files a/client/android/res/drawable-xhdpi/icon.png and b/client/android/res/drawable-xhdpi/icon.png differ diff --git a/client/android/res/drawable-xxhdpi/icon.png b/client/android/res/drawable-xxhdpi/icon.png index dc07319f..0f94f39e 100644 Binary files a/client/android/res/drawable-xxhdpi/icon.png and b/client/android/res/drawable-xxhdpi/icon.png differ diff --git a/client/android/res/drawable-xxxhdpi/icon.png b/client/android/res/drawable-xxxhdpi/icon.png index 8975ea0c..671b0ff9 100644 Binary files a/client/android/res/drawable-xxxhdpi/icon.png and b/client/android/res/drawable-xxxhdpi/icon.png differ 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 66fec366..7466d1af 100644 --- a/client/protocols/wireguardprotocol.cpp +++ b/client/protocols/wireguardprotocol.cpp @@ -18,8 +18,8 @@ WireguardProtocol::WireguardProtocol(const QJsonObject &configuration, QObject * // MZ #if defined(MZ_LINUX) - // m_impl.reset(new LinuxController()); -#elif defined(MZ_MACOS) // || defined(MZ_WINDOWS) + //m_impl.reset(new LinuxController()); +#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) { @@ -39,7 +39,7 @@ WireguardProtocol::~WireguardProtocol() void WireguardProtocol::stop() { -#ifdef Q_OS_MAC +#if defined(Q_OS_MAC) || defined(Q_OS_WIN) stopMzImpl(); return; #endif @@ -100,9 +100,11 @@ void WireguardProtocol::stop() setConnectionState(Vpn::ConnectionState::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; } @@ -172,7 +174,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 5d421763..bce653fd 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