diff --git a/client/platforms/macos/daemon/macosfirewall.cpp b/client/platforms/macos/daemon/macosfirewall.cpp new file mode 100644 index 00000000..f72b0efc --- /dev/null +++ b/client/platforms/macos/daemon/macosfirewall.cpp @@ -0,0 +1,167 @@ +#include "macosfirewall.h" +#include "logger.h" +#include +#include + +#define BRAND_IDENTIFIER "amn" + +namespace { + Logger logger("MacOSFirewall"); +} // namespace + +#include "macosfirewall.h" + +#define ResourceDir "./pf" +#define DaemonDataDir "./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..8b13d363 --- /dev/null +++ b/client/platforms/macos/daemon/macosfirewall.h @@ -0,0 +1,63 @@ +#ifndef MACOSFIREWALL_H +#define MACOSFIREWALL_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 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) +}; + +// TODO: Break out firewall handling to a base class that can be used directly +// by the base daemon class, for some common functionality. + +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 0c12271c..f9196fc3 100644 --- a/client/protocols/openvpnprotocol.cpp +++ b/client/protocols/openvpnprotocol.cpp @@ -55,7 +55,7 @@ void OpenVpnProtocol::stop() m_managementServer.stop(); } -#if defined(Q_OS_WIN) || defined(Q_OS_LINUX) +#if defined(Q_OS_WIN) || defined(Q_OS_LINUX) || defined(Q_OS_MACOS) IpcClient::Interface()->disableKillSwitch(); #endif @@ -345,7 +345,7 @@ void OpenVpnProtocol::updateVpnGateway(const QString &line) } } #endif -#ifdef Q_OS_LINUX +#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/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/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/ipcserver.cpp b/ipc/ipcserver.cpp index cc918629..580cf1a9 100644 --- a/ipc/ipcserver.cpp +++ b/ipc/ipcserver.cpp @@ -18,6 +18,10 @@ #include "../client/platforms/linux/daemon/linuxfirewall.h" #endif +#ifdef Q_OS_MACOS +#include "../client/platforms/macos/daemon/macosfirewall.h" +#endif + IpcServer::IpcServer(QObject *parent): IpcInterfaceSource(parent) {} @@ -208,6 +212,37 @@ bool IpcServer::enableKillSwitch(const QJsonObject &configStr, int vpnAdapterInd // true, // LinuxFirewall::kRawTable); #endif + +#ifdef Q_OS_MACOS + + if (configStr.value(amnezia::config_key::splitTunnelType) == 0) { + // 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"), true); + MacOSFirewall::setAnchorEnabled(QStringLiteral("110.allowNets"), false); + MacOSFirewall::setAnchorTable(QStringLiteral("110.allowNets"), false, + QStringLiteral("allownets"), QStringList()); + + MacOSFirewall::setAnchorEnabled(QStringLiteral("120.blockNets"), false); + MacOSFirewall::setAnchorTable(QStringLiteral("120.blockNets"), false, + QStringLiteral("blocknets"), QStringList()); + 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; } @@ -220,6 +255,11 @@ bool IpcServer::disableKillSwitch() #ifdef Q_OS_LINUX LinuxFirewall::uninstall(); #endif + +#ifdef Q_OS_MACOS + MacOSFirewall::uninstall(); +#endif + return true; } diff --git a/service/server/CMakeLists.txt b/service/server/CMakeLists.txt index 31bf5d3d..985186f0 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()