diff --git a/CMakeLists.txt b/CMakeLists.txt index 06e92993..42cd7799 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR) set(PROJECT AmneziaVPN) -project(${PROJECT} VERSION 4.2.1.0 +project(${PROJECT} VERSION 4.2.1.1 DESCRIPTION "AmneziaVPN" HOMEPAGE_URL "https://amnezia.org/" ) @@ -11,7 +11,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d") set(RELEASE_DATE "${CURRENT_DATE}") set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH}) -set(APP_ANDROID_VERSION_CODE 42) +set(APP_ANDROID_VERSION_CODE 43) if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") set(MZ_PLATFORM_NAME "linux") diff --git a/client/containers/containers_defs.cpp b/client/containers/containers_defs.cpp index b4014051..206faa35 100644 --- a/client/containers/containers_defs.cpp +++ b/client/containers/containers_defs.cpp @@ -98,11 +98,11 @@ QMap ContainerProps::containerDescriptions() QObject::tr("OpenVPN is the most popular VPN protocol, with flexible configuration options. It uses its " "own security protocol with SSL/TLS for key exchange.") }, { DockerContainer::ShadowSocks, - QObject::tr("ShadowSocks - masks VPN traffic, making it similar to normal web traffic, but is " - "recognised by analysis systems in some highly censored regions.") }, + QObject::tr("ShadowSocks - masks VPN traffic, making it similar to normal web traffic, but it " + "may be recognized by analysis systems in some highly censored regions.") }, { DockerContainer::Cloak, QObject::tr("OpenVPN over Cloak - OpenVPN with VPN masquerading as web traffic and protection against " - "active-probbing detection. Ideal for bypassing blocking in regions with the highest levels " + "active-probing detection. Ideal for bypassing blocking in regions with the highest levels " "of censorship.") }, { DockerContainer::WireGuard, QObject::tr("WireGuard - New popular VPN protocol with high performance, high speed and low power " @@ -119,7 +119,7 @@ QMap ContainerProps::containerDescriptions() { DockerContainer::Dns, QObject::tr("Replace the current DNS server with your own. This will increase your privacy level.") }, { DockerContainer::Sftp, - QObject::tr("Creates a file vault on your server to securely store and transfer files.") } }; + QObject::tr("Create a file vault on your server to securely store and transfer files.") } }; } QMap ContainerProps::containerDetailedDescriptions() @@ -153,8 +153,8 @@ QMap ContainerProps::containerDetailedDescriptions() "* Works over TCP network protocol.") }, { DockerContainer::Cloak, QObject::tr("This is a combination of the OpenVPN protocol and the Cloak plugin designed specifically for " - "blocking protection.\n\n" - "OpenVPN provides a secure VPN connection by encrypting all Internet traffic between the client " + "protecting against blocking.\n\n" + "OpenVPN provides a secure VPN connection by encrypting all internet traffic between the client " "and the server.\n\n" "Cloak protects OpenVPN from detection and blocking. \n\n" "Cloak can modify packet metadata so that it completely masks VPN traffic as normal web traffic, " @@ -172,7 +172,7 @@ QMap ContainerProps::containerDetailedDescriptions() "* Works over TCP network protocol, 443 port.\n") }, { DockerContainer::WireGuard, QObject::tr("A relatively new popular VPN protocol with a simplified architecture.\n" - "Provides stable VPN connection, high performance on all devices. Uses hard-coded encryption " + "WireGuard provides stable VPN connection and high performance on all devices. It uses hard-coded encryption " "settings. WireGuard compared to OpenVPN has lower latency and better data transfer throughput.\n" "WireGuard is very susceptible to blocking due to its distinct packet signatures. " "Unlike some other VPN protocols that employ obfuscation techniques, " diff --git a/client/platforms/ios/ioslogger.swift b/client/platforms/ios/ioslogger.swift index 595c2712..747227d1 100644 --- a/client/platforms/ios/ioslogger.swift +++ b/client/platforms/ios/ioslogger.swift @@ -17,41 +17,41 @@ public class Logger { deinit {} func log(message: String) { - let suiteName = "group.org.amnezia.AmneziaVPN" - let logKey = "logMessages" - let sharedDefaults = UserDefaults(suiteName: suiteName) - var logs = sharedDefaults?.array(forKey: logKey) as? [String] ?? [] - logs.append(message) - sharedDefaults?.set(logs, forKey: logKey) +// let suiteName = "group.org.amnezia.AmneziaVPN" +// let logKey = "logMessages" +// let sharedDefaults = UserDefaults(suiteName: suiteName) +// var logs = sharedDefaults?.array(forKey: logKey) as? [String] ?? [] +// logs.append(message) +// sharedDefaults?.set(logs, forKey: logKey) } - func writeLog(to targetFile: String) -> Bool { + private func writeLog(to targetFile: String) -> Bool { return true; } static func configureGlobal(tagged tag: String, withFilePath filePath: String?) { - if Logger.global != nil { - return - } - - Logger.global = Logger(tagged: tag) - - var appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown version" - - if let appBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String { - appVersion += " (\(appBuild))" - } - - Logger.global?.log(message: "App version: \(appVersion)") +// if Logger.global != nil { +// return +// } +// +// Logger.global = Logger(tagged: tag) +// +// var appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown version" +// +// if let appBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String { +// appVersion += " (\(appBuild))" +// } +// +// Logger.global?.log(message: "App version: \(appVersion)") } } func wg_log(_ type: OSLogType, staticMessage msg: StaticString) { - os_log(msg, log: OSLog.default, type: type) - Logger.global?.log(message: "\(msg)") +// os_log(msg, log: OSLog.default, type: type) +// Logger.global?.log(message: "\(msg)") } func wg_log(_ type: OSLogType, message msg: String) { - os_log("%{AMNEZIA}s", log: OSLog.default, type: type, msg) - Logger.global?.log(message: msg) +// os_log("%{AMNEZIA}s", log: OSLog.default, type: type, msg) +// Logger.global?.log(message: msg) } diff --git a/client/platforms/linux/daemon/linuxfirewall.cpp b/client/platforms/linux/daemon/linuxfirewall.cpp new file mode 100644 index 00000000..393c24f2 --- /dev/null +++ b/client/platforms/linux/daemon/linuxfirewall.cpp @@ -0,0 +1,518 @@ +// Copyright (c) 2023 Private Internet Access, Inc. +// +// This file is part of the Private Internet Access Desktop Client. +// +// The Private Internet Access Desktop Client is free software: you can +// redistribute it and/or modify it under the terms of the GNU General Public +// License as published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. +// +// The Private Internet Access Desktop Client is distributed in the hope that +// it will be useful, but WITHOUT ANY WARRANTY; without even the implied +// warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with the Private Internet Access Desktop Client. If not, see +// . + +// Copyright (c) 2024 AmneziaVPN +// This file has been modified for AmneziaVPN +// +// This file is based on the work of the Private Internet Access Desktop Client. +// The original code of the Private Internet Access Desktop Client is copyrighted (c) 2023 Private Internet Access, Inc. and licensed under GPL3. +// +// The modified version of this file is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this file. If not, see . + +#include "linuxfirewall.h" +#include "logger.h" +#include + +#define BRAND_CODE "amn" + +namespace { +Logger logger("LinuxFirewall"); +} // namespace + +namespace +{ +const QString kAnchorName{BRAND_CODE "vpn"}; +const QString kPacketTag{"0x3211"}; +const QString kCGroupId{"0x567"}; +const QString enabledKeyTemplate = "enabled:%1:%2"; +const QString disabledKeyTemplate = "disabled:%1:%2"; +const QString kVpnGroupName = BRAND_CODE "vpn"; +QHash anchorCallbacks; +} + +QString LinuxFirewall::kRtableName = QStringLiteral("%1rt").arg(kAnchorName); +QString LinuxFirewall::kOutputChain = QStringLiteral("OUTPUT"); +QString LinuxFirewall::kPostRoutingChain = QStringLiteral("POSTROUTING"); +QString LinuxFirewall::kPreRoutingChain = QStringLiteral("PREROUTING"); +QString LinuxFirewall::kRootChain = QStringLiteral("%1.anchors").arg(kAnchorName); +QString LinuxFirewall::kFilterTable = QStringLiteral("filter"); +QString LinuxFirewall::kNatTable = QStringLiteral("nat"); +QString LinuxFirewall::kRawTable = QStringLiteral("raw"); +QString LinuxFirewall::kMangleTable = QStringLiteral("mangle"); + +static QString getCommand(LinuxFirewall::IPVersion ip) +{ + return ip == LinuxFirewall::IPv6 ? QStringLiteral("ip6tables") : QStringLiteral("iptables"); +} + +int LinuxFirewall::createChain(LinuxFirewall::IPVersion ip, const QString& chain, const QString& tableName) +{ + if (ip == Both) + { + int result4 = createChain(IPv4, chain, tableName); + int result6 = createChain(IPv6, chain, tableName); + return result4 ? result4 : result6; + } + const QString cmd = getCommand(ip); + return execute(QStringLiteral("%1 -N %2 -t %3 || %1 -F %2 -t %3").arg(cmd, chain, tableName)); +} + +int LinuxFirewall::deleteChain(LinuxFirewall::IPVersion ip, const QString& chain, const QString& tableName) +{ + if (ip == Both) + { + int result4 = deleteChain(IPv4, chain, tableName); + int result6 = deleteChain(IPv6, chain, tableName); + return result4 ? result4 : result6; + } + const QString cmd = getCommand(ip); + return execute(QStringLiteral("if %1 -L %2 -n -t %3 > /dev/null 2> /dev/null ; then %1 -F %2 -t %3 && %1 -X %2 -t %3; fi").arg(cmd, chain, tableName)); +} + +int LinuxFirewall::linkChain(LinuxFirewall::IPVersion ip, const QString& chain, const QString& parent, bool mustBeFirst, const QString& tableName) +{ + if (ip == Both) + { + int result4 = linkChain(IPv4, chain, parent, mustBeFirst, tableName); + int result6 = linkChain(IPv6, chain, parent, mustBeFirst, tableName); + return result4 ? result4 : result6; + } + const QString cmd = getCommand(ip); + if (mustBeFirst) + { + // This monster shell script does the following: + // 1. Check if a rule with the appropriate target exists at the top of the parent chain + // 2. If not, insert a jump rule at the top of the parent chain + // 3. Look for and delete a single rule with the designated target at an index > 1 + // (we can't safely delete all rules at once since rule numbers change) + // TODO: occasionally this script results in warnings in logs "Bad rule (does a matching rule exist in the chain?)" - this happens when + // the e.g OUTPUT chain is empty but this script attempts to delete things from it anyway. It doesn't cause any problems, but we should still fix at some point.. + return execute(QStringLiteral("if ! %1 -L %2 -n --line-numbers -t %4 2> /dev/null | awk 'int($1) == 1 && $2 == \"%3\" { found=1 } END { if(found==1) { exit 0 } else { exit 1 } }' ; then %1 -I %2 -j %3 -t %4 && %1 -L %2 -n --line-numbers -t %4 2> /dev/null | awk 'int($1) > 1 && $2 == \"%3\" { print $1; exit }' | xargs %1 -t %4 -D %2 ; fi").arg(cmd, parent, chain, tableName)); + } + else + return execute(QStringLiteral("if ! %1 -C %2 -j %3 -t %4 2> /dev/null ; then %1 -A %2 -j %3 -t %4; fi").arg(cmd, parent, chain, tableName)); +} + +int LinuxFirewall::unlinkChain(LinuxFirewall::IPVersion ip, const QString& chain, const QString& parent, const QString& tableName) +{ + if (ip == Both) + { + int result4 = unlinkChain(IPv4, chain, parent, tableName); + int result6 = unlinkChain(IPv6, chain, parent, tableName); + return result4 ? result4 : result6; + } + const QString cmd = getCommand(ip); + return execute(QStringLiteral("if %1 -C %2 -j %3 -t %4 2> /dev/null ; then %1 -D %2 -j %3 -t %4; fi").arg(cmd, parent, chain, tableName)); +} + +void LinuxFirewall::ensureRootAnchorPriority(LinuxFirewall::IPVersion ip) +{ + linkChain(ip, kRootChain, kOutputChain, true); +} + +void LinuxFirewall::installAnchor(LinuxFirewall::IPVersion ip, const QString& anchor, const QStringList& rules, const QString& tableName, + const FilterCallbackFunc& enableFunc, const FilterCallbackFunc& disableFunc) +{ + if (ip == Both) + { + installAnchor(IPv4, anchor, rules, tableName, enableFunc, disableFunc); + installAnchor(IPv6, anchor, rules, tableName, enableFunc, disableFunc); + return; + } + + const QString cmd = getCommand(ip); + const QString anchorChain = QStringLiteral("%1.a.%2").arg(kAnchorName, anchor); + const QString actualChain = QStringLiteral("%1.%2").arg(kAnchorName, anchor); + + // Start by defining a placeholder chain, which stays locked into place + // in the root chain without being removed or recreated, ensuring the + // intended precedence order. + createChain(ip, anchorChain, tableName); + linkChain(ip, anchorChain, kRootChain, false, tableName); + + if(enableFunc) + { + const QString key = enabledKeyTemplate.arg(tableName, anchor); + if(!anchorCallbacks.contains(key)) anchorCallbacks[key] = enableFunc; + } + if(disableFunc) + { + const QString key = disabledKeyTemplate.arg(tableName, anchor); + if(!anchorCallbacks.contains(key)) anchorCallbacks[key] = disableFunc; + } + + // Create the actual rule chain, which we'll insert or remove from the + // placeholder anchor when needed. + createChain(ip, actualChain, tableName); + for (const QString& rule : rules) + execute(QStringLiteral("%1 -A %2 %3 -t %4").arg(cmd, actualChain, rule, tableName)); +} + +void LinuxFirewall::uninstallAnchor(LinuxFirewall::IPVersion ip, const QString& anchor, const QString& tableName) +{ + if (ip == Both) + { + uninstallAnchor(IPv4, anchor, tableName); + uninstallAnchor(IPv6, anchor, tableName); + return; + } + + const QString cmd = getCommand(ip); + const QString anchorChain = QStringLiteral("%1.a.%2").arg(kAnchorName, anchor); + const QString actualChain = QStringLiteral("%1.%2").arg(kAnchorName, anchor); + + unlinkChain(ip, anchorChain, kRootChain, tableName); + deleteChain(ip, anchorChain, tableName); + deleteChain(ip, actualChain, tableName); +} + +QStringList LinuxFirewall::getDNSRules(const QStringList& servers) +{ + QStringList result; + for (const QString& server : servers) + { + result << QStringLiteral("-o amn0+ -d %1 -p udp --dport 53 -j ACCEPT").arg(server); + result << QStringLiteral("-o amn0+ -d %1 -p tcp --dport 53 -j ACCEPT").arg(server); + result << QStringLiteral("-o tun0+ -d %1 -p udp --dport 53 -j ACCEPT").arg(server); + result << QStringLiteral("-o tun0+ -d %1 -p tcp --dport 53 -j ACCEPT").arg(server); + } + return result; +} + +QStringList LinuxFirewall::getAllowRule(const QStringList& servers) +{ + QStringList result; + for (const QString& server : servers) + { + result << QStringLiteral("-d %1 -j ACCEPT").arg(server); + } + return result; +} + +QStringList LinuxFirewall::getBlockRule(const QStringList& servers) +{ + QStringList result; + for (const QString& server : servers) + { + result << QStringLiteral("-d %1 -j REJECT").arg(server); + } + return result; +} + + +void LinuxFirewall::install() +{ + // Clean up any existing rules if they exist. + uninstall(); + + // Create a root filter chain to hold all our other anchors in order. + createChain(Both, kRootChain, kFilterTable); + + // Create a root raw chain + createChain(Both, kRootChain, kRawTable); + + // Create a root NAT chain + createChain(Both, kRootChain, kNatTable); + + // Create a root Mangle chain + createChain(Both, kRootChain, kMangleTable); + + // Install our filter rulesets in each corresponding anchor chain. + installAnchor(Both, QStringLiteral("000.allowLoopback"), { + QStringLiteral("-o lo+ -j ACCEPT"), + }); + + installAnchor(IPv4, QStringLiteral("320.allowDNS"), {}); + + installAnchor(Both, QStringLiteral("310.blockDNS"), { + QStringLiteral("-p udp --dport 53 -j REJECT"), + QStringLiteral("-p tcp --dport 53 -j REJECT"), + }); + installAnchor(IPv4, QStringLiteral("300.allowLAN"), { + QStringLiteral("-d 10.0.0.0/8 -j ACCEPT"), + QStringLiteral("-d 169.254.0.0/16 -j ACCEPT"), + QStringLiteral("-d 172.16.0.0/12 -j ACCEPT"), + QStringLiteral("-d 192.168.0.0/16 -j ACCEPT"), + QStringLiteral("-d 224.0.0.0/4 -j ACCEPT"), + QStringLiteral("-d 255.255.255.255/32 -j ACCEPT"), + }); + installAnchor(IPv6, QStringLiteral("300.allowLAN"), { + QStringLiteral("-d fc00::/7 -j ACCEPT"), + QStringLiteral("-d fe80::/10 -j ACCEPT"), + QStringLiteral("-d ff00::/8 -j ACCEPT"), + }); + + + installAnchor(IPv4, QStringLiteral("290.allowDHCP"), { + QStringLiteral("-p udp -d 255.255.255.255 --sport 68 --dport 67 -j ACCEPT"), + }); + installAnchor(IPv6, QStringLiteral("290.allowDHCP"), { + QStringLiteral("-p udp -d ff00::/8 --sport 546 --dport 547 -j ACCEPT"), + }); + installAnchor(IPv6, QStringLiteral("250.blockIPv6"), { + QStringLiteral("! -o lo+ -j REJECT"), + }); + + installAnchor(Both, QStringLiteral("200.allowVPN"), { + QStringLiteral("-o amn0+ -j ACCEPT"), + QStringLiteral("-o tun0+ -j ACCEPT"), + }); + + installAnchor(IPv4, QStringLiteral("120.blockNets"), {}); + + installAnchor(IPv4, QStringLiteral("110.allowNets"), {}); + + installAnchor(Both, QStringLiteral("100.blockAll"), { + QStringLiteral("-j REJECT"), + }); + // NAT rules + installAnchor(Both, QStringLiteral("100.transIp"), { + + // Only need the original interface, not the IP. + // The interface should remain much more stable/unchangeable than the IP + // (IP can change when changing networks, but interface only changes if adding/removing NICs) + // this is just a stub rule - the real rule is set at run-time + // and updates dynamically (via replaceAnchor) when our interface changes + // it'll take this form: "-o -j MASQUERADE" + QStringLiteral("-j MASQUERADE") + }, kNatTable); + + // Mangle rules + installAnchor(Both, QStringLiteral("100.tagPkts"), { + QStringLiteral("-m cgroup --cgroup %1 -j MARK --set-mark %2").arg(kCGroupId, kPacketTag) + }, kMangleTable, setupTrafficSplitting, teardownTrafficSplitting); + + // A rule to mitigate CVE-2019-14899 - drop packets addressed to the local + // VPN IP but that are not actually received on the VPN interface. + // See here: https://seclists.org/oss-sec/2019/q4/122 + installAnchor(Both, QStringLiteral("100.vpnTunOnly"), { + // To be replaced at runtime + QStringLiteral("-j ACCEPT") + }, kRawTable); + + + // Insert our fitler root chain at the top of the OUTPUT chain. + linkChain(Both, kRootChain, kOutputChain, true, kFilterTable); + + // Insert our NAT root chain at the top of the POSTROUTING chain. + linkChain(Both, kRootChain, kPostRoutingChain, true, kNatTable); + + // Insert our Mangle root chain at the top of the OUTPUT chain. + linkChain(Both, kRootChain, kOutputChain, true, kMangleTable); + + // Insert our Raw root chain at the top of the PREROUTING chain. + linkChain(Both, kRootChain, kPreRoutingChain, true, kRawTable); + + setupTrafficSplitting(); +} + +void LinuxFirewall::uninstall() +{ + // Filter chain + unlinkChain(Both, kRootChain, kOutputChain, kFilterTable); + deleteChain(Both, kRootChain, kFilterTable); + + // Raw chain + unlinkChain(Both, kRootChain, kPreRoutingChain, kRawTable); + deleteChain(Both, kRootChain, kRawTable); + + // NAT chain + unlinkChain(Both, kRootChain, kPostRoutingChain, kNatTable); + deleteChain(Both, kRootChain, kNatTable); + + // Mangle chain + unlinkChain(Both, kRootChain, kOutputChain, kMangleTable); + deleteChain(Both, kRootChain, kMangleTable); + + // Remove filter anchors + uninstallAnchor(Both, QStringLiteral("000.allowLoopback")); + uninstallAnchor(Both, QStringLiteral("400.allowPIA")); + uninstallAnchor(IPv4, QStringLiteral("320.allowDNS")); + uninstallAnchor(Both, QStringLiteral("310.blockDNS")); + uninstallAnchor(Both, QStringLiteral("300.allowLAN")); + uninstallAnchor(Both, QStringLiteral("290.allowDHCP")); + uninstallAnchor(IPv6, QStringLiteral("250.blockIPv6")); + uninstallAnchor(Both, QStringLiteral("200.allowVPN")); + uninstallAnchor(IPv4, QStringLiteral("120.blockNets")); + uninstallAnchor(IPv4, QStringLiteral("110.allowNets")); + uninstallAnchor(Both, QStringLiteral("100.blockAll")); + + // Remove Nat anchors + uninstallAnchor(Both, QStringLiteral("100.transIp"), kNatTable); + + // Remove Mangle anchors + uninstallAnchor(Both, QStringLiteral("100.tagPkts"), kMangleTable); + + // Remove Raw anchors + uninstallAnchor(Both, QStringLiteral("100.vpnTunOnly"), kRawTable); + + teardownTrafficSplitting(); + + logger.debug() << "LinuxFirewall::uninstall() complete"; +} + +bool LinuxFirewall::isInstalled() +{ + return execute(QStringLiteral("iptables -C %1 -j %2 2> /dev/null").arg(kOutputChain, kRootChain)) == 0; +} + +void LinuxFirewall::enableAnchor(LinuxFirewall::IPVersion ip, const QString &anchor, const QString& tableName) +{ + if (ip == Both) + { + enableAnchor(IPv4, anchor, tableName); + enableAnchor(IPv6, anchor, tableName); + return; + } + const QString cmd = getCommand(ip); + const QString ipStr = ip == IPv6 ? QStringLiteral("(IPv6)") : QStringLiteral("(IPv4)"); + + execute(QStringLiteral("if %1 -C %5.a.%2 -j %5.%2 -t %4 2> /dev/null ; then echo '%2%3: ON' ; else echo '%2%3: OFF -> ON' ; %1 -A %5.a.%2 -j %5.%2 -t %4; fi").arg(cmd, anchor, ipStr, tableName, kAnchorName)); +} + +void LinuxFirewall::replaceAnchor(LinuxFirewall::IPVersion ip, const QString &anchor, const QString &newRule, const QString& tableName) +{ + if (ip == Both) + { + replaceAnchor(IPv4, anchor, newRule, tableName); + replaceAnchor(IPv6, anchor, newRule, tableName); + return; + } + const QString cmd = getCommand(ip); + const QString ipStr = ip == IPv6 ? QStringLiteral("(IPv6)") : QStringLiteral("(IPv4)"); + + execute(QStringLiteral("%1 -R %7.%2 1 %3 -t %4 ; echo 'Replaced rule %7.%2 %5 with %6'").arg(cmd, anchor, newRule, tableName, ipStr, newRule, kAnchorName)); +} + +void LinuxFirewall::disableAnchor(LinuxFirewall::IPVersion ip, const QString &anchor, const QString& tableName) +{ + if (ip == Both) + { + disableAnchor(IPv4, anchor, tableName); + disableAnchor(IPv6, anchor, tableName); + return; + } + const QString cmd = getCommand(ip); + const QString ipStr = ip == IPv6 ? QStringLiteral("(IPv6)") : QStringLiteral("(IPv4)"); + execute(QStringLiteral("if ! %1 -C %5.a.%2 -j %5.%2 -t %4 2> /dev/null ; then echo '%2%3: OFF' ; else echo '%2%3: ON -> OFF' ; %1 -F %5.a.%2 -t %4; fi").arg(cmd, anchor, ipStr, tableName, kAnchorName)); +} + +bool LinuxFirewall::isAnchorEnabled(LinuxFirewall::IPVersion ip, const QString &anchor, const QString& tableName) +{ + const QString cmd = getCommand(ip); + return execute(QStringLiteral("%1 -C %4.a.%2 -j %4.%2 -t %3 2> /dev/null").arg(cmd, anchor, tableName, kAnchorName)) == 0; +} + +void LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPVersion ip, const QString &anchor, bool enabled, const QString &tableName) +{ + if (enabled) + { + enableAnchor(ip, anchor, tableName); + const QString key = enabledKeyTemplate.arg(tableName, anchor); + if(anchorCallbacks.contains(key)) anchorCallbacks[key](); + } + else + { + disableAnchor(ip, anchor, tableName); + const QString key = disabledKeyTemplate.arg(tableName, anchor); + if(anchorCallbacks.contains(key)) anchorCallbacks[key](); + } +} + +void LinuxFirewall::updateDNSServers(const QStringList& servers) +{ + static QStringList existingServers {}; + + existingServers = servers; + execute(QStringLiteral("iptables -F %1.320.allowDNS").arg(kAnchorName)); + for (const QString& rule : getDNSRules(servers)) + execute(QStringLiteral("iptables -A %1.320.allowDNS %2").arg(kAnchorName, rule)); +} + +void LinuxFirewall::updateAllowNets(const QStringList& servers) +{ + static QStringList existingServers {}; + + existingServers = servers; + execute(QStringLiteral("iptables -F %1.110.allowNets").arg(kAnchorName)); + for (const QString& rule : getAllowRule(servers)) + execute(QStringLiteral("iptables -A %1.110.allowNets %2").arg(kAnchorName, rule)); +} + +void LinuxFirewall::updateBlockNets(const QStringList& servers) +{ + static QStringList existingServers {}; + + existingServers = servers; + execute(QStringLiteral("iptables -F %1.120.blockNets").arg(kAnchorName)); + for (const QString& rule : getBlockRule(servers)) + execute(QStringLiteral("iptables -A %1.120.blockNets %2").arg(kAnchorName, rule)); +} + +int waitForExitCode(QProcess& process) +{ + if (!process.waitForFinished() || process.error() == QProcess::FailedToStart) + return -2; + else if (process.exitStatus() != QProcess::NormalExit) + return -1; + else + return process.exitCode(); +} + +int LinuxFirewall::execute(const QString &command, bool ignoreErrors) +{ + QProcess p; + p.start(QStringLiteral("/bin/bash"), {QStringLiteral("-c"), command}, QProcess::ReadOnly); + p.closeWriteChannel(); + + int exitCode = waitForExitCode(p); + auto out = p.readAllStandardOutput().trimmed(); + auto err = p.readAllStandardError().trimmed(); + if ((exitCode != 0 || !err.isEmpty()) && !ignoreErrors) + logger.warning() << "(" << exitCode << ") $ " << command; + else if (false) + logger.debug() << "(" << exitCode << ") $ " << command; + if (!out.isEmpty()) + logger.info() << out; + if (!err.isEmpty()) + logger.warning() << err; + return exitCode; +} + +void LinuxFirewall::setupTrafficSplitting() +{ + auto cGroupDir = "/sys/fs/cgroup/net_cls/" BRAND_CODE "vpnexclusions/"; + logger.info() << "Should be setting up cgroup in" << cGroupDir << "for traffic splitting"; + execute(QStringLiteral("if [ ! -d %1 ] ; then mkdir %1 ; sleep 0.1 ; echo %2 > %1/net_cls.classid ; fi").arg(cGroupDir).arg(kCGroupId)); + // Set a rule with priority 100 (lower priority than local but higher than main/default, 0 is highest priority) + execute(QStringLiteral("if ! ip rule list | grep -q %1 ; then ip rule add from all fwmark %1 lookup %2 pri 100 ; fi").arg(kPacketTag, kRtableName)); +} + +void LinuxFirewall::teardownTrafficSplitting() +{ + logger.info() << "Tearing down cgroup and routing rules"; + execute(QStringLiteral("if ip rule list | grep -q %1; then ip rule del from all fwmark %1 lookup %2 2> /dev/null ; fi").arg(kPacketTag, kRtableName)); + execute(QStringLiteral("ip route flush table %1").arg(kRtableName)); + execute(QStringLiteral("ip route flush cache")); +} diff --git a/client/platforms/linux/daemon/linuxfirewall.h b/client/platforms/linux/daemon/linuxfirewall.h new file mode 100644 index 00000000..38049265 --- /dev/null +++ b/client/platforms/linux/daemon/linuxfirewall.h @@ -0,0 +1,107 @@ +// Copyright (c) 2023 Private Internet Access, Inc. +// +// This file is part of the Private Internet Access Desktop Client. +// +// The Private Internet Access Desktop Client is free software: you can +// redistribute it and/or modify it under the terms of the GNU General Public +// License as published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. +// +// The Private Internet Access Desktop Client is distributed in the hope that +// it will be useful, but WITHOUT ANY WARRANTY; without even the implied +// warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with the Private Internet Access Desktop Client. If not, see +// . + +// Copyright (c) 2024 AmneziaVPN +// This file has been modified for AmneziaVPN +// +// This file is based on the work of the Private Internet Access Desktop Client. +// The original code of the Private Internet Access Desktop Client is copyrighted (c) 2023 Private Internet Access, Inc. and licensed under GPL3. +// +// The modified version of this file is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this file. If not, see . + +#ifndef LINUXFIREWALL_H +#define LINUXFIREWALL_H + + +#include +#include + +// Descriptor for a set of firewall rules to be appled. +// +struct FirewallParams +{ + QStringList dnsServers; + QVector excludeApps; // Apps to exclude if VPN exemptions are enabled + QStringList allowAddrs; + QStringList blockAddrs; + // The follow flags indicate which general rulesets are needed. Note that + // this is after some sanity filtering, i.e. an allow rule may be listed + // as not needed if there were no block rules preceding it. The rulesets + // should be thought of as in last-match order. + + bool blockAll; // Block all traffic by default + bool allowVPN; // Exempt traffic through VPN tunnel + bool allowDHCP; // Exempt DHCP traffic + bool blockIPv6; // Block all IPv6 traffic + bool allowLAN; // Exempt LAN traffic, including IPv6 LAN traffic + bool blockDNS; // Block all DNS traffic except specified DNS servers + bool allowPIA; // Exempt PIA executables + bool allowLoopback; // Exempt loopback traffic + bool allowHnsd; // Exempt Handshake DNS traffic + bool allowVpnExemptions; // Exempt specified traffic from the tunnel (route it over the physical uplink instead) + bool allowNets; + bool blockNets; +}; + +class LinuxFirewall +{ +public: + enum IPVersion { IPv4, IPv6, Both }; + // Table names + static QString kFilterTable, kNatTable, kMangleTable, kRtableName, kRawTable; +public: + using FilterCallbackFunc = std::function; +private: + static int createChain(IPVersion ip, const QString& chain, const QString& tableName = kFilterTable); + static int deleteChain(IPVersion ip, const QString& chain, const QString& tableName = kFilterTable); + static int linkChain(IPVersion ip, const QString& chain, const QString& parent, bool mustBeFirst = false, const QString& tableName = kFilterTable); + static int unlinkChain(IPVersion ip, const QString& chain, const QString& parent, const QString& tableName = kFilterTable); + static void installAnchor(IPVersion ip, const QString& anchor, const QStringList& rules, const QString& tableName = kFilterTable, const FilterCallbackFunc& enableFunc = {}, const FilterCallbackFunc& disableFunc = {}); + static void uninstallAnchor(IPVersion ip, const QString& anchor, const QString& tableName = kFilterTable); + static QStringList getDNSRules(const QStringList& servers); + static QStringList getAllowRule(const QStringList& servers); + static QStringList getBlockRule(const QStringList& servers); + static void setupTrafficSplitting(); + static void teardownTrafficSplitting(); + static int execute(const QString& command, bool ignoreErrors = false); +private: + // Chain names + static QString kOutputChain, kRootChain, kPostRoutingChain, kPreRoutingChain; + +public: + static void install(); + static void uninstall(); + static bool isInstalled(); + static void ensureRootAnchorPriority(IPVersion ip = Both); + static void enableAnchor(IPVersion ip, const QString& anchor, const QString& tableName = kFilterTable); + static void disableAnchor(IPVersion ip, const QString& anchor, const QString& tableName = kFilterTable); + static bool isAnchorEnabled(IPVersion ip, const QString& anchor, const QString& tableName = kFilterTable); + static void setAnchorEnabled(IPVersion ip, const QString& anchor, bool enabled, const QString& tableName = kFilterTable); + static void replaceAnchor(LinuxFirewall::IPVersion ip, const QString &anchor, const QString &newRule, const QString& tableName); + static void updateDNSServers(const QStringList& servers); + static void updateAllowNets(const QStringList& servers); + static void updateBlockNets(const QStringList& servers); +}; + +#endif // LINUXFIREWALL_H diff --git a/client/platforms/linux/daemon/wireguardutilslinux.cpp b/client/platforms/linux/daemon/wireguardutilslinux.cpp index 792120a7..e5dce524 100644 --- a/client/platforms/linux/daemon/wireguardutilslinux.cpp +++ b/client/platforms/linux/daemon/wireguardutilslinux.cpp @@ -11,7 +11,9 @@ #include #include #include +#include +#include "linuxfirewall.h" #include "leakdetector.h" #include "logger.h" @@ -116,7 +118,27 @@ bool WireguardUtilsLinux::addInterface(const InterfaceConfig& config) { int err = uapiErrno(uapiCommand(message)); if (err != 0) { logger.error() << "Interface configuration failed:" << strerror(err); + } else { + FirewallParams params { }; + params.dnsServers.append(config.m_dnsServer); + if (config.m_allowedIPAddressRanges.at(0).toString() == "0.0.0.0/0"){ + params.blockAll = true; + if (config.m_excludedAddresses.size()) { + params.allowNets = true; + foreach (auto net, config.m_excludedAddresses) { + params.allowAddrs.append(net.toUtf8()); + } + } + } else { + params.blockNets = true; + foreach (auto net, config.m_allowedIPAddressRanges) { + params.blockAddrs.append(net.toString()); + } + } + + applyFirewallRules(params); } + return (err == 0); } @@ -140,6 +162,9 @@ bool WireguardUtilsLinux::deleteInterface() { // Garbage collect. QDir wgRuntimeDir(WG_RUNTIME_DIR); QFile::remove(wgRuntimeDir.filePath(QString(WG_INTERFACE) + ".name")); + + // double-check + ensure our firewall is installed and enabled + LinuxFirewall::uninstall(); return true; } @@ -252,6 +277,31 @@ QList WireguardUtilsLinux::getPeerStatus() { return peerList; } + +void WireguardUtilsLinux::applyFirewallRules(FirewallParams& params) +{ + // double-check + ensure our firewall is installed and enabled + if (!LinuxFirewall::isInstalled()) LinuxFirewall::install(); + + // Note: rule precedence is handled inside IpTablesFirewall + LinuxFirewall::ensureRootAnchorPriority(); + + LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("000.allowLoopback"), true); + LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("100.blockAll"), params.blockAll); + LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("110.allowNets"), params.allowNets); + LinuxFirewall::updateAllowNets(params.allowAddrs); + LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("120.blockNets"), params.blockNets); + LinuxFirewall::updateBlockNets(params.blockAddrs); + LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("200.allowVPN"), true); + LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv6, QStringLiteral("250.blockIPv6"), true); + LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("290.allowDHCP"), true); + LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("300.allowLAN"), true); + LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("310.blockDNS"), true); + LinuxFirewall::updateDNSServers(params.dnsServers); + LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("320.allowDNS"), true); + LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("400.allowPIA"), true); +} + bool WireguardUtilsLinux::updateRoutePrefix(const IPAddress& prefix) { if (!m_rtmonitor) { return false; diff --git a/client/platforms/linux/daemon/wireguardutilslinux.h b/client/platforms/linux/daemon/wireguardutilslinux.h index a8320c95..9746ea4b 100644 --- a/client/platforms/linux/daemon/wireguardutilslinux.h +++ b/client/platforms/linux/daemon/wireguardutilslinux.h @@ -8,8 +8,11 @@ #include #include + #include "daemon/wireguardutils.h" #include "linuxroutemonitor.h" +#include "linuxfirewall.h" + class WireguardUtilsLinux final : public WireguardUtils { Q_OBJECT @@ -34,7 +37,7 @@ public: bool addExclusionRoute(const IPAddress& prefix) override; bool deleteExclusionRoute(const IPAddress& prefix) override; - + void applyFirewallRules(FirewallParams& params); signals: void backendFailure(); diff --git a/client/platforms/macos/daemon/macosfirewall.cpp b/client/platforms/macos/daemon/macosfirewall.cpp new file mode 100644 index 00000000..0fe51f23 --- /dev/null +++ b/client/platforms/macos/daemon/macosfirewall.cpp @@ -0,0 +1,199 @@ +// Copyright (c) 2023 Private Internet Access, Inc. +// +// This file is part of the Private Internet Access Desktop Client. +// +// The Private Internet Access Desktop Client is free software: you can +// redistribute it and/or modify it under the terms of the GNU General Public +// License as published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. +// +// The Private Internet Access Desktop Client is distributed in the hope that +// it will be useful, but WITHOUT ANY WARRANTY; without even the implied +// warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with the Private Internet Access Desktop Client. If not, see +// . + +// Copyright (c) 2024 AmneziaVPN +// This file has been modified for AmneziaVPN +// +// This file is based on the work of the Private Internet Access Desktop Client. +// The original code of the Private Internet Access Desktop Client is copyrighted (c) 2023 Private Internet Access, Inc. and licensed under GPL3. +// +// The modified version of this file is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this file. If not, see . + +#include "macosfirewall.h" +#include "logger.h" +#include +#include + +#define BRAND_IDENTIFIER "amn" + +namespace { + Logger logger("MacOSFirewall"); +} // namespace + +#include "macosfirewall.h" + +#define ResourceDir qApp->applicationDirPath() + "/pf" +#define DaemonDataDir qApp->applicationDirPath() + "/pf" + +#include + +static QString kRootAnchor = QStringLiteral(BRAND_IDENTIFIER); +static QByteArray kPfWarning = "pfctl: Use of -f option, could result in flushing of rules\npresent in the main ruleset added by the system at startup.\nSee /etc/pf.conf for further details.\n"; + +int waitForExitCode(QProcess& process) +{ + if (!process.waitForFinished() || process.error() == QProcess::FailedToStart) + return -2; + else if (process.exitStatus() != QProcess::NormalExit) + return -1; + else + return process.exitCode(); +} + +int MacOSFirewall::execute(const QString& command, bool ignoreErrors) +{ + QProcess p; + + p.start(QStringLiteral("/bin/bash"), { QStringLiteral("-c"), command }, QProcess::ReadOnly); + p.closeWriteChannel(); + int exitCode = waitForExitCode(p); + auto out = p.readAllStandardOutput().trimmed(); + + auto err = p.readAllStandardError().replace(kPfWarning, "").trimmed(); + if ((exitCode != 0 || !err.isEmpty()) && !ignoreErrors) + logger.info() << "(" << exitCode << ") $ " << command; + else if (false) + logger.info() << "(" << exitCode << ") $ " << command; + if (!out.isEmpty()) logger.info() << out; + if (!err.isEmpty()) logger.info() << err; + return exitCode; +} + +void MacOSFirewall::installRootAnchors() +{ + logger.info() << "Installing PF root anchors"; + + // Append our NAT anchors by reading back and re-applying NAT rules only + auto insertNatAnchors = QStringLiteral( + "( " + R"(pfctl -sn | grep -v '%1/*'; )" // Translation rules (includes both nat and rdr, despite the modifier being 'nat') + R"(echo 'nat-anchor "%2/*"'; )" // PIA's translation anchors + R"(echo 'rdr-anchor "%3/*"'; )" + R"(echo 'load anchor "%4" from "%5/%6.conf"'; )" // Load the PIA anchors from file + ") | pfctl -N -f -").arg(kRootAnchor, kRootAnchor, kRootAnchor, kRootAnchor, ResourceDir, kRootAnchor); + + execute(insertNatAnchors); + + // Append our filter anchor by reading back and re-applying filter rules + // only. pfctl -sr also includes scrub rules, but these will be ignored + // due to -R. + auto insertFilterAnchor = QStringLiteral( + "( " + R"(pfctl -sr | grep -v '%1/*'; )" // Filter rules (everything from pfctl -sr except 'scrub') + R"(echo 'anchor "%2/*"'; )" // PIA's filter anchors + R"(echo 'load anchor "%3" from "%4/%5.conf"'; )" // Load the PIA anchors from file + " ) | pfctl -R -f -").arg(kRootAnchor, kRootAnchor, kRootAnchor, ResourceDir, kRootAnchor); + execute(insertFilterAnchor); +} + +void MacOSFirewall::install() +{ + // remove hard-coded (legacy) pia anchor from /etc/pf.conf if it exists + execute(QStringLiteral("if grep -Fq '%1' /etc/pf.conf ; then echo \"`cat /etc/pf.conf | grep -vF '%1'`\" > /etc/pf.conf ; fi").arg(kRootAnchor)); + + // Clean up any existing rules if they exist. + uninstall(); + + timespec waitTime{0, 10'000'000}; + ::nanosleep(&waitTime, nullptr); + + logger.info() << "Installing PF root anchor"; + + installRootAnchors(); + execute(QStringLiteral("pfctl -E 2>&1 | grep -F 'Token : ' | cut -c9- > '%1/pf.token'").arg(DaemonDataDir)); +} + + +void MacOSFirewall::uninstall() +{ + logger.info() << "Uninstalling PF root anchor"; + + execute(QStringLiteral("pfctl -q -a '%1' -F all").arg(kRootAnchor)); + execute(QStringLiteral("test -f '%1/pf.token' && pfctl -X `cat '%1/pf.token'` && rm '%1/pf.token'").arg(DaemonDataDir)); + execute(QStringLiteral("test -f /etc/pf.conf && pfctl -F all -f /etc/pf.conf")); +} + +bool MacOSFirewall::isInstalled() +{ + return isPFEnabled() && isRootAnchorLoaded(); +} + +bool MacOSFirewall::isPFEnabled() +{ + return 0 == execute(QStringLiteral("test -s '%1/pf.token' && pfctl -s References | grep -qFf '%1/pf.token'").arg(DaemonDataDir), true); +} + +void MacOSFirewall::ensureRootAnchorPriority() +{ + // We check whether our anchor appears last in the ruleset. If it does not, then remove it and re-add it last (this happens atomically). + // Appearing last ensures priority. + execute(QStringLiteral("if ! pfctl -sr | tail -1 | grep -qF '%1'; then echo -e \"$(pfctl -sr | grep -vF '%1')\\n\"'anchor \"%1\"' | pfctl -f - ; fi").arg(kRootAnchor)); +} + +bool MacOSFirewall::isRootAnchorLoaded() +{ + // Our Root anchor is loaded if: + // 1. It is is included among the top-level anchors + // 2. It is not empty (i.e it contains sub-anchors) + return 0 == execute(QStringLiteral("pfctl -sr | grep -q '%1' && pfctl -q -a '%1' -s rules 2> /dev/null | grep -q .").arg(kRootAnchor), true); +} + +void MacOSFirewall::enableAnchor(const QString& anchor) +{ + execute(QStringLiteral("if pfctl -q -a '%1/%2' -s rules 2> /dev/null | grep -q . ; then echo '%2: ON' ; else echo '%2: OFF -> ON' ; pfctl -q -a '%1/%2' -F all -f '%3/%1.%2.conf' ; fi").arg(kRootAnchor, anchor, ResourceDir)); +} + +void MacOSFirewall::disableAnchor(const QString& anchor) +{ + execute(QStringLiteral("if ! pfctl -q -a '%1/%2' -s rules 2> /dev/null | grep -q . ; then echo '%2: OFF' ; else echo '%2: ON -> OFF' ; pfctl -q -a '%1/%2' -F all ; fi").arg(kRootAnchor, anchor)); +} + +bool MacOSFirewall::isAnchorEnabled(const QString& anchor) +{ + return 0 == execute(QStringLiteral("pfctl -q -a '%1/%2' -s rules 2> /dev/null | grep -q .").arg(kRootAnchor, anchor), true); +} + +void MacOSFirewall::setAnchorEnabled(const QString& anchor, bool enabled) +{ + if (enabled) + enableAnchor(anchor); + else + disableAnchor(anchor); +} + +void MacOSFirewall::setAnchorTable(const QString& anchor, bool enabled, const QString& table, const QStringList& items) +{ + if (enabled) + execute(QStringLiteral("pfctl -q -a '%1/%2' -t '%3' -T replace %4").arg(kRootAnchor, anchor, table, items.join(' '))); + else + execute(QStringLiteral("pfctl -q -a '%1/%2' -t '%3' -T kill").arg(kRootAnchor, anchor, table), true); +} + +void MacOSFirewall::setAnchorWithRules(const QString& anchor, bool enabled, const QStringList &ruleList) +{ + if (!enabled) + return (void)execute(QStringLiteral("pfctl -q -a '%1/%2' -F rules").arg(kRootAnchor, anchor), true); + else + return (void)execute(QStringLiteral("echo -e \"%1\" | pfctl -q -a '%2/%3' -f -").arg(ruleList.join('\n'), kRootAnchor, anchor), true); +} diff --git a/client/platforms/macos/daemon/macosfirewall.h b/client/platforms/macos/daemon/macosfirewall.h new file mode 100644 index 00000000..faa87c8c --- /dev/null +++ b/client/platforms/macos/daemon/macosfirewall.h @@ -0,0 +1,90 @@ +// Copyright (c) 2023 Private Internet Access, Inc. +// +// This file is part of the Private Internet Access Desktop Client. +// +// The Private Internet Access Desktop Client is free software: you can +// redistribute it and/or modify it under the terms of the GNU General Public +// License as published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. +// +// The Private Internet Access Desktop Client is distributed in the hope that +// it will be useful, but WITHOUT ANY WARRANTY; without even the implied +// warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with the Private Internet Access Desktop Client. If not, see +// . + +// Copyright (c) 2024 AmneziaVPN +// This file has been modified for AmneziaVPN +// +// This file is based on the work of the Private Internet Access Desktop Client. +// The original code of the Private Internet Access Desktop Client is copyrighted (c) 2023 Private Internet Access, Inc. and licensed under GPL3. +// +// The modified version of this file is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this file. If not, see . + +#ifndef MACOSFIREWALL_H +#define MACOSFIREWALL_H + +#include +#include + +// Descriptor for a set of firewall rules to be appled. +// +struct FirewallParams +{ + QStringList dnsServers; + QVector excludeApps; // Apps to exclude if VPN exemptions are enabled + + QStringList allowAddrs; + QStringList blockAddrs; + + // The follow flags indicate which general rulesets are needed. Note that + // this is after some sanity filtering, i.e. an allow rule may be listed + // as not needed if there were no block rules preceding it. The rulesets + // should be thought of as in last-match order. + + bool blockAll; // Block all traffic by default + bool blockNets; + bool allowNets; + bool allowVPN; // Exempt traffic through VPN tunnel + bool allowDHCP; // Exempt DHCP traffic + bool blockIPv6; // Block all IPv6 traffic + bool allowLAN; // Exempt LAN traffic, including IPv6 LAN traffic + bool blockDNS; // Block all DNS traffic except specified DNS servers + bool allowPIA; // Exempt PIA executables + bool allowLoopback; // Exempt loopback traffic + bool allowHnsd; // Exempt Handshake DNS traffic + bool allowVpnExemptions; // Exempt specified traffic from the tunnel (route it over the physical uplink instead) +}; + +class MacOSFirewall +{ + +private: + static int execute(const QString &command, bool ignoreErrors = false); + static bool isPFEnabled(); + static bool isRootAnchorLoaded(); + +public: + static void install(); + static void uninstall(); + static bool isInstalled(); + static void enableAnchor(const QString &anchor); + static void disableAnchor(const QString &anchor); + static bool isAnchorEnabled(const QString &anchor); + static void setAnchorEnabled(const QString &anchor, bool enable); + static void setAnchorTable(const QString &anchor, bool enabled, const QString &table, const QStringList &items); + static void setAnchorWithRules(const QString &anchor, bool enabled, const QStringList &rules); + static void ensureRootAnchorPriority(); + static void installRootAnchors(); +}; + +#endif // MACOSFIREWALL_H diff --git a/client/platforms/macos/daemon/wireguardutilsmacos.cpp b/client/platforms/macos/daemon/wireguardutilsmacos.cpp index ef13f4c7..718edaba 100644 --- a/client/platforms/macos/daemon/wireguardutilsmacos.cpp +++ b/client/platforms/macos/daemon/wireguardutilsmacos.cpp @@ -114,9 +114,30 @@ bool WireguardUtilsMacos::addInterface(const InterfaceConfig& config) { } int err = uapiErrno(uapiCommand(message)); + if (err != 0) { logger.error() << "Interface configuration failed:" << strerror(err); + } else { + FirewallParams params { }; + params.dnsServers.append(config.m_dnsServer); + if (config.m_allowedIPAddressRanges.at(0).toString() == "0.0.0.0/0"){ + params.blockAll = true; + if (config.m_excludedAddresses.size()) { + params.allowNets = true; + foreach (auto net, config.m_excludedAddresses) { + params.allowAddrs.append(net.toUtf8()); + } + } + } else { + params.blockNets = true; + foreach (auto net, config.m_allowedIPAddressRanges) { + params.blockAddrs.append(net.toString()); + } + } + + applyFirewallRules(params); } + return (err == 0); } @@ -140,6 +161,10 @@ bool WireguardUtilsMacos::deleteInterface() { // Garbage collect. QDir wgRuntimeDir(WG_RUNTIME_DIR); QFile::remove(wgRuntimeDir.filePath(QString(WG_INTERFACE) + ".name")); + + // double-check + ensure our firewall is installed and enabled + MacOSFirewall::uninstall(); + return true; } @@ -302,6 +327,31 @@ bool WireguardUtilsMacos::addExclusionRoute(const IPAddress& prefix) { return m_rtmonitor->addExclusionRoute(prefix); } +void WireguardUtilsMacos::applyFirewallRules(FirewallParams& params) +{ + // double-check + ensure our firewall is installed and enabled. This is necessary as + // other software may disable pfctl before re-enabling with their own rules (e.g other VPNs) + if (!MacOSFirewall::isInstalled()) MacOSFirewall::install(); + + MacOSFirewall::ensureRootAnchorPriority(); + MacOSFirewall::setAnchorEnabled(QStringLiteral("000.allowLoopback"), true); + MacOSFirewall::setAnchorEnabled(QStringLiteral("100.blockAll"), params.blockAll); + MacOSFirewall::setAnchorEnabled(QStringLiteral("110.allowNets"), params.allowNets); + MacOSFirewall::setAnchorTable(QStringLiteral("110.allowNets"), params.allowNets, + QStringLiteral("allownets"), params.allowAddrs); + + MacOSFirewall::setAnchorEnabled(QStringLiteral("120.blockNets"), params.blockNets); + MacOSFirewall::setAnchorTable(QStringLiteral("120.blockNets"), params.blockNets, + QStringLiteral("blocknets"), params.blockAddrs); + + MacOSFirewall::setAnchorEnabled(QStringLiteral("200.allowVPN"), true); + MacOSFirewall::setAnchorEnabled(QStringLiteral("250.blockIPv6"), true); + MacOSFirewall::setAnchorEnabled(QStringLiteral("290.allowDHCP"), true); + MacOSFirewall::setAnchorEnabled(QStringLiteral("300.allowLAN"), true); + MacOSFirewall::setAnchorEnabled(QStringLiteral("310.blockDNS"), true); + MacOSFirewall::setAnchorTable(QStringLiteral("310.blockDNS"), true, QStringLiteral("dnsaddr"), params.dnsServers); +} + bool WireguardUtilsMacos::deleteExclusionRoute(const IPAddress& prefix) { if (!m_rtmonitor) { return false; diff --git a/client/platforms/macos/daemon/wireguardutilsmacos.h b/client/platforms/macos/daemon/wireguardutilsmacos.h index aa9f19eb..243f4b64 100644 --- a/client/platforms/macos/daemon/wireguardutilsmacos.h +++ b/client/platforms/macos/daemon/wireguardutilsmacos.h @@ -10,6 +10,7 @@ #include "daemon/wireguardutils.h" #include "macosroutemonitor.h" +#include "macosfirewall.h" class WireguardUtilsMacos final : public WireguardUtils { Q_OBJECT @@ -34,6 +35,7 @@ class WireguardUtilsMacos final : public WireguardUtils { bool addExclusionRoute(const IPAddress& prefix) override; bool deleteExclusionRoute(const IPAddress& prefix) override; + void applyFirewallRules(FirewallParams& params); signals: void backendFailure(); diff --git a/client/protocols/openvpnovercloakprotocol.cpp b/client/protocols/openvpnovercloakprotocol.cpp index 7000e5ef..706e651a 100644 --- a/client/protocols/openvpnovercloakprotocol.cpp +++ b/client/protocols/openvpnovercloakprotocol.cpp @@ -66,7 +66,7 @@ ErrorCode OpenVpnOverCloakProtocol::start() emit protocolError(amnezia::ErrorCode::CloakExecutableCrashed); stop(); } - if (exitCode !=0 ){ + if (exitCode !=0 ) { emit protocolError(amnezia::ErrorCode::InternalError); stop(); } diff --git a/client/protocols/openvpnprotocol.cpp b/client/protocols/openvpnprotocol.cpp index c38c6eea..5f8db625 100644 --- a/client/protocols/openvpnprotocol.cpp +++ b/client/protocols/openvpnprotocol.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include "logger.h" #include "openvpnprotocol.h" @@ -53,6 +54,11 @@ void OpenVpnProtocol::stop() QThread::msleep(10); m_managementServer.stop(); } + +#if defined(Q_OS_WIN) || defined(Q_OS_LINUX) || defined(Q_OS_MACOS) + IpcClient::Interface()->disableKillSwitch(); +#endif + setConnectionState(Vpn::ConnectionState::Disconnected); } @@ -85,13 +91,13 @@ void OpenVpnProtocol::killOpenVpnProcess() void OpenVpnProtocol::readOpenVpnConfiguration(const QJsonObject &configuration) { if (configuration.contains(ProtocolProps::key_proto_config_data(Proto::OpenVpn))) { + m_configData = configuration; QJsonObject jConfig = configuration.value(ProtocolProps::key_proto_config_data(Proto::OpenVpn)).toObject(); m_configFile.open(); m_configFile.write(jConfig.value(config_key::config).toString().toUtf8()); m_configFile.close(); m_configFileName = m_configFile.fileName(); - qDebug().noquote() << QString("Set config data") << m_configFileName; } } @@ -138,12 +144,18 @@ uint OpenVpnProtocol::selectMgmtPort() void OpenVpnProtocol::updateRouteGateway(QString line) { - // TODO: fix for macos - line = line.split("ROUTE_GATEWAY", Qt::SkipEmptyParts).at(1); - if (!line.contains("/")) - return; - m_routeGateway = line.split("/", Qt::SkipEmptyParts).first(); - m_routeGateway.replace(" ", ""); + if (line.contains("net_route_v4_best_gw")) { + QStringList params = line.split(" "); + if (params.size() == 6) { + m_routeGateway = params.at(3); + } + } else { + line = line.split("ROUTE_GATEWAY", Qt::SkipEmptyParts).at(1); + if (!line.contains("/")) + return; + m_routeGateway = line.split("/", Qt::SkipEmptyParts).first(); + m_routeGateway.replace(" ", ""); + } qDebug() << "Set VPN route gateway" << m_routeGateway; } @@ -282,7 +294,7 @@ void OpenVpnProtocol::onReadyReadDataFromManagementServer() } } - if (line.contains("ROUTE_GATEWAY")) { + if (line.contains("ROUTE_GATEWAY") || line.contains("net_route_v4_best_gw")) { updateRouteGateway(line); } @@ -320,14 +332,28 @@ void OpenVpnProtocol::updateVpnGateway(const QString &line) // line looks like // PUSH: Received control message: 'PUSH_REPLY,route 10.8.0.1,topology net30,ping 10,ping-restart // 120,ifconfig 10.8.0.6 10.8.0.5,peer-id 0,cipher AES-256-GCM' - QStringList params = line.split(","); for (const QString &l : params) { if (l.contains("ifconfig")) { if (l.split(" ").size() == 3) { m_vpnLocalAddress = l.split(" ").at(1); m_vpnGateway = l.split(" ").at(2); - +#ifdef Q_OS_WIN + QList netInterfaces = QNetworkInterface::allInterfaces(); + for (int i = 0; i < netInterfaces.size(); i++) { + for (int j=0; j < netInterfaces.at(i).addressEntries().size(); j++) + { + if (m_vpnLocalAddress == netInterfaces.at(i).addressEntries().at(j).ip().toString()) { + IpcClient::Interface()->enableKillSwitch(QJsonObject(), netInterfaces.at(i).index()); + m_configData.insert("vpnGateway", m_vpnGateway); + IpcClient::Interface()->enablePeerTraffic(m_configData); + } + } + } +#endif +#if defined(Q_OS_LINUX) || defined(Q_OS_MACOS) + IpcClient::Interface()->enableKillSwitch(m_configData, 0); +#endif qDebug() << QString("Set vpn local address %1, gw %2").arg(m_vpnLocalAddress).arg(vpnGateway()); } } diff --git a/client/protocols/openvpnprotocol.h b/client/protocols/openvpnprotocol.h index ad80fe50..b07d1268 100644 --- a/client/protocols/openvpnprotocol.h +++ b/client/protocols/openvpnprotocol.h @@ -44,6 +44,7 @@ private: ManagementServer m_managementServer; QString m_configFileName; + QJsonObject m_configData; QTemporaryFile m_configFile; uint selectMgmtPort(); diff --git a/client/protocols/wireguardprotocol.cpp b/client/protocols/wireguardprotocol.cpp index 3b95a41a..61b2e261 100644 --- a/client/protocols/wireguardprotocol.cpp +++ b/client/protocols/wireguardprotocol.cpp @@ -13,9 +13,6 @@ WireguardProtocol::WireguardProtocol(const QJsonObject &configuration, QObject *parent) : VpnProtocol(configuration, parent) { - m_configFile.setFileName(QDir::tempPath() + QDir::separator() + serviceName() + ".conf"); - writeWireguardConfiguration(configuration); - m_impl.reset(new LocalSocketController()); connect(m_impl.get(), &ControllerImpl::connected, this, [this](const QString &pubkey, const QDateTime &connectionTimestamp) { @@ -50,45 +47,9 @@ ErrorCode WireguardProtocol::stopMzImpl() return ErrorCode::NoError; } -void WireguardProtocol::writeWireguardConfiguration(const QJsonObject &configuration) -{ - QJsonObject jConfig = configuration.value(ProtocolProps::key_proto_config_data(Proto::WireGuard)).toObject(); - - if (!m_configFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) { - qCritical() << "Failed to save wireguard config to" << m_configFile.fileName(); - return; - } - - m_configFile.write(jConfig.value(config_key::config).toString().toUtf8()); - m_configFile.close(); - - - m_configFileName = m_configFile.fileName(); - - m_isConfigLoaded = true; - - qDebug().noquote() << QString("Set config data") << configPath(); - qDebug().noquote() << QString("Set config data") - << configuration.value(ProtocolProps::key_proto_config_data(Proto::WireGuard)).toString().toUtf8(); -} - -QString WireguardProtocol::configPath() const -{ - return m_configFileName; -} - -QString WireguardProtocol::serviceName() const -{ - return "AmneziaVPN.WireGuard0"; -} ErrorCode WireguardProtocol::start() { - if (!m_isConfigLoaded) { - setLastError(ErrorCode::ConfigMissing); - return lastError(); - } - return startMzImpl(); } diff --git a/client/protocols/wireguardprotocol.h b/client/protocols/wireguardprotocol.h index 4a6ae1e6..6d1a0518 100644 --- a/client/protocols/wireguardprotocol.h +++ b/client/protocols/wireguardprotocol.h @@ -26,15 +26,6 @@ public: ErrorCode stopMzImpl(); private: - QString configPath() const; - void writeWireguardConfiguration(const QJsonObject &configuration); - QString serviceName() const; - -private: - QString m_configFileName; - QFile m_configFile; - - bool m_isConfigLoaded = false; QScopedPointer m_impl; }; diff --git a/client/translations/amneziavpn_ru.ts b/client/translations/amneziavpn_ru.ts index ddd7b0e9..c4c9fe58 100644 --- a/client/translations/amneziavpn_ru.ts +++ b/client/translations/amneziavpn_ru.ts @@ -379,7 +379,7 @@ Already installed containers were found on the server. All installed containers Save and Restart Amnezia - Сохранить и пререзагрузить Amnezia + Сохранить и перезагрузить Amnezia diff --git a/client/ui/controllers/exportController.cpp b/client/ui/controllers/exportController.cpp index 4f3fe7d5..2b102e13 100644 --- a/client/ui/controllers/exportController.cpp +++ b/client/ui/controllers/exportController.cpp @@ -327,7 +327,8 @@ void ExportController::updateClientManagementModel(const DockerContainer contain void ExportController::revokeConfig(const int row, const DockerContainer container, ServerCredentials credentials) { - ErrorCode errorCode = m_clientManagementModel->revokeClient(row, container, credentials); + ErrorCode errorCode = m_clientManagementModel->revokeClient(row, container, credentials, + m_serversModel->getCurrentlyProcessedServerIndex()); if (errorCode != ErrorCode::NoError) { emit exportErrorOccurred(errorString(errorCode)); } diff --git a/client/ui/models/clientManagementModel.cpp b/client/ui/models/clientManagementModel.cpp index 0b1be2cc..7c81c80e 100644 --- a/client/ui/models/clientManagementModel.cpp +++ b/client/ui/models/clientManagementModel.cpp @@ -296,30 +296,36 @@ ErrorCode ClientManagementModel::renameClient(const int row, const QString &clie } ErrorCode ClientManagementModel::revokeClient(const int row, const DockerContainer container, - ServerCredentials credentials) + ServerCredentials credentials, const int serverIndex) { ErrorCode errorCode = ErrorCode::NoError; + auto client = m_clientsTable.at(row).toObject(); + QString clientId = client.value(configKey::clientId).toString(); if (container == DockerContainer::OpenVpn || container == DockerContainer::ShadowSocks || container == DockerContainer::Cloak) { - errorCode = revokeOpenVpn(row, container, credentials); + errorCode = revokeOpenVpn(row, container, credentials, serverIndex); } else if (container == DockerContainer::WireGuard || container == DockerContainer::Awg) { errorCode = revokeWireGuard(row, container, credentials); } if (errorCode == ErrorCode::NoError) { - auto client = m_clientsTable.at(row).toObject(); - QString clientId = client.value(configKey::clientId).toString(); - - const auto server = m_settings->defaultServer(); + const auto server = m_settings->server(serverIndex); QJsonArray containers = server.value(config_key::containers).toArray(); for (auto i = 0; i < containers.size(); i++) { auto containerConfig = containers.at(i).toObject(); auto containerType = ContainerProps::containerFromString(containerConfig.value(config_key::container).toString()); - auto protocolConfig = containerConfig.value(ContainerProps::containerTypeToString(containerType)).toObject(); + if (containerType == container) { + QJsonObject protocolConfig; + if (container == DockerContainer::ShadowSocks || container == DockerContainer::Cloak) { + protocolConfig = containerConfig.value(ContainerProps::containerTypeToString(DockerContainer::OpenVpn)).toObject(); + } else { + protocolConfig = containerConfig.value(ContainerProps::containerTypeToString(containerType)).toObject(); + } - if (protocolConfig.value(config_key::last_config).toString().contains(clientId)) { - emit adminConfigRevoked(container); + if (protocolConfig.value(config_key::last_config).toString().contains(clientId)) { + emit adminConfigRevoked(container); + } } } } @@ -328,7 +334,7 @@ ErrorCode ClientManagementModel::revokeClient(const int row, const DockerContain } ErrorCode ClientManagementModel::revokeOpenVpn(const int row, const DockerContainer container, - ServerCredentials credentials) + ServerCredentials credentials, const int serverIndex) { auto client = m_clientsTable.at(row).toObject(); QString clientId = client.value(configKey::clientId).toString(); @@ -337,6 +343,7 @@ ErrorCode ClientManagementModel::revokeOpenVpn(const int row, const DockerContai "cd /opt/amnezia/openvpn ;\\" "easyrsa revoke %1 ;\\" "easyrsa gen-crl ;\\" + "chmod 666 pki/crl.pem ;\\" "cp pki/crl.pem .'") .arg(clientId); @@ -356,12 +363,7 @@ ErrorCode ClientManagementModel::revokeOpenVpn(const int row, const DockerContai const QByteArray clientsTableString = QJsonDocument(m_clientsTable).toJson(); QString clientsTableFile = QString("/opt/amnezia/%1/clientsTable"); - if (container == DockerContainer::OpenVpn || container == DockerContainer::ShadowSocks - || container == DockerContainer::Cloak) { - clientsTableFile = clientsTableFile.arg(ContainerProps::containerTypeToString(DockerContainer::OpenVpn)); - } else { - clientsTableFile = clientsTableFile.arg(ContainerProps::containerTypeToString(container)); - } + clientsTableFile = clientsTableFile.arg(ContainerProps::containerTypeToString(DockerContainer::OpenVpn)); error = serverController.uploadTextFileToContainer(container, credentials, clientsTableString, clientsTableFile); if (error != ErrorCode::NoError) { logger.error() << "Failed to upload the clientsTable file to the server"; diff --git a/client/ui/models/clientManagementModel.h b/client/ui/models/clientManagementModel.h index ba36c26f..c003881b 100644 --- a/client/ui/models/clientManagementModel.h +++ b/client/ui/models/clientManagementModel.h @@ -28,7 +28,7 @@ public slots: ServerCredentials credentials); ErrorCode renameClient(const int row, const QString &userName, const DockerContainer container, ServerCredentials credentials, bool addTimeStamp = false); - ErrorCode revokeClient(const int index, const DockerContainer container, ServerCredentials credentials); + ErrorCode revokeClient(const int index, const DockerContainer container, ServerCredentials credentials, const int serverIndex); protected: QHash roleNames() const override; @@ -41,7 +41,7 @@ private: void migration(const QByteArray &clientsTableString); - ErrorCode revokeOpenVpn(const int row, const DockerContainer container, ServerCredentials credentials); + ErrorCode revokeOpenVpn(const int row, const DockerContainer container, ServerCredentials credentials, const int serverIndex); ErrorCode revokeWireGuard(const int row, const DockerContainer container, ServerCredentials credentials); ErrorCode getOpenVpnClients(ServerController &serverController, DockerContainer container, ServerCredentials credentials, int &count); diff --git a/client/ui/models/protocols/ikev2ConfigModel.cpp b/client/ui/models/protocols/ikev2ConfigModel.cpp index f22b965c..a11b6652 100644 --- a/client/ui/models/protocols/ikev2ConfigModel.cpp +++ b/client/ui/models/protocols/ikev2ConfigModel.cpp @@ -1,4 +1,4 @@ -#include "ikev2ConfigModel.h".h " +#include "ikev2ConfigModel.h" #include "protocols/protocols_defs.h" diff --git a/client/ui/qml/Pages2/PageProtocolOpenVpnSettings.qml b/client/ui/qml/Pages2/PageProtocolOpenVpnSettings.qml index 971f9f39..571b8eaa 100644 --- a/client/ui/qml/Pages2/PageProtocolOpenVpnSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolOpenVpnSettings.qml @@ -81,7 +81,7 @@ PageType { Layout.fillWidth: true Layout.topMargin: 32 - headerText: qsTr("VPN Addresses Subnet") + headerText: qsTr("VPN address subnet") textFieldText: subnetAddress textField.onEditingFinished: { diff --git a/client/ui/qml/Pages2/PageServiceTorWebsiteSettings.qml b/client/ui/qml/Pages2/PageServiceTorWebsiteSettings.qml index aca4575c..eef174c3 100644 --- a/client/ui/qml/Pages2/PageServiceTorWebsiteSettings.qml +++ b/client/ui/qml/Pages2/PageServiceTorWebsiteSettings.qml @@ -91,7 +91,7 @@ PageType { onLinkActivated: Qt.openUrlExternally(link) textFormat: Text.RichText - text: qsTr("Use Tor Browser to open this url.") + text: qsTr("Use Tor Browser to open this URL.") } ParagraphTextType { @@ -100,7 +100,7 @@ PageType { Layout.leftMargin: 16 Layout.rightMargin: 16 - text: qsTr("After installation it takes several minutes while your onion site will become available in the Tor Network.") + text: qsTr("After creating your onion site, it takes a few minutes for the Tor network to make it available for use.") } ParagraphTextType { diff --git a/client/ui/qml/Pages2/PageSettingsAbout.qml b/client/ui/qml/Pages2/PageSettingsAbout.qml index b387cc64..f8128507 100644 --- a/client/ui/qml/Pages2/PageSettingsAbout.qml +++ b/client/ui/qml/Pages2/PageSettingsAbout.qml @@ -53,7 +53,7 @@ PageType { Layout.leftMargin: 16 Layout.rightMargin: 16 - text: qsTr("Support the project with a donation") + text: qsTr("Support Amnezia") horizontalAlignment: Text.AlignHCenter } diff --git a/client/ui/qml/Pages2/PageSettingsConnection.qml b/client/ui/qml/Pages2/PageSettingsConnection.qml index c12c335d..3a0c5c3c 100644 --- a/client/ui/qml/Pages2/PageSettingsConnection.qml +++ b/client/ui/qml/Pages2/PageSettingsConnection.qml @@ -83,7 +83,7 @@ PageType { Layout.fillWidth: true text: qsTr("DNS servers") - descriptionText: qsTr("If AmneziaDNS is not used or installed") + descriptionText: qsTr("When AmneziaDNS is not used or installed") rightImageSource: "qrc:/images/controls/chevron-right.svg" clickedFunction: function() { @@ -117,7 +117,7 @@ PageType { Layout.fillWidth: true text: qsTr("App-based split tunneling") - descriptionText: qsTr("Allows you to use the VPN only for certain applications") + descriptionText: qsTr("Allows you to use the VPN only for certain Apps") rightImageSource: "qrc:/images/controls/chevron-right.svg" clickedFunction: function() { diff --git a/client/ui/qml/Pages2/PageSettingsSplitTunneling.qml b/client/ui/qml/Pages2/PageSettingsSplitTunneling.qml index 4300d591..3aa5742a 100644 --- a/client/ui/qml/Pages2/PageSettingsSplitTunneling.qml +++ b/client/ui/qml/Pages2/PageSettingsSplitTunneling.qml @@ -56,7 +56,7 @@ PageType { QtObject { id: onlyForwardSites - property string name: qsTr("Addresses from the list should be accessed via VPN") + property string name: qsTr("Only the sites listed here will be accessed through the VPN") property int type: routeMode.onlyForwardSites } QtObject { @@ -251,7 +251,7 @@ PageType { TextFieldWithHeaderType { Layout.fillWidth: true - textFieldPlaceholderText: qsTr("Site or IP") + textFieldPlaceholderText: qsTr("website or IP") buttonImageSource: "qrc:/images/controls/plus.svg" clickedFunc: function() { @@ -295,7 +295,7 @@ PageType { Layout.fillWidth: true Layout.margins: 16 - headerText: qsTr("Import/Export Sites") + headerText: qsTr("Import / Export Sites") } LabelWithButtonType { diff --git a/client/ui/qml/Pages2/PageSetupWizardCredentials.qml b/client/ui/qml/Pages2/PageSetupWizardCredentials.qml index 3eadb647..4a1af4a5 100644 --- a/client/ui/qml/Pages2/PageSetupWizardCredentials.qml +++ b/client/ui/qml/Pages2/PageSetupWizardCredentials.qml @@ -73,7 +73,7 @@ PageType { property bool hidePassword: true Layout.fillWidth: true - headerText: qsTr("Password / SSH private key") + headerText: qsTr("Password or SSH private key") textField.echoMode: hidePassword ? TextInput.Password : TextInput.Normal buttonImageSource: textFieldText !== "" ? (hidePassword ? "qrc:/images/controls/eye.svg" : "qrc:/images/controls/eye-off.svg") : "" diff --git a/deploy/data/macos/pf/amn.000.allowLoopback.conf b/deploy/data/macos/pf/amn.000.allowLoopback.conf new file mode 100644 index 00000000..3c85b9c3 --- /dev/null +++ b/deploy/data/macos/pf/amn.000.allowLoopback.conf @@ -0,0 +1,3 @@ +# Always allow at least loopback/localhost traffic +set skip on lo0 +pass quick on lo0 flags any diff --git a/deploy/data/macos/pf/amn.100.blockAll.conf b/deploy/data/macos/pf/amn.100.blockAll.conf new file mode 100644 index 00000000..32182ed9 --- /dev/null +++ b/deploy/data/macos/pf/amn.100.blockAll.conf @@ -0,0 +1,3 @@ +# Block all traffic by default (can be overridden by later rules) +block out all flags any no state + diff --git a/deploy/data/macos/pf/amn.110.allowNets.conf b/deploy/data/macos/pf/amn.110.allowNets.conf new file mode 100644 index 00000000..6fed3b46 --- /dev/null +++ b/deploy/data/macos/pf/amn.110.allowNets.conf @@ -0,0 +1,2 @@ +table {} +pass out to flags any no state diff --git a/deploy/data/macos/pf/amn.120.blockNets.conf b/deploy/data/macos/pf/amn.120.blockNets.conf new file mode 100644 index 00000000..028f1c4f --- /dev/null +++ b/deploy/data/macos/pf/amn.120.blockNets.conf @@ -0,0 +1,2 @@ +table {} +block out to flags any no state diff --git a/deploy/data/macos/pf/amn.150.allowExcludedApps.conf b/deploy/data/macos/pf/amn.150.allowExcludedApps.conf new file mode 100644 index 00000000..57e15e99 --- /dev/null +++ b/deploy/data/macos/pf/amn.150.allowExcludedApps.conf @@ -0,0 +1,2 @@ +# Rules are set at runtime + diff --git a/deploy/data/macos/pf/amn.200.allowVPN.conf b/deploy/data/macos/pf/amn.200.allowVPN.conf new file mode 100644 index 00000000..6e1b74bc --- /dev/null +++ b/deploy/data/macos/pf/amn.200.allowVPN.conf @@ -0,0 +1,9 @@ +# Exempt the tunnel interface(s) used by the VPN connection + +utunInterfaces = "{ \ + utun0, utun1, utun2, utun3, utun4, utun5, utun6, utun7, utun8, utun9, utun10, \ + utun11, utun12, utun13, utun14, utun15, utun16, utun17, utun18, utun19, utun20, \ + utun21, utun22, utun23, utun24, utun25, utun26, utun27, utun28, utun29, utun30 \ +}" + +pass out on $utunInterfaces flags any no state diff --git a/deploy/data/macos/pf/amn.250.blockIPv6.conf b/deploy/data/macos/pf/amn.250.blockIPv6.conf new file mode 100644 index 00000000..f48d8886 --- /dev/null +++ b/deploy/data/macos/pf/amn.250.blockIPv6.conf @@ -0,0 +1,2 @@ +# Block all outgoing IPv6 traffic (even over the VPN) +block return out inet6 flags any no state diff --git a/deploy/data/macos/pf/amn.290.allowDHCP.conf b/deploy/data/macos/pf/amn.290.allowDHCP.conf new file mode 100644 index 00000000..5d92105d --- /dev/null +++ b/deploy/data/macos/pf/amn.290.allowDHCP.conf @@ -0,0 +1,5 @@ +# Allow DHCP +pass out inet proto udp from port 68 to 255.255.255.255 port 67 no state + +# Allow DHCPv6 +pass out inet6 proto udp from port 546 to ff00::/8 port 547 no state diff --git a/deploy/data/macos/pf/amn.300.allowLAN.conf b/deploy/data/macos/pf/amn.300.allowLAN.conf new file mode 100644 index 00000000..0ee82265 --- /dev/null +++ b/deploy/data/macos/pf/amn.300.allowLAN.conf @@ -0,0 +1,3 @@ +# Allow LAN IP ranges +table { 10.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.168.0.0/16, 224.0.0.0/4, 255.255.255.255/32, fc00::/7, fe80::/10, ff00::/8 } +pass out to flags any no state diff --git a/deploy/data/macos/pf/amn.310.blockDNS.conf b/deploy/data/macos/pf/amn.310.blockDNS.conf new file mode 100644 index 00000000..b7ca429b --- /dev/null +++ b/deploy/data/macos/pf/amn.310.blockDNS.conf @@ -0,0 +1,7 @@ +# Block all DNS traffic +block return out proto { tcp, udp } to port 53 flags any no state + +# Allow our DNS servers +table {} +pass out proto { tcp, udp } to port 53 flags any no state + diff --git a/deploy/data/macos/pf/amn.350.allowHnsd.conf b/deploy/data/macos/pf/amn.350.allowHnsd.conf new file mode 100644 index 00000000..3565ffcf --- /dev/null +++ b/deploy/data/macos/pf/amn.350.allowHnsd.conf @@ -0,0 +1,14 @@ +utunInterfaces = "{ \ + utun0, utun1, utun2, utun3, utun4, utun5, utun6, utun7, utun8, utun9, utun10, \ + utun11, utun12, utun13, utun14, utun15, utun16, utun17, utun18, utun19, utun20, \ + utun21, utun22, utun23, utun24, utun25, utun26, utun27, utun28, utun29, utun30 \ +}" + +hnsdGroup=amnhnsd + +# Block everything from handshake group +# Without this initial block hnsd traffic could possibly travel outside the tunnel (we don't trust the routing table) +block return out group $hnsdGroup flags any no state + +# Next, poke a hole in this block but only for traffic on the tunnel (port 13038 is the handshake control port) +pass out on $utunInterfaces proto { tcp, udp } to port { 53, 13038 } group $hnsdGroup flags any no state diff --git a/deploy/data/macos/pf/amn.400.allowPIA.conf b/deploy/data/macos/pf/amn.400.allowPIA.conf new file mode 100644 index 00000000..7c8a3680 --- /dev/null +++ b/deploy/data/macos/pf/amn.400.allowPIA.conf @@ -0,0 +1,2 @@ +# Allow traffic by privileged group (used by daemon) +pass out proto { tcp, udp } group { amnvpn } flags any no state diff --git a/deploy/data/macos/pf/amn.conf b/deploy/data/macos/pf/amn.conf new file mode 100644 index 00000000..224017d2 --- /dev/null +++ b/deploy/data/macos/pf/amn.conf @@ -0,0 +1,16 @@ +# This root anchor file establishes multiple sub-anchors which can be +# individually turned on or off; they have a numeric prefix in order to +# produce a well-defined alphabetical order. + +anchor "000.allowLoopback" +anchor "100.blockAll" +anchor "110.allowNets" +anchor "120.blockNets" +anchor "150.allowExcludedApps" +anchor "200.allowVPN" +anchor "250.blockIPv6" +anchor "290.allowDHCP" +anchor "300.allowLAN" +anchor "310.blockDNS" +anchor "350.allowHnsd" +anchor "400.allowPIA" diff --git a/ipc/ipc_interface.rep b/ipc/ipc_interface.rep index 8970f7c8..b7305250 100644 --- a/ipc/ipc_interface.rep +++ b/ipc/ipc_interface.rep @@ -1,5 +1,7 @@ #include #include +#include +#include "../client/daemon/interfaceconfig.h" class IpcInterface { @@ -19,8 +21,8 @@ class IpcInterface SLOT( void cleanUp() ); SLOT( void setLogsEnabled(bool enabled) ); - SLOT( bool copyWireguardConfig(const QString &sourcePath) ); - SLOT( bool isWireguardRunning() ); - SLOT( bool isWireguardConfigExists(const QString &configPath) ); + SLOT( bool disableKillSwitch() ); + SLOT( bool enablePeerTraffic( const QJsonObject &configStr) ); + SLOT( bool enableKillSwitch( const QJsonObject &excludeAddr, int vpnAdapterIndex) ); }; diff --git a/ipc/ipcserver.cpp b/ipc/ipcserver.cpp index e9f57c60..c0e87fc9 100644 --- a/ipc/ipcserver.cpp +++ b/ipc/ipcserver.cpp @@ -8,8 +8,18 @@ #include "router.h" #include "logger.h" +#include "../client/protocols/protocols_defs.h" #ifdef Q_OS_WIN #include "tapcontroller_win.h" +#include "../client/platforms/windows/daemon/windowsfirewall.h" +#endif + +#ifdef Q_OS_LINUX +#include "../client/platforms/linux/daemon/linuxfirewall.h" +#endif + +#ifdef Q_OS_MACOS +#include "../client/platforms/macos/daemon/macosfirewall.h" #endif IpcServer::IpcServer(QObject *parent): @@ -22,15 +32,14 @@ int IpcServer::createPrivilegedProcess() qDebug() << "IpcServer::createPrivilegedProcess"; #endif +#ifdef Q_OS_WIN + WindowsFirewall::instance()->init(); +#endif + m_localpid++; ProcessDescriptor pd(this); -// pd.serverNode->setHostUrl(QUrl(amnezia::getIpcProcessUrl(m_localpid))); -// pd.serverNode->enableRemoting(pd.ipcProcess.data()); - - - //pd.localServer = QSharedPointer(new QLocalServer(this)); pd.localServer->setSocketOptions(QLocalServer::WorldAccessOption); if (!pd.localServer->listen(amnezia::getIpcProcessUrl(m_localpid))) { @@ -165,61 +174,160 @@ void IpcServer::setLogsEnabled(bool enabled) } } -bool IpcServer::copyWireguardConfig(const QString &sourcePath) + +bool IpcServer::enableKillSwitch(const QJsonObject &configStr, int vpnAdapterIndex) { -#ifdef MZ_DEBUG - qDebug() << "IpcServer::copyWireguardConfig"; +#ifdef Q_OS_WIN + return WindowsFirewall::instance()->enableKillSwitch(vpnAdapterIndex); #endif -#ifdef Q_OS_LINUX - const QString wireguardConfigPath = "/etc/wireguard/wg99.conf"; - if (QFile::exists(wireguardConfigPath)) +#if defined(Q_OS_LINUX) || defined(Q_OS_MACOS) + int splitTunnelType = configStr.value("splitTunnelType").toInt(); + QJsonArray splitTunnelSites = configStr.value("splitTunnelSites").toArray(); + bool blockAll = 0; + bool allowNets = 0; + bool blockNets = 0; + QStringList allownets; + QStringList blocknets; + + if (splitTunnelType == 0) { - QFile::remove(wireguardConfigPath); + blockAll = true; + allowNets = true; + allownets.append(configStr.value(amnezia::config_key::hostName).toString()); + } else if (splitTunnelType == 1) + { + blockNets = true; + for (auto v : splitTunnelSites) { + blocknets.append(v.toString()); + } + } else if (splitTunnelType == 2) { + blockAll = true; + allowNets = true; + allownets.append(configStr.value(amnezia::config_key::hostName).toString()); + for (auto v : splitTunnelSites) { + allownets.append(v.toString()); + } } - - if (!QFile::copy(sourcePath, wireguardConfigPath)) { - qDebug() << "WireguardProtocol::WireguardProtocol error occurred while copying wireguard config:"; - return false; - } - return true; -#else - return false; -#endif -} - -bool IpcServer::isWireguardRunning() -{ -#ifdef MZ_DEBUG - qDebug() << "IpcServer::isWireguardRunning"; #endif #ifdef Q_OS_LINUX - QProcess checkWireguardStatusProcess; - - connect(&checkWireguardStatusProcess, &QProcess::errorOccurred, this, [](QProcess::ProcessError error) { - qDebug() << "WireguardProtocol::WireguardProtocol error occurred while checking wireguard status: " << error; - }); - - checkWireguardStatusProcess.setProgram("/bin/wg"); - checkWireguardStatusProcess.setArguments(QStringList{"show"}); - checkWireguardStatusProcess.start(); - checkWireguardStatusProcess.waitForFinished(10000); - QString output = checkWireguardStatusProcess.readAllStandardOutput(); - if (!output.isEmpty()) { - return true; - } - return false; -#else - return false; + // double-check + ensure our firewall is installed and enabled + LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("000.allowLoopback"), true); + LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("100.blockAll"), blockAll); + LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("110.allowNets"), allowNets); + LinuxFirewall::updateAllowNets(allownets); + LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("120.blockNets"), blockAll); + LinuxFirewall::updateBlockNets(blocknets); + LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("200.allowVPN"), true); + LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv6, QStringLiteral("250.blockIPv6"), true); + LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("290.allowDHCP"), true); + LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("300.allowLAN"), true); + QStringList dnsServers; + dnsServers.append(configStr.value(amnezia::config_key::dns1).toString()); + dnsServers.append(configStr.value(amnezia::config_key::dns2).toString()); + dnsServers.append("127.0.0.1"); + dnsServers.append("127.0.0.53"); + LinuxFirewall::updateDNSServers(dnsServers); + LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("320.allowDNS"), true); + LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("400.allowPIA"), true); #endif + +#ifdef Q_OS_MACOS + + // double-check + ensure our firewall is installed and enabled. This is necessary as + // other software may disable pfctl before re-enabling with their own rules (e.g other VPNs) + if (!MacOSFirewall::isInstalled()) MacOSFirewall::install(); + + MacOSFirewall::ensureRootAnchorPriority(); + MacOSFirewall::setAnchorEnabled(QStringLiteral("000.allowLoopback"), true); + MacOSFirewall::setAnchorEnabled(QStringLiteral("100.blockAll"), blockAll); + MacOSFirewall::setAnchorEnabled(QStringLiteral("110.allowNets"), allowNets); + MacOSFirewall::setAnchorTable(QStringLiteral("110.allowNets"), allowNets, + QStringLiteral("allownets"), allownets); + + MacOSFirewall::setAnchorEnabled(QStringLiteral("120.blockNets"), blockNets); + MacOSFirewall::setAnchorTable(QStringLiteral("120.blockNets"), blockNets, + QStringLiteral("blocknets"), blocknets); + MacOSFirewall::setAnchorEnabled(QStringLiteral("200.allowVPN"), true); + MacOSFirewall::setAnchorEnabled(QStringLiteral("250.blockIPv6"), true); + MacOSFirewall::setAnchorEnabled(QStringLiteral("290.allowDHCP"), true); + MacOSFirewall::setAnchorEnabled(QStringLiteral("300.allowLAN"), true); + + QStringList dnsServers; + dnsServers.append(configStr.value(amnezia::config_key::dns1).toString()); + dnsServers.append(configStr.value(amnezia::config_key::dns2).toString()); + MacOSFirewall::setAnchorEnabled(QStringLiteral("310.blockDNS"), true); + MacOSFirewall::setAnchorTable(QStringLiteral("310.blockDNS"), true, QStringLiteral("dnsaddr"), dnsServers); +#endif + + return true; } -bool IpcServer::isWireguardConfigExists(const QString &configPath) +bool IpcServer::disableKillSwitch() { -#ifdef MZ_DEBUG - qDebug() << "IpcServer::isWireguardConfigExists"; +#ifdef Q_OS_WIN + return WindowsFirewall::instance()->disableKillSwitch(); #endif - return QFileInfo::exists(configPath); +#ifdef Q_OS_LINUX + LinuxFirewall::uninstall(); +#endif + +#ifdef Q_OS_MACOS + MacOSFirewall::uninstall(); +#endif + + return true; +} + +bool IpcServer::enablePeerTraffic(const QJsonObject &configStr) +{ +#ifdef Q_OS_WIN + InterfaceConfig config; + config.m_dnsServer = configStr.value(amnezia::config_key::dns1).toString(); + config.m_serverPublicKey = "openvpn"; + config.m_serverIpv4Gateway = configStr.value("vpnGateway").toString(); + + int splitTunnelType = configStr.value("splitTunnelType").toInt(); + QJsonArray splitTunnelSites = configStr.value("splitTunnelSites").toArray(); + + qDebug() << "splitTunnelType " << splitTunnelType << "splitTunnelSites " << splitTunnelSites; + + QStringList AllowedIPAddesses; + + // Use APP split tunnel + if (splitTunnelType == 0 || splitTunnelType == 2) { + config.m_allowedIPAddressRanges.append( + IPAddress(QHostAddress("0.0.0.0"), 0)); + config.m_allowedIPAddressRanges.append( + IPAddress(QHostAddress("::"), 0)); + } + + if (splitTunnelType == 1) { + for (auto v : splitTunnelSites) { + QString ipRange = v.toString(); + qDebug() << "ipRange " << ipRange; + if (ipRange.split('/').size() > 1){ + config.m_allowedIPAddressRanges.append( + IPAddress(QHostAddress(ipRange.split('/')[0]), atoi(ipRange.split('/')[1].toLocal8Bit()))); + } else { + config.m_allowedIPAddressRanges.append( + IPAddress(QHostAddress(ipRange), 32)); + + } + } + } + + config.m_excludedAddresses.append(configStr.value(amnezia::config_key::hostName).toString()); + if (splitTunnelType == 2) { + for (auto v : splitTunnelSites) { + QString ipRange = v.toString(); + config.m_excludedAddresses.append(ipRange); + } + } + + return WindowsFirewall::instance()->enablePeerTraffic(config); +#endif + return true; } diff --git a/ipc/ipcserver.h b/ipc/ipcserver.h index d5706784..20bbb191 100644 --- a/ipc/ipcserver.h +++ b/ipc/ipcserver.h @@ -4,6 +4,8 @@ #include #include #include +#include +#include "../client/daemon/interfaceconfig.h" #include "ipc.h" #include "ipcserverprocess.h" @@ -25,9 +27,9 @@ public: virtual QStringList getTapList() override; virtual void cleanUp() override; virtual void setLogsEnabled(bool enabled) override; - virtual bool copyWireguardConfig(const QString &sourcePath) override; - virtual bool isWireguardRunning() override; - virtual bool isWireguardConfigExists(const QString &configPath) override; + virtual bool enablePeerTraffic(const QJsonObject &configStr) override; + virtual bool enableKillSwitch(const QJsonObject &excludeAddr, int vpnAdapterIndex) override; + virtual bool disableKillSwitch() override; private: int m_localpid = 0; diff --git a/service/server/CMakeLists.txt b/service/server/CMakeLists.txt index e34f5ca3..742b8ae3 100644 --- a/service/server/CMakeLists.txt +++ b/service/server/CMakeLists.txt @@ -182,6 +182,7 @@ if(APPLE) ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/macos/daemon/macosdaemon.h ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/macos/daemon/macosroutemonitor.h ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/macos/daemon/wireguardutilsmacos.h + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/macos/daemon/macosfirewall.h ) set(SOURCES ${SOURCES} @@ -195,6 +196,7 @@ if(APPLE) ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/macos/daemon/macosdaemon.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/macos/daemon/macosroutemonitor.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/macos/daemon/wireguardutilsmacos.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/macos/daemon/macosfirewall.cpp ) endif() @@ -211,6 +213,7 @@ if(LINUX) ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/daemon/dnsutilslinux.h ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/daemon/wireguardutilslinux.h ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/daemon/linuxroutemonitor.h + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/daemon/linuxfirewall.h ) set(SOURCES ${SOURCES} @@ -223,6 +226,7 @@ if(LINUX) ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/daemon/linuxdaemon.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/daemon/wireguardutilslinux.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/daemon/linuxroutemonitor.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/daemon/linuxfirewall.cpp ) endif()