MacOS WG/AWG killswitch
This commit is contained in:
parent
1a17f2956a
commit
3d2174d84e
23 changed files with 397 additions and 51 deletions
167
client/platforms/macos/daemon/macosfirewall.cpp
Normal file
167
client/platforms/macos/daemon/macosfirewall.cpp
Normal file
|
@ -0,0 +1,167 @@
|
|||
#include "macosfirewall.h"
|
||||
#include "logger.h"
|
||||
#include <QProcess>
|
||||
#include <QCoreApplication>
|
||||
|
||||
#define BRAND_IDENTIFIER "amn"
|
||||
|
||||
namespace {
|
||||
Logger logger("MacOSFirewall");
|
||||
} // namespace
|
||||
|
||||
#include "macosfirewall.h"
|
||||
|
||||
#define ResourceDir "./pf"
|
||||
#define DaemonDataDir "./pf"
|
||||
|
||||
#include <QProcess>
|
||||
|
||||
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);
|
||||
}
|
63
client/platforms/macos/daemon/macosfirewall.h
Normal file
63
client/platforms/macos/daemon/macosfirewall.h
Normal file
|
@ -0,0 +1,63 @@
|
|||
#ifndef MACOSFIREWALL_H
|
||||
#define MACOSFIREWALL_H
|
||||
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
// Descriptor for a set of firewall rules to be appled.
|
||||
//
|
||||
struct FirewallParams
|
||||
{
|
||||
QStringList dnsServers;
|
||||
// QSharedPointer<NetworkAdapter> adapter;
|
||||
QVector<QString> 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
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue