diff --git a/client/platforms/linux/daemon/linuxfirewall.cpp b/client/platforms/linux/daemon/linuxfirewall.cpp new file mode 100644 index 00000000..ab51ea74 --- /dev/null +++ b/client/platforms/linux/daemon/linuxfirewall.cpp @@ -0,0 +1,462 @@ +#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::getExcludeRule(const QStringList& servers) +{ + QStringList result; + for (const QString& server : servers) + { + result << QStringLiteral("-d %1 -j ACCEPT").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(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(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::updateExcludeAddrs(const QStringList& servers) +{ + static QStringList existingServers {}; + + existingServers = servers; + execute(QStringLiteral("iptables -F %1.100.blockAll").arg(kAnchorName)); + for (const QString& rule : getExcludeRule(servers)) + execute(QStringLiteral("iptables -A %1.100.blockAll %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..486fafbc --- /dev/null +++ b/client/platforms/linux/daemon/linuxfirewall.h @@ -0,0 +1,73 @@ +#ifndef LINUXFIREWALL_H +#define LINUXFIREWALL_H + + +#include +#include + +// Descriptor for a set of firewall rules to be appled. +// +struct FirewallParams +{ + QStringList dnsServers; + // QSharedPointer adapter; + QVector excludeApps; // Apps to exclude if VPN exemptions are enabled + + QStringList excludeAddrs; + // 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) +}; + +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 getExcludeRule(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 updateExcludeAddrs(const QStringList& servers); +}; + + +#endif // LINUXFIREWALL_H diff --git a/client/platforms/linux/daemon/wireguardutilslinux.cpp b/client/platforms/linux/daemon/wireguardutilslinux.cpp index 792120a7..79f7dd2d 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" @@ -117,6 +119,12 @@ bool WireguardUtilsLinux::addInterface(const InterfaceConfig& config) { if (err != 0) { logger.error() << "Interface configuration failed:" << strerror(err); } + + FirewallParams params {}; + params.dnsServers.append(config.m_dnsServer); + params.excludeAddrs.append(config.m_serverIpv4AddrIn); + applyFirewallRules(params); + return (err == 0); } @@ -140,6 +148,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 +263,34 @@ 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"), true); + LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, 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::Both, QStringLiteral("310.blockDNS"), true); + LinuxFirewall::updateDNSServers(params.dnsServers); + LinuxFirewall::updateExcludeAddrs(params.excludeAddrs); + LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("320.allowDNS"), true); + LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("400.allowPIA"), true); + + LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, + QStringLiteral("100.vpnTunOnly"), + true, + LinuxFirewall::kRawTable); + +} + 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/protocols/openvpnprotocol.cpp b/client/protocols/openvpnprotocol.cpp index c7cc839d..0c12271c 100644 --- a/client/protocols/openvpnprotocol.cpp +++ b/client/protocols/openvpnprotocol.cpp @@ -55,7 +55,7 @@ void OpenVpnProtocol::stop() m_managementServer.stop(); } -#ifdef Q_OS_WIN +#if defined(Q_OS_WIN) || defined(Q_OS_LINUX) IpcClient::Interface()->disableKillSwitch(); #endif @@ -338,12 +338,15 @@ void OpenVpnProtocol::updateVpnGateway(const QString &line) 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(netInterfaces.at(i).index()); + IpcClient::Interface()->enableKillSwitch(QJsonObject(), netInterfaces.at(i).index()); m_configData.insert("vpnGateway", m_vpnGateway); IpcClient::Interface()->enablePeerTraffic(m_configData); } } } +#endif +#ifdef Q_OS_LINUX + IpcClient::Interface()->enableKillSwitch(m_configData, 0); #endif qDebug() << QString("Set vpn local address %1, gw %2").arg(m_vpnLocalAddress).arg(vpnGateway()); } diff --git a/ipc/ipc_interface.rep b/ipc/ipc_interface.rep index faf70079..67520834 100644 --- a/ipc/ipc_interface.rep +++ b/ipc/ipc_interface.rep @@ -24,8 +24,8 @@ class IpcInterface SLOT( bool copyWireguardConfig(const QString &sourcePath) ); SLOT( bool isWireguardRunning() ); SLOT( bool isWireguardConfigExists(const QString &configPath) ); - SLOT( bool enableKillSwitch(int vpnAdapterIndex) ); SLOT( bool disableKillSwitch() ); - SLOT( bool enablePeerTraffic(const QJsonObject &configStr)); + 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 4517a5bf..5fb32101 100644 --- a/ipc/ipcserver.cpp +++ b/ipc/ipcserver.cpp @@ -12,7 +12,10 @@ #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 IpcServer::IpcServer(QObject *parent): @@ -217,21 +220,44 @@ bool IpcServer::isWireguardRunning() #endif } -bool IpcServer::isWireguardConfigExists(const QString &configPath) -{ -#ifdef MZ_DEBUG - qDebug() << "IpcServer::isWireguardConfigExists"; -#endif - - return QFileInfo::exists(configPath); -} - -bool IpcServer::enableKillSwitch(int vpnAdapterIndex) +bool IpcServer::enableKillSwitch(const QJsonObject &configStr, int vpnAdapterIndex) { #ifdef Q_OS_WIN return WindowsFirewall::instance()->enableKillSwitch(vpnAdapterIndex); #endif + + // 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"), true); + LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, 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::Both, QStringLiteral("310.blockDNS"), true); + QStringList serverAddr; + serverAddr.append(configStr.value(amnezia::config_key::hostName).toString()); + LinuxFirewall::updateExcludeAddrs(serverAddr); + 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); + + // LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, + // QStringLiteral("100.vpnTunOnly"), + // true, + // LinuxFirewall::kRawTable); return true; + + } bool IpcServer::disableKillSwitch() @@ -239,6 +265,8 @@ bool IpcServer::disableKillSwitch() #ifdef Q_OS_WIN return WindowsFirewall::instance()->disableKillSwitch(); #endif + + LinuxFirewall::uninstall(); return true; } diff --git a/ipc/ipcserver.h b/ipc/ipcserver.h index 1508049a..9a68ed5b 100644 --- a/ipc/ipcserver.h +++ b/ipc/ipcserver.h @@ -30,9 +30,9 @@ public: virtual bool copyWireguardConfig(const QString &sourcePath) override; virtual bool isWireguardRunning() override; virtual bool isWireguardConfigExists(const QString &configPath) override; - virtual bool enableKillSwitch(int vpnAdapterIndex) override; - virtual bool disableKillSwitch() 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..31bf5d3d 100644 --- a/service/server/CMakeLists.txt +++ b/service/server/CMakeLists.txt @@ -223,6 +223,8 @@ 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.h + ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/daemon/linuxfirewall.cpp ) endif()