feat: split daemon activation into bare bring-up and setPrimary

This commit is contained in:
cd-amn
2026-05-18 16:44:42 +00:00
parent adb8eb4937
commit 864b8c6f8a
21 changed files with 295 additions and 427 deletions
+201 -190
View File
@@ -34,8 +34,8 @@ Daemon::Daemon(QObject* parent) : QObject(parent) {
Q_ASSERT(s_daemon == nullptr);
s_daemon = this;
m_handshakeTimer.setSingleShot(true);
connect(&m_handshakeTimer, &QTimer::timeout, this, &Daemon::checkHandshake);
m_activationTimer.setSingleShot(false);
connect(&m_activationTimer, &QTimer::timeout, this, &Daemon::checkActivations);
}
Daemon::~Daemon() {
@@ -43,6 +43,9 @@ Daemon::~Daemon() {
logger.debug() << "Daemon released";
qDeleteAll(m_tunnels);
m_tunnels.clear();
Q_ASSERT(s_daemon == this);
s_daemon = nullptr;
}
@@ -53,69 +56,36 @@ Daemon* Daemon::instance() {
return s_daemon;
}
bool Daemon::activate(const InterfaceConfig& config) {
Q_ASSERT(wgutils() != nullptr);
bool Daemon::activate(const QString& ifname, const InterfaceConfig& config) {
logger.debug() << "Activating tunnel";
// There are 3 possible scenarios in which this method is called:
//
// 1. the VPN is off: the method tries to enable the VPN.
// 2. the VPN is on and the platform doesn't support the server-switching:
// this method calls deactivate() and then it continues as 1.
// 3. the VPN is on and the platform supports the server-switching: this
// method calls switchServer().
//
// At the end, if the activation succeds, the `connected` signal is emitted.
// If the activation abort's for any reason `the `activationFailure` signal is
// emitted.
logger.debug() << "Activating interface";
auto emit_failure_guard = qScopeGuard([this] { emit activationFailure(); });
if (m_connections.contains(config.m_hopType)) {
if (supportServerSwitching(config)) {
logger.debug() << "Already connected. Server switching supported.";
if (!switchServer(config)) {
WireguardUtils* wg = m_tunnels.value(ifname);
if (!wg) {
wg = createWgUtils();
if (!wg) {
logger.error() << "Failed to create wireguard utils.";
return false;
}
if (!dnsutils()->restoreResolvers()) {
return false;
m_tunnels.insert(ifname, wg);
}
if (m_primaryIfname.isEmpty()) {
m_primaryIfname = ifname;
}
if (!maybeUpdateResolvers(config)) {
return false;
}
ConnectionState& cs = m_connections[ifname];
cs.m_config = config;
cs.m_date = QDateTime();
cs.m_deadline = QDateTime::currentDateTime().addMSecs(ACTIVATION_TIMEOUT_MSEC);
bool status = run(Switch, config);
logger.debug() << "Connection status:" << status;
if (status) {
m_connections[config.m_hopType] = ConnectionState(config);
m_handshakeTimer.start(HANDSHAKE_POLL_MSEC);
emit_failure_guard.dismiss();
return true;
}
return false;
}
logger.warning() << "Already connected. Server switching not supported.";
if (!deactivate(false)) {
return false;
}
Q_ASSERT(!m_connections.contains(config.m_hopType));
if (activate(config)) {
emit_failure_guard.dismiss();
return true;
}
return false;
}
auto failure_guard = qScopeGuard([this, ifname] {
deactivateTunnel(ifname);
});
prepareActivation(config);
// Bring up the wireguard interface if not already done.
if (!wgutils()->interfaceExists()) {
// Create the interface.
if (!wgutils()->addInterface(config)) {
if (!wg->interfaceExists()) {
if (!wg->addInterface(config)) {
logger.error() << "Interface creation failed.";
return false;
}
@@ -131,38 +101,137 @@ bool Daemon::activate(const InterfaceConfig& config) {
}
}
// Configure routing for excluded addresses.
for (const QString& i : config.m_excludedAddresses) {
addExclusionRoute(IPAddress(i));
if (!config.m_serverIpv4AddrIn.isEmpty()) {
addExclusionRoute(IPAddress(config.m_serverIpv4AddrIn));
}
if (!config.m_serverIpv6AddrIn.isEmpty()) {
addExclusionRoute(IPAddress(config.m_serverIpv6AddrIn));
}
// Add the peer to this interface.
if (!wgutils()->updatePeer(config)) {
if (!wg->updatePeer(config)) {
logger.error() << "Peer creation failed.";
return false;
}
if (!maybeUpdateResolvers(config)) {
return false;
if (!m_activationTimer.isActive()) {
m_activationTimer.start(HANDSHAKE_POLL_MSEC);
}
// set routing
for (const IPAddress& ip : config.m_allowedIPAddressRanges) {
if (!wgutils()->updateRoutePrefix(ip)) {
logger.debug() << "Routing configuration failed for" << ip.toString();
return false;
}
}
bool status = run(Up, config);
logger.debug() << "Connection status:" << status;
if (status) {
m_connections[config.m_hopType] = ConnectionState(config);
m_handshakeTimer.start(HANDSHAKE_POLL_MSEC);
emit_failure_guard.dismiss();
failure_guard.dismiss();
return true;
}
}
bool Daemon::setPrimary(const QString& ifname, const InterfaceConfig& config) {
WireguardUtils* wg = m_tunnels.value(ifname);
if (!wg) {
logger.error() << "setPrimary: no tunnel for" << ifname;
return false;
}
logger.debug() << "setPrimary" << wg->interfaceName();
const QString priorPrimary = m_primaryIfname;
m_primaryIfname = ifname;
auto failure_guard = qScopeGuard([this, ifname, priorPrimary] {
deactivateTunnel(ifname);
m_primaryIfname = priorPrimary;
});
for (const QString& i : config.m_excludedAddresses) {
addExclusionRoute(IPAddress(i));
}
for (const IPAddress& ip : config.m_allowedIPAddressRanges) {
if (!wg->updateRoutePrefix(ip)) {
logger.warning() << "setPrimary: route setup failed for" << ip.toString();
}
}
if (!maybeUpdateResolvers(config)) {
logger.warning() << "setPrimary: DNS resolver update failed";
}
if (!run(Up, config)) {
return false;
}
m_connections[ifname].m_config = config;
// Demote the prior primary AFTER the new primary is fully installed.
// Delete-after-install order preserves coverage during the make-before-break overlap.
if (!priorPrimary.isEmpty() && priorPrimary != ifname) {
demotePrimary(priorPrimary);
}
failure_guard.dismiss();
return true;
}
void Daemon::demotePrimary(const QString& ifname) {
WireguardUtils* wg = wgutilsFor(ifname);
if (!wg) {
return;
}
const ConnectionState cs = m_connections.value(ifname);
const InterfaceConfig& config = cs.m_config;
for (const IPAddress& ip : config.m_allowedIPAddressRanges) {
wg->deleteRoutePrefix(ip);
}
for (const QString& addr : config.m_excludedAddresses) {
if (addr.isEmpty()) {
continue;
}
IPAddress ip(addr);
if (m_excludedAddrSet.contains(ip)) {
delExclusionRoute(ip);
}
}
}
bool Daemon::deactivateTunnel(const QString& ifname) {
WireguardUtils* wg = m_tunnels.value(ifname);
const ConnectionState cs = m_connections.value(ifname);
const InterfaceConfig& config = cs.m_config;
const bool wasPrimary = (ifname == m_primaryIfname);
if (wg) {
logger.debug() << "deactivateTunnel" << wg->interfaceName();
if (wasPrimary) {
for (const IPAddress& ip : config.m_allowedIPAddressRanges) {
wg->deleteRoutePrefix(ip);
}
}
wg->deletePeer(config);
auto removeExclusion = [&](const QString& addr) {
if (addr.isEmpty()) {
return;
}
IPAddress ip(addr);
if (m_excludedAddrSet.contains(ip)) {
delExclusionRoute(ip);
}
};
removeExclusion(config.m_serverIpv4AddrIn);
removeExclusion(config.m_serverIpv6AddrIn);
if (wasPrimary) {
for (const QString& i : config.m_excludedAddresses) {
removeExclusion(i);
}
}
wg->deleteInterface();
m_tunnels.remove(ifname);
delete wg;
}
m_connections.remove(ifname);
if (wasPrimary) {
m_primaryIfname.clear();
}
return true;
}
bool Daemon::maybeUpdateResolvers(const InterfaceConfig& config) {
@@ -180,7 +249,8 @@ bool Daemon::maybeUpdateResolvers(const InterfaceConfig& config) {
resolvers.append(QHostAddress(config.m_serverIpv6Gateway));
}
if (!dnsutils()->updateResolvers(wgutils()->interfaceName(), resolvers)) {
const QString ifname = wgutilsFor(config.m_ifname)->interfaceName();
if (!dnsutils()->updateResolvers(ifname, resolvers)) {
return false;
}
}
@@ -214,7 +284,7 @@ bool Daemon::addExclusionRoute(const IPAddress& prefix) {
m_excludedAddrSet[prefix]++;
return true;
}
if (!wgutils()->addExclusionRoute(prefix)) {
if (!primaryWgutils()->addExclusionRoute(prefix)) {
return false;
}
m_excludedAddrSet[prefix] = 1;
@@ -228,7 +298,8 @@ bool Daemon::delExclusionRoute(const IPAddress& prefix) {
return true;
}
m_excludedAddrSet.remove(prefix);
return wgutils()->deleteExclusionRoute(prefix);
WireguardUtils* wg = primaryWgutils();
return wg && wg->deleteExclusionRoute(prefix);
}
// static
@@ -440,18 +511,16 @@ bool Daemon::parseConfig(const QJsonObject& obj, InterfaceConfig& config) {
if (!obj.value("I5").isNull()) {
config.m_specialJunk["I5"] = obj.value("I5").toString();
}
config.m_ifname = obj.value("ifname").toString(WG_INTERFACE);
config.m_ifname = obj.value("ifname").toString();
return true;
}
bool Daemon::deactivate(bool emitSignals) {
Q_ASSERT(wgutils() != nullptr);
const QString primary = m_primaryIfname;
// Deactivate the main interface.
if (!m_connections.isEmpty()) {
const ConnectionState& state = m_connections.first();
if (!run(Down, state.m_config)) {
if (m_connections.contains(primary)) {
if (!run(Down, m_connections.value(primary).m_config)) {
return false;
}
}
@@ -460,31 +529,22 @@ bool Daemon::deactivate(bool emitSignals) {
emit disconnected();
}
// Cleanup DNS
if (!dnsutils()->restoreResolvers()) {
logger.warning() << "Failed to restore DNS resolvers.";
}
// Cleanup peers and routing
for (const ConnectionState& state : m_connections) {
const InterfaceConfig& config = state.m_config;
logger.debug() << "Deleting routes for" << config.m_hopType;
for (const IPAddress& ip : config.m_allowedIPAddressRanges) {
wgutils()->deleteRoutePrefix(ip);
const QStringList ifnames = m_tunnels.keys();
for (const QString& ifname : ifnames) {
if (ifname != primary) {
deactivateTunnel(ifname);
}
wgutils()->deletePeer(config);
}
if (m_tunnels.contains(primary)) {
deactivateTunnel(primary);
}
// Cleanup routing for excluded addresses.
for (auto iterator = m_excludedAddrSet.constBegin();
iterator != m_excludedAddrSet.constEnd(); ++iterator) {
wgutils()->deleteExclusionRoute(iterator.key());
}
m_excludedAddrSet.clear();
m_connections.clear();
// Delete the interface
return wgutils()->deleteInterface();
m_activationTimer.stop();
return true;
}
QString Daemon::logs() {
@@ -493,79 +553,18 @@ QString Daemon::logs() {
void Daemon::cleanLogs() { }
bool Daemon::supportServerSwitching(const InterfaceConfig& config) const {
if (!m_connections.contains(config.m_hopType)) {
return false;
}
const InterfaceConfig& current =
m_connections.value(config.m_hopType).m_config;
return current.m_privateKey == config.m_privateKey &&
current.m_deviceIpv4Address == config.m_deviceIpv4Address &&
current.m_deviceIpv6Address == config.m_deviceIpv6Address &&
current.m_serverIpv4Gateway == config.m_serverIpv4Gateway &&
current.m_serverIpv6Gateway == config.m_serverIpv6Gateway;
}
bool Daemon::switchServer(const InterfaceConfig& config) {
Q_ASSERT(wgutils() != nullptr);
logger.debug() << "Switching server for" << config.m_hopType;
Q_ASSERT(m_connections.contains(config.m_hopType));
const InterfaceConfig& lastConfig =
m_connections.value(config.m_hopType).m_config;
// Configure routing for new excluded addresses.
for (const QString& i : config.m_excludedAddresses) {
addExclusionRoute(IPAddress(i));
}
// Activate the new peer and its routes.
if (!wgutils()->updatePeer(config)) {
logger.error() << "Server switch failed to update the wireguard interface";
return false;
}
for (const IPAddress& ip : config.m_allowedIPAddressRanges) {
if (!wgutils()->updateRoutePrefix(ip)) {
logger.error() << "Server switch failed to update the routing table";
break;
}
}
// Remove routing entries for the old peer.
for (const QString& i : lastConfig.m_excludedAddresses) {
delExclusionRoute(QHostAddress(i));
}
for (const IPAddress& ip : lastConfig.m_allowedIPAddressRanges) {
if (!config.m_allowedIPAddressRanges.contains(ip)) {
wgutils()->deleteRoutePrefix(ip);
}
}
// Remove the old peer if it is no longer necessary.
if (config.m_serverPublicKey != lastConfig.m_serverPublicKey) {
if (!wgutils()->deletePeer(lastConfig)) {
return false;
}
}
m_connections[config.m_hopType] = ConnectionState(config);
return true;
}
QJsonObject Daemon::getStatus() {
Q_ASSERT(wgutils() != nullptr);
QJsonObject json;
logger.debug() << "Status request";
if (!wgutils()->interfaceExists() || m_connections.isEmpty()) {
WireguardUtils* wg = primaryWgutils();
if (!wg || !wg->interfaceExists() || !m_connections.contains(m_primaryIfname)) {
json.insert("connected", QJsonValue(false));
return json;
}
const ConnectionState& connection = m_connections.first();
QList<WireguardUtils::PeerStatus> peers = wgutils()->getPeerStatus();
const ConnectionState& connection = m_connections.value(m_primaryIfname);
QList<WireguardUtils::PeerStatus> peers = wg->getPeerStatus();
for (const WireguardUtils::PeerStatus& status : peers) {
if (status.m_pubkey != connection.m_config.m_serverPublicKey) {
continue;
@@ -585,38 +584,50 @@ QJsonObject Daemon::getStatus() {
return json;
}
void Daemon::checkHandshake() {
Q_ASSERT(wgutils() != nullptr);
void Daemon::checkActivations() {
const QDateTime now = QDateTime::currentDateTime();
QStringList timedOut;
bool anyPending = false;
logger.debug() << "Checking for handshake...";
int pendingHandshakes = 0;
QList<WireguardUtils::PeerStatus> peers = wgutils()->getPeerStatus();
for (ConnectionState& connection : m_connections) {
const InterfaceConfig& config = connection.m_config;
if (connection.m_date.isValid()) {
continue;
for (auto it = m_connections.begin(); it != m_connections.end(); ++it) {
const QString& ifname = it.key();
ConnectionState& cs = it.value();
if (cs.m_date.isValid()) {
continue; // already handshaked
}
logger.debug() << "awaiting" << config.m_serverPublicKey;
logger.debug() << "awaiting" << cs.m_config.m_serverPublicKey;
// Check if the handshake has completed.
for (const WireguardUtils::PeerStatus& status : peers) {
if (config.m_serverPublicKey != status.m_pubkey) {
WireguardUtils* wg = m_tunnels.value(ifname);
bool handshaked = false;
if (wg) {
for (const WireguardUtils::PeerStatus& status : wg->getPeerStatus()) {
if (status.m_pubkey != cs.m_config.m_serverPublicKey) {
continue;
}
if (status.m_handshake != 0) {
connection.m_date.setMSecsSinceEpoch(status.m_handshake);
emit connected(status.m_pubkey);
cs.m_date.setMSecsSinceEpoch(status.m_handshake);
emit tunnelConnected(ifname, status.m_pubkey);
handshaked = true;
}
}
}
if (handshaked) {
continue;
}
if (cs.m_deadline.isValid() && now > cs.m_deadline) {
timedOut.append(ifname);
} else {
anyPending = true;
}
}
if (!connection.m_date.isValid()) {
pendingHandshakes++;
}
for (const QString& ifname : timedOut) {
logger.warning() << "Tunnel handshake timed out:" << m_tunnels.value(ifname)->interfaceName();
emit tunnelHandshakeFailed(ifname);
deactivateTunnel(ifname);
}
// Check again if there were connections that haven't completed a handshake.
if (pendingHandshakes > 0) {
m_handshakeTimer.start(HANDSHAKE_POLL_MSEC);
if (!anyPending) {
m_activationTimer.stop();
}
}
+17 -17
View File
@@ -22,7 +22,6 @@ class Daemon : public QObject {
enum Op {
Up,
Down,
Switch,
};
explicit Daemon(QObject* parent);
@@ -32,10 +31,15 @@ class Daemon : public QObject {
static bool parseConfig(const QJsonObject& obj, InterfaceConfig& config);
virtual bool activate(const InterfaceConfig& config);
bool activate(const QString& ifname, const InterfaceConfig& config);
bool setPrimary(const QString& ifname, const InterfaceConfig& config);
bool deactivateTunnel(const QString& ifname);
virtual bool deactivate(bool emitSignals = true);
virtual QJsonObject getStatus();
const QString& primaryIfname() const { return m_primaryIfname; }
WireguardUtils* wgutilsFor(const QString& ifname) const { return m_tunnels.value(ifname); }
// Callback before any Activating measure is done
virtual void prepareActivation(const InterfaceConfig& config, int inetAdapterIndex = 0) {
Q_UNUSED(config) };
@@ -46,12 +50,8 @@ class Daemon : public QObject {
void cleanLogs();
signals:
void connected(const QString& pubkey);
/**
* Can be fired if a call to activate() was unsucessfull
* and connected systems should rollback
*/
void activationFailure();
void tunnelConnected(const QString& ifname, const QString& pubkey);
void tunnelHandshakeFailed(const QString& ifname);
void disconnected();
void backendFailure(DaemonError reason = DaemonError::ERROR_FATAL);
@@ -59,6 +59,10 @@ class Daemon : public QObject {
bool maybeUpdateResolvers(const InterfaceConfig& config);
bool addExclusionRoute(const IPAddress& address);
bool delExclusionRoute(const IPAddress& address);
void demotePrimary(const QString& ifname);
void checkActivations();
WireguardUtils* primaryWgutils() const { return m_tunnels.value(m_primaryIfname); }
QTimer m_activationTimer;
protected:
virtual bool run(Op op, const InterfaceConfig& config) {
@@ -66,13 +70,11 @@ class Daemon : public QObject {
Q_UNUSED(config);
return true;
}
virtual bool supportServerSwitching(const InterfaceConfig& config) const;
virtual bool switchServer(const InterfaceConfig& config);
virtual WireguardUtils* wgutils() const = 0;
virtual WireguardUtils* createWgUtils() = 0;
virtual void replaceActiveWgUtils(WireguardUtils* newUtils) = 0;
WireguardUtils* m_stagingWgutils = nullptr;
QMap<QString, WireguardUtils*> m_tunnels;
QString m_primaryIfname;
virtual bool supportIPUtils() const { return false; }
virtual IPUtils* iputils() { return nullptr; }
virtual DnsUtils* dnsutils() { return nullptr; }
@@ -80,18 +82,16 @@ class Daemon : public QObject {
static bool parseStringList(const QJsonObject& obj, const QString& name,
QStringList& list);
void checkHandshake();
class ConnectionState {
public:
ConnectionState(){};
ConnectionState(const InterfaceConfig& config) { m_config = config; }
QDateTime m_date;
QDateTime m_deadline;
InterfaceConfig m_config;
};
QMap<InterfaceConfig::HopType, ConnectionState> m_connections;
QMap<QString, ConnectionState> m_connections;
QHash<IPAddress, int> m_excludedAddrSet;
QTimer m_handshakeTimer;
};
#endif // DAEMON_H
+44 -8
View File
@@ -31,8 +31,10 @@ DaemonLocalServerConnection::DaemonLocalServerConnection(QObject* parent,
&DaemonLocalServerConnection::readData);
Daemon* daemon = Daemon::instance();
connect(daemon, &Daemon::connected, this,
&DaemonLocalServerConnection::connected);
connect(daemon, &Daemon::tunnelConnected,
this, &DaemonLocalServerConnection::onTunnelConnected);
connect(daemon, &Daemon::tunnelHandshakeFailed,
this, &DaemonLocalServerConnection::onTunnelHandshakeFailed);
connect(daemon, &Daemon::disconnected, this,
&DaemonLocalServerConnection::disconnected);
connect(daemon, &Daemon::backendFailure, this,
@@ -107,19 +109,44 @@ void DaemonLocalServerConnection::parseCommand(const QByteArray& data) {
InterfaceConfig config;
if (!Daemon::parseConfig(obj, config)) {
logger.error() << "Invalid configuration";
emit disconnected();
disconnected();
return;
}
if (!Daemon::instance()->activate(config)) {
if (!Daemon::instance()->activate(config.m_ifname, config)) {
logger.error() << "Failed to activate the interface";
emit disconnected();
disconnected();
}
return;
}
if (type == "deactivate") {
const QString ifname = obj.value("ifname").toString();
if (!ifname.isEmpty()) {
Daemon::instance()->deactivateTunnel(ifname);
} else {
Daemon::instance()->deactivate(true);
}
return;
}
if (type == "setPrimary") {
InterfaceConfig config;
if (!Daemon::parseConfig(obj, config)) {
logger.error() << "setPrimary: invalid configuration";
return;
}
if (!Daemon::instance()->setPrimary(config.m_ifname, config)) {
logger.error() << "setPrimary failed";
QJsonObject reply;
reply.insert("type", "primaryFailed");
reply.insert("ifname", config.m_ifname);
write(reply);
return;
}
QJsonObject reply;
reply.insert("type", "primaryReady");
reply.insert("ifname", config.m_ifname);
write(reply);
return;
}
@@ -146,10 +173,19 @@ void DaemonLocalServerConnection::parseCommand(const QByteArray& data) {
logger.warning() << "Invalid command:" << type;
}
void DaemonLocalServerConnection::connected(const QString& pubkey) {
void DaemonLocalServerConnection::onTunnelConnected(const QString& ifname,
const QString& pubkey) {
QJsonObject obj;
obj.insert("type", "connected");
obj.insert("pubkey", QJsonValue(pubkey));
obj.insert("ifname", ifname);
obj.insert("pubkey", pubkey);
write(obj);
}
void DaemonLocalServerConnection::onTunnelHandshakeFailed(const QString& ifname) {
QJsonObject obj;
obj.insert("type", "disconnected");
obj.insert("ifname", ifname);
write(obj);
}
+3 -1
View File
@@ -6,6 +6,7 @@
#define DAEMONLOCALSERVERCONNECTION_H
#include <QObject>
#include <QString>
#include "daemonerrors.h"
@@ -23,7 +24,8 @@ class DaemonLocalServerConnection final : public QObject {
void parseCommand(const QByteArray& json);
void connected(const QString& pubkey);
void onTunnelConnected(const QString& ifname, const QString& pubkey);
void onTunnelHandshakeFailed(const QString& ifname);
void disconnected();
void backendFailure(DaemonError err);
+2
View File
@@ -13,6 +13,8 @@
class QJsonObject;
constexpr int ACTIVATION_TIMEOUT_MSEC = 30000;
class InterfaceConfig {
Q_GADGET
+1 -3
View File
@@ -14,8 +14,6 @@
#include "interfaceconfig.h"
constexpr const char* WG_INTERFACE = "amn0";
constexpr uint16_t WG_KEEPALIVE_PERIOD = 60;
class WireguardUtils : public QObject {
@@ -35,7 +33,7 @@ class WireguardUtils : public QObject {
virtual ~WireguardUtils() = default;
virtual bool interfaceExists() = 0;
virtual QString interfaceName() { return WG_INTERFACE; }
virtual QString interfaceName() = 0;
virtual bool addInterface(const InterfaceConfig& config) = 0;
virtual bool deleteInterface() = 0;
@@ -29,7 +29,6 @@ LinuxDaemon::LinuxDaemon() : Daemon(nullptr) {
logger.debug() << "Daemon created";
m_wgutils = new WireguardUtilsLinux(this);
m_dnsutils = new DnsUtilsLinux(this);
m_iputils = new IPUtilsLinux(this);
@@ -12,8 +12,6 @@
#include "wireguardutilslinux.h"
class LinuxDaemon final : public Daemon {
friend class IPUtilsMacos;
public:
LinuxDaemon();
~LinuxDaemon();
@@ -23,7 +21,6 @@ class LinuxDaemon final : public Daemon {
bool deactivate(bool emitSignals = true) override;
protected:
WireguardUtils* wgutils() const override { return m_wgutils; }
DnsUtils* dnsutils() override { return m_dnsutils; }
bool supportIPUtils() const override { return true; }
IPUtils* iputils() override { return m_iputils; }
@@ -32,13 +29,7 @@ class LinuxDaemon final : public Daemon {
return new WireguardUtilsLinux(this);
}
void replaceActiveWgUtils(WireguardUtils* newUtils) override {
delete m_wgutils;
m_wgutils = static_cast<WireguardUtilsLinux*>(newUtils);
}
private:
WireguardUtilsLinux* m_wgutils = nullptr;
DnsUtilsLinux* m_dnsutils = nullptr;
IPUtilsLinux* m_iputils = nullptr;
};
@@ -193,8 +193,8 @@ 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 amn+ -d %1 -p udp --dport 53 -j ACCEPT").arg(server);
result << QStringLiteral("-o amn+ -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);
result << QStringLiteral("-o tun2+ -d %1 -p udp --dport 53 -j ACCEPT").arg(server);
@@ -278,7 +278,7 @@ void LinuxFirewall::install()
});
installAnchor(Both, QStringLiteral("200.allowVPN"), {
QStringLiteral("-o amn0+ -j ACCEPT"),
QStringLiteral("-o amn+ -j ACCEPT"),
QStringLiteral("-o tun0+ -j ACCEPT"),
QStringLiteral("-o tun2+ -j ACCEPT"),
});
@@ -37,33 +37,6 @@
#include <QString>
#include <QStringList>
// Descriptor for a set of firewall rules to be appled.
//
struct FirewallParams
{
QStringList dnsServers;
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 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:
@@ -39,8 +39,6 @@ typedef struct wg_allowedip {
struct wg_allowedip *next_allowedip;
} wg_allowedip;
constexpr const char* WG_INTERFACE = "amn0";
static void nlmsg_append_attr(struct nlmsghdr* nlmsg, size_t maxlen,
int attrtype, const void* attrdata,
size_t attrlen);
@@ -14,7 +14,6 @@
#include <QTimer>
#include <QThread>
#include "linuxfirewall.h"
#include "leakdetector.h"
#include "logger.h"
@@ -63,7 +62,7 @@ bool WireguardUtilsLinux::addInterface(const InterfaceConfig& config) {
return false;
}
const QString ifname = config.m_ifname.isEmpty() ? QString(WG_INTERFACE) : config.m_ifname;
const QString ifname = config.m_ifname;
QDir wgRuntimeDir(WG_RUNTIME_DIR);
if (!wgRuntimeDir.exists()) {
@@ -147,29 +146,6 @@ bool WireguardUtilsLinux::addInterface(const InterfaceConfig& config) {
int err = uapiErrno(uapiCommand(message));
if (err != 0) {
logger.error() << "Interface configuration failed:" << strerror(err);
} else {
if (config.m_killSwitchEnabled) {
FirewallParams params { };
params.dnsServers.append(config.m_primaryDnsServer);
if (!config.m_secondaryDnsServer.isEmpty()) {
params.dnsServers.append(config.m_secondaryDnsServer);
}
if (config.m_allowedIPAddressRanges.contains(IPAddress("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);
@@ -454,27 +430,3 @@ QString WireguardUtilsLinux::waitForTunnelName(const QString& filename) {
return QString();
}
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);
}
@@ -11,7 +11,6 @@
#include "daemon/wireguardutils.h"
#include "linuxroutemonitor.h"
#include "linuxfirewall.h"
class WireguardUtilsLinux final : public WireguardUtils {
@@ -40,7 +39,6 @@ public:
bool excludeLocalNetworks(const QList<IPAddress>& lanAddressRanges) override;
void applyFirewallRules(FirewallParams& params);
signals:
void backendFailure();
+18 -6
View File
@@ -39,8 +39,12 @@ bool IPUtilsMacos::addInterfaceIPs(const InterfaceConfig& config) {
}
bool IPUtilsMacos::setMTUAndUp(const InterfaceConfig& config) {
Q_UNUSED(config);
QString ifname = MacOSDaemon::instance()->m_wgutils->interfaceName();
WireguardUtils* wg = MacOSDaemon::instance()->wgutilsFor(config.m_ifname);
if (!wg) {
logger.error() << "No wireguard interface for" << config.m_ifname;
return false;
}
QString ifname = wg->interfaceName();
struct ifreq ifr;
// Create socket file descriptor to perform the ioctl operations on
@@ -80,8 +84,12 @@ bool IPUtilsMacos::setMTUAndUp(const InterfaceConfig& config) {
}
bool IPUtilsMacos::addIP4AddressToDevice(const InterfaceConfig& config) {
Q_UNUSED(config);
QString ifname = MacOSDaemon::instance()->m_wgutils->interfaceName();
WireguardUtils* wg = MacOSDaemon::instance()->wgutilsFor(config.m_ifname);
if (!wg) {
logger.error() << "No wireguard interface for" << config.m_ifname;
return false;
}
QString ifname = wg->interfaceName();
struct ifaliasreq ifr;
struct sockaddr_in* ifrAddr = (struct sockaddr_in*)&ifr.ifra_addr;
struct sockaddr_in* ifrMask = (struct sockaddr_in*)&ifr.ifra_mask;
@@ -130,8 +138,12 @@ bool IPUtilsMacos::addIP4AddressToDevice(const InterfaceConfig& config) {
}
bool IPUtilsMacos::addIP6AddressToDevice(const InterfaceConfig& config) {
Q_UNUSED(config);
QString ifname = MacOSDaemon::instance()->m_wgutils->interfaceName();
WireguardUtils* wg = MacOSDaemon::instance()->wgutilsFor(config.m_ifname);
if (!wg) {
logger.error() << "No wireguard interface for" << config.m_ifname;
return false;
}
QString ifname = wg->interfaceName();
struct in6_aliasreq ifr6;
// Name the interface and set family
@@ -29,7 +29,6 @@ MacOSDaemon::MacOSDaemon() : Daemon(nullptr) {
logger.debug() << "Daemon created";
m_wgutils = new WireguardUtilsMacos(this);
m_dnsutils = new DnsUtilsMacos(this);
m_iputils = new IPUtilsMacos(this);
@@ -11,8 +11,6 @@
#include "wireguardutilsmacos.h"
class MacOSDaemon final : public Daemon {
friend class IPUtilsMacos;
public:
MacOSDaemon();
~MacOSDaemon();
@@ -22,7 +20,6 @@ class MacOSDaemon final : public Daemon {
bool deactivate(bool emitSignals = true) override;
protected:
WireguardUtils* wgutils() const override { return m_wgutils; }
DnsUtils* dnsutils() override { return m_dnsutils; }
bool supportIPUtils() const override { return true; }
IPUtils* iputils() override { return m_iputils; }
@@ -31,13 +28,7 @@ class MacOSDaemon final : public Daemon {
return new WireguardUtilsMacos(this);
}
void replaceActiveWgUtils(WireguardUtils* newUtils) override {
delete m_wgutils;
m_wgutils = static_cast<WireguardUtilsMacos*>(newUtils);
}
private:
WireguardUtilsMacos* m_wgutils = nullptr;
DnsUtilsMacos* m_dnsutils = nullptr;
IPUtilsMacos* m_iputils = nullptr;
};
@@ -36,35 +36,6 @@
#include <QString>
#include <QStringList>
// Descriptor for a set of firewall rules to be appled.
//
struct FirewallParams
{
QStringList dnsServers;
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)
};
class MacOSFirewall
{
@@ -62,7 +62,7 @@ bool WireguardUtilsMacos::addInterface(const InterfaceConfig& config) {
return false;
}
const QString ifname = config.m_ifname.isEmpty() ? QString(WG_INTERFACE) : config.m_ifname;
const QString ifname = config.m_ifname;
QDir wgRuntimeDir(WG_RUNTIME_DIR);
if (!wgRuntimeDir.exists()) {
@@ -146,30 +146,6 @@ bool WireguardUtilsMacos::addInterface(const InterfaceConfig& config) {
int err = uapiErrno(uapiCommand(message));
if (err != 0) {
logger.error() << "Interface configuration failed:" << strerror(err);
} else {
if (config.m_killSwitchEnabled) {
FirewallParams params { };
params.dnsServers.append(config.m_primaryDnsServer);
if (!config.m_secondaryDnsServer.isEmpty()) {
params.dnsServers.append(config.m_secondaryDnsServer);
}
if (config.m_allowedIPAddressRanges.contains(IPAddress("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);
}
@@ -455,28 +431,3 @@ QString WireguardUtilsMacos::waitForTunnelName(const QString& filename) {
return QString();
}
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);
}
@@ -10,7 +10,6 @@
#include "daemon/wireguardutils.h"
#include "macosroutemonitor.h"
#include "macosfirewall.h"
class WireguardUtilsMacos final : public WireguardUtils {
Q_OBJECT
@@ -38,8 +37,6 @@ class WireguardUtilsMacos final : public WireguardUtils {
bool excludeLocalNetworks(const QList<IPAddress>& lanAddressRanges) override;
void applyFirewallRules(FirewallParams& params);
signals:
void backendFailure();
@@ -35,14 +35,8 @@ WindowsDaemon::WindowsDaemon() : Daemon(nullptr) {
m_firewallManager = WindowsFirewall::create(this);
Q_ASSERT(m_firewallManager != nullptr);
m_wgutils = WireguardUtilsWindows::create(m_firewallManager, this);
m_dnsutils = new DnsUtilsWindows(this);
m_splitTunnelManager = WindowsSplitTunnel::create(m_firewallManager);
connect(m_wgutils.get(), &WireguardUtilsWindows::backendFailure, this,
&WindowsDaemon::monitorBackendFailure);
connect(this, &WindowsDaemon::activationFailure,
[this]() { m_firewallManager->disableKillSwitch(); });
}
WindowsDaemon::~WindowsDaemon() {
@@ -120,7 +114,3 @@ WireguardUtils* WindowsDaemon::createWgUtils() {
&WindowsDaemon::monitorBackendFailure);
return utils.release();
}
void WindowsDaemon::replaceActiveWgUtils(WireguardUtils* newUtils) {
m_wgutils.reset(static_cast<WireguardUtilsWindows*>(newUtils));
}
@@ -28,10 +28,8 @@ class WindowsDaemon final : public Daemon {
protected:
bool run(Op op, const InterfaceConfig& config) override;
WireguardUtils* wgutils() const override { return m_wgutils.get(); }
DnsUtils* dnsutils() override { return m_dnsutils; }
WireguardUtils* createWgUtils() override;
void replaceActiveWgUtils(WireguardUtils* newUtils) override;
private:
void monitorBackendFailure();
@@ -44,7 +42,6 @@ class WindowsDaemon final : public Daemon {
int m_inetAdapterIndex = -1;
std::unique_ptr<WireguardUtilsWindows> m_wgutils;
DnsUtilsWindows* m_dnsutils = nullptr;
std::unique_ptr<WindowsSplitTunnel> m_splitTunnelManager;
QPointer<WindowsFirewall> m_firewallManager;