Compare commits

...

42 Commits

Author SHA1 Message Date
cd-amn cd382546ac fix: keep Xray catch-all routes during seamless switch on macOS 2026-06-19 16:22:19 +00:00
cd-amn 8c5b4781d5 fix: keep DNS and routing during seamless Xray switch on Linux 2026-06-18 19:45:12 +04:00
cd-amn 8da439f0b3 fix: pick physical uplink for Xray during seamless switch on macOS 2026-06-18 14:45:58 +04:00
cd-amn e9cd043b10 fix: keep DNS and routing during seamless Xray switch on macOS 2026-06-18 14:45:58 +04:00
cd-amn eda9ed8016 fix: allocate utun-pattern interface names on macOS 2026-06-18 14:45:58 +04:00
cd-amn 225b693ea4 fix: keep VPN routing on macOS when switching between WG/AWG tunnels 2026-06-18 14:45:58 +04:00
cd-amn 97b2de8cd1 refactor: TrafficGuard owns adapter IP swap for WG/AWG and Xray 2026-06-18 14:45:58 +04:00
cd-amn ef1ed7064a feat: TrafficGuard owns Xray adapter IP swap on Windows 2026-06-18 14:45:58 +04:00
cd-amn 40ba31f54b fix: dedup RouterLinux route tracking and tolerate EEXIST/ESRCH 2026-06-18 14:45:58 +04:00
cd-amn 7bb609866a fix: close traffic leak during seamless tunnel switch 2026-06-18 14:45:58 +04:00
cd-amn 54d28862f3 refactor: TrafficGuard owns xray DNS and uplink routes 2026-06-18 14:45:58 +04:00
cd-amn bff3e228fc feat: route Xray through Tunnel for seamless server switch 2026-06-18 14:45:58 +04:00
cd-amn d528a241d8 feat: key xray worker per tunnel via ifname-scoped IPC 2026-06-18 14:45:58 +04:00
cd-amn 99e6c18f15 feat: run xray-core in a forked worker process 2026-06-18 14:45:58 +04:00
cd-amn 6d49a9416e fix: propagate tun2socks FailedToStart in XrayProtocol 2026-06-18 14:45:58 +04:00
cd-amn 80fa788802 refactor: prepare XrayProtocol for Tunnel two-phase lifecycle 2026-06-18 14:45:58 +04:00
cd-amn 3590b2d323 feat: seamless WG switch on Windows for shared client IPs 2026-06-18 14:45:58 +04:00
cd-amn 9a1e380ffb feat: per-tunnel Windows firewall for seamless WG switch 2026-06-18 14:45:58 +04:00
cd-amn eb42ce8fef refactor: route Windows WG killswitch through TrafficGuard 2026-06-18 14:45:58 +04:00
cd-amn 72147d3a67 feat: per-tunnel ifname for WG service, UAPI pipe, and SCM for Windows 2026-06-18 14:45:58 +04:00
cd-amn 39f9bcfd50 fix: revert API country selection on server switch failure 2026-06-18 14:45:58 +04:00
cd-amn b6188baeb8 fix: traffic drops for killswitch blacklisted sites during the switch 2026-06-18 14:45:58 +04:00
cd-amn 9b329ad5b1 refactor: move routing/KS/DNS lifecycle from Daemon to TrafficGuard 2026-06-18 14:45:58 +04:00
cd-amn ce05b4e99c refactor: internalize AMNEZIA_DESKTOP guard in appendKillSwitchConfig 2026-06-18 14:45:58 +04:00
cd-amn 75f522e9dc chore: remove unused VpnConnection::m_routeMode field 2026-06-18 14:45:58 +04:00
cd-amn d6349b5734 fix: remove duplicate routeAddList in setupRoutes 2026-06-18 14:45:58 +04:00
cd-amn fa8014093b feat: drive WG via Tunnel coordinator for seamless server switch 2026-06-18 14:45:57 +04:00
cd-amn 9d69ab89d5 feat: introduce Tunnel wrapping VpnProtocol with two-phase lifecycle 2026-06-18 14:33:36 +04:00
cd-amn f2ff8a7b3b refactor: thread interface name through LocalSocketController 2026-06-18 14:33:36 +04:00
cd-amn 864b8c6f8a feat: split daemon activation into bare bring-up and setPrimary 2026-06-18 14:33:36 +04:00
cd-amn adb8eb4937 refactor: own killswitch teardown at daemon level 2026-06-18 14:33:36 +04:00
cd-amn 6750afd330 refactor: cache physical gateway and use kernel-assigned netlink pid 2026-06-18 14:33:36 +04:00
cd-amn 7ad0692306 feat: enable base Daemon to create and swap WireguardUtils without knowing platform type 2026-06-18 14:33:36 +04:00
cd-amn 0dcd05c6c3 feat: use per-tunnel ifname instead of hardcoded WG_INTERFACE in platform helpers 2026-06-18 14:33:36 +04:00
cd-amn 83e82c16a7 feat: decouple TUN name from XrayProtocol to support dual tunnels 2026-06-18 14:33:36 +04:00
cd-amn f29b6cf027 feat: let callers specify WireGuard interface name per tunnel 2026-06-18 14:33:36 +04:00
cd-amn 4bca2df4a2 feat: revoke old endpoint from KS allowlist after server switch 2026-06-18 14:33:36 +04:00
cd-amn f67927667a feat: killswitch/routing teardown only on explicit protocol stop 2026-06-18 14:33:36 +04:00
cd-amn cc469e74ed fix: deleteRoutePrefix incorrectly called insertRoute for specific prefixes 2026-06-18 14:33:36 +04:00
NickVs2015 234c70f495 fix: NM down/up reconnection problem 2026-06-18 14:33:36 +04:00
cd-amn 850b698e83 fix: add missing Qt includes to updateController 2026-06-18 14:33:36 +04:00
cd-amn 42570c54f8 feat: decouple routing/killswitch from protocol handling 2026-06-18 14:33:35 +04:00
82 changed files with 2717 additions and 1213 deletions
+4
View File
@@ -65,6 +65,8 @@ set(HEADERS ${HEADERS}
${CLIENT_ROOT_DIR}/core/utils/utilities.h
${CLIENT_ROOT_DIR}/core/utils/managementServer.h
${CLIENT_ROOT_DIR}/core/utils/constants.h
${CLIENT_ROOT_DIR}/core/vpnTrafficGuard.h
${CLIENT_ROOT_DIR}/core/tunnel.h
)
# Mozilla headres
@@ -145,6 +147,8 @@ set(SOURCES ${SOURCES}
${CLIENT_ROOT_DIR}/core/utils/osSignalHandler.cpp
${CLIENT_ROOT_DIR}/core/utils/utilities.cpp
${CLIENT_ROOT_DIR}/core/utils/managementServer.cpp
${CLIENT_ROOT_DIR}/core/vpnTrafficGuard.cpp
${CLIENT_ROOT_DIR}/core/tunnel.cpp
)
# Mozilla sources
@@ -484,6 +484,12 @@ ErrorCode SubscriptionController::updateServiceFromGateway(const QString &server
return ErrorCode::NoError;
}
void SubscriptionController::restoreApiV2Config(const QString &serverId, const ApiV2ServerConfig &config)
{
const QJsonObject json = config.toJson();
m_serversRepository->editServer(serverId, json, serverConfigUtils::configTypeFromJson(json));
}
ErrorCode SubscriptionController::deactivateDevice(const QString &serverId)
{
auto apiV2 = m_serversRepository->apiV2Config(serverId);
@@ -68,6 +68,8 @@ public:
ErrorCode updateServiceFromGateway(const QString &serverId, const QString &newCountryCode, bool isConnectEvent);
void restoreApiV2Config(const QString &serverId, const ApiV2ServerConfig &config);
ErrorCode deactivateDevice(const QString &serverId);
ErrorCode deactivateExternalDevice(const QString &serverId, const QString &uuid, const QString &serverCountryCode);
@@ -28,6 +28,7 @@ ConnectionController::ConnectionController(SecureServersRepository* serversRepos
m_vpnConnection(vpnConnection)
{
connect(m_vpnConnection, &VpnConnection::connectionStateChanged, this, &ConnectionController::connectionStateChanged);
connect(m_vpnConnection, &VpnConnection::serverSwitchFailed, this, &ConnectionController::serverSwitchFailed);
connect(this, &ConnectionController::openConnectionRequested, m_vpnConnection, &VpnConnection::connectToVpn, Qt::QueuedConnection);
connect(this, &ConnectionController::closeConnectionRequested, m_vpnConnection, &VpnConnection::disconnectFromVpn, Qt::QueuedConnection);
connect(this, &ConnectionController::setConnectionStateRequested, m_vpnConnection, &VpnConnection::setConnectionState, Qt::QueuedConnection);
@@ -69,6 +69,7 @@ signals:
void closeConnectionRequested();
void setConnectionStateRequested(Vpn::ConnectionState state);
void killSwitchModeChangedRequested(bool enabled);
void serverSwitchFailed();
#ifdef Q_OS_ANDROID
void restoreConnectionRequested();
@@ -94,6 +94,12 @@ void CoreSignalHandlers::initErrorMessagesHandler()
m_coreController->m_connectionController->setConnectionState(Vpn::ConnectionState::Disconnected);
});
connect(m_coreController->m_connectionUiController, &ConnectionUiController::serverSwitchFailed, this, [this]() {
m_coreController->m_subscriptionUiController->revertLastCountryChange();
emit m_coreController->m_pageController->showNotificationMessage(
tr("Failed to switch server. Existing connection maintained."));
});
connect(m_coreController->m_subscriptionUiController, &SubscriptionUiController::errorOccurred, m_coreController->m_pageController,
qOverload<ErrorCode>(&PageController::showErrorMessage));
@@ -79,7 +79,7 @@ GatewayController::EncryptedRequestData GatewayController::prepareRequest(const
QString ip = NetworkUtilities::getIPAddress(host);
if (!ip.isEmpty()) {
IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
QRemoteObjectPendingReply<bool> reply = iface->addKillSwitchAllowedRange(QStringList { ip });
QRemoteObjectPendingReply<bool> reply = iface->addKillSwitchAllowedRange(QString(), QStringList { ip });
if (!reply.waitForFinished(1000) || !reply.returnValue())
qWarning() << "GatewayController::prepareRequest(): Failed to execute remote addKillSwitchAllowedRange call";
});
@@ -7,6 +7,8 @@
#include <QJsonObject>
#include <QSysInfo>
#include <QTimer>
#include <QStandardPaths>
#include <QTemporaryDir>
#include "amneziaApplication.h"
#include "logger.h"
+2 -54
View File
@@ -48,15 +48,6 @@ void OpenVpnProtocol::cleanupResources()
QThread::msleep(10);
}
m_managementServer.stop();
#if defined(Q_OS_WIN) || defined(Q_OS_LINUX) || defined(Q_OS_MACOS)
IpcClient::withInterface([](QSharedPointer<IpcInterfaceReplica> iface) {
QRemoteObjectPendingReply<bool> reply = iface->disableKillSwitch();
if (!reply.waitForFinished(1000) && !reply.returnValue()) {
qWarning() << "OpenVpnProtocol::cleanupResources(): Failed to disable killswitch";
}
});
#endif
}
void OpenVpnProtocol::stop()
@@ -186,20 +177,6 @@ ErrorCode OpenVpnProtocol::start()
return lastError();
}
#ifdef AMNEZIA_DESKTOP
const ErrorCode res = IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
QString ip = NetworkUtilities::getIPAddress(m_configData.value(amnezia::configKey::hostName).toString());
QRemoteObjectPendingReply<bool> reply = iface->addKillSwitchAllowedRange(QStringList(ip));
if (!reply.waitForFinished(1000) || !reply.returnValue()) {
return ErrorCode::AmneziaServiceConnectionFailed;
}
return ErrorCode::NoError;
});
if (res != ErrorCode::NoError) {
return res;
}
#endif
// Detect default gateway
#ifdef Q_OS_MAC
QProcess p;
@@ -354,38 +331,9 @@ void OpenVpnProtocol::updateVpnGateway(const QString &line)
m_vpnGateway = l.split(" ").at(2);
#ifdef Q_OS_WIN
QThread::msleep(300);
IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
QList<QNetworkInterface> netInterfaces = QNetworkInterface::allInterfaces();
for (int i = 0; i < netInterfaces.size(); i++) {
for (int j=0; j < netInterfaces.at(i).addressEntries().size(); j++)
{
// killSwitch toggle
if (m_vpnLocalAddress == netInterfaces.at(i).addressEntries().at(j).ip().toString()) {
if (QVariant(m_configData.value(configKey::killSwitchOption).toString()).toBool()) {
iface->enableKillSwitch(m_configData, netInterfaces.at(i).index());
}
m_configData.insert("vpnAdapterIndex", netInterfaces.at(i).index());
m_configData.insert("vpnGateway", m_vpnGateway);
m_configData.insert("vpnServer",
NetworkUtilities::getIPAddress(m_configData.value(amnezia::configKey::hostName).toString()));
iface->enablePeerTraffic(m_configData);
}
}
}
});
#endif
#if defined(Q_OS_LINUX) || defined(Q_OS_MACOS)
// killSwitch toggle
if (QVariant(m_configData.value(configKey::killSwitchOption).toString()).toBool()) {
m_configData.insert("vpnServer",
NetworkUtilities::getIPAddress(m_configData.value(amnezia::configKey::hostName).toString()));
IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
QRemoteObjectPendingReply<bool> reply = iface->enableKillSwitch(m_configData, 0);
if (!reply.waitForFinished(1000) || !reply.returnValue()) {
qWarning() << "OpenVpnProtocol::updateVpnGateway(): Failed to enable killswitch";
}
});
}
#if defined(Q_OS_WIN) || defined(Q_OS_LINUX) || defined(Q_OS_MACOS)
emit tunnelAddressesUpdated(m_vpnGateway, m_vpnLocalAddress);
#endif
qDebug() << QString("Set vpn local address %1, gw %2").arg(m_vpnLocalAddress).arg(vpnGateway());
}
+14
View File
@@ -106,6 +106,19 @@ QString VpnProtocol::vpnLocalAddress() const
return m_vpnLocalAddress;
}
bool VpnProtocol::isWireGuardBased(amnezia::DockerContainer container)
{
return container == amnezia::DockerContainer::Awg
|| container == amnezia::DockerContainer::Awg2
|| container == amnezia::DockerContainer::WireGuard;
}
bool VpnProtocol::isXrayBased(amnezia::DockerContainer container)
{
return container == amnezia::DockerContainer::Xray
|| container == amnezia::DockerContainer::SSXray;
}
VpnProtocol *VpnProtocol::factory(DockerContainer container, const QJsonObject &configuration)
{
switch (container) {
@@ -137,6 +150,7 @@ QString VpnProtocol::textConnectionState(Vpn::ConnectionState connectionState)
case Vpn::ConnectionState::Preparing: return tr("Preparing");
case Vpn::ConnectionState::Connecting: return tr("Connecting...");
case Vpn::ConnectionState::Connected: return tr("Connected");
case Vpn::ConnectionState::Switching: return tr("Switching...");
case Vpn::ConnectionState::Disconnecting: return tr("Disconnecting...");
case Vpn::ConnectionState::Reconnecting: return tr("Reconnecting...");
case Vpn::ConnectionState::Error: return tr("Error");
+6
View File
@@ -27,6 +27,7 @@ namespace Vpn
Preparing,
Connecting,
Connected,
Switching,
Disconnecting,
Reconnecting,
Error
@@ -60,6 +61,7 @@ public:
virtual bool isDisconnected() const;
virtual ErrorCode start() = 0;
virtual void stop() = 0;
virtual void setPrimary(const QJsonObject& config) { Q_UNUSED(config) }
Vpn::ConnectionState connectionState() const;
ErrorCode lastError() const;
@@ -71,6 +73,8 @@ public:
QString vpnLocalAddress() const;
static VpnProtocol* factory(amnezia::DockerContainer container, const QJsonObject &configuration);
static bool isWireGuardBased(amnezia::DockerContainer container);
static bool isXrayBased(amnezia::DockerContainer container);
signals:
void bytesChanged(quint64 receivedBytes, quint64 sentBytes);
@@ -78,6 +82,8 @@ signals:
void timeoutTimerEvent();
void protocolError(amnezia::ErrorCode e);
void tunnelAddressesUpdated(const QString& gateway, const QString& localAddress);
void primaryReady();
void primaryFailed();
public slots:
virtual void onTimeout(); // todo: remove?
+19 -16
View File
@@ -12,9 +12,11 @@
WireguardProtocol::WireguardProtocol(const QJsonObject &configuration, QObject *parent)
: VpnProtocol(configuration, parent)
{
m_impl.reset(new LocalSocketController());
const QString ifname = configuration.value("ifname").toString();
m_impl.reset(new LocalSocketController(ifname));
connect(m_impl.get(), &ControllerImpl::connected, this,
[this](const QString &pubkey, const QDateTime &connectionTimestamp) {
[this](const QString& pubkey, const QDateTime&) {
Q_UNUSED(pubkey)
setConnectionState(Vpn::ConnectionState::Connected);
});
connect(m_impl.get(), &ControllerImpl::statusUpdated, this,
@@ -33,12 +35,18 @@ WireguardProtocol::WireguardProtocol(const QJsonObject &configuration, QObject *
if ((!m_vpnGateway.isEmpty() && m_vpnGateway != previousGateway) ||
(!m_vpnLocalAddress.isEmpty() && m_vpnLocalAddress != previousLocal)) {
emit tunnelAddressesUpdated(m_vpnGateway, m_vpnLocalAddress);
}
if (m_connectionState == Vpn::ConnectionState::Connected) {
emit tunnelAddressesUpdated(m_vpnGateway, m_vpnLocalAddress);
}
}
});
connect(m_impl.get(), &ControllerImpl::disconnected, this,
[this]() { setConnectionState(Vpn::ConnectionState::Disconnected); });
connect(m_impl.get(), &ControllerImpl::primaryReady,
this, &WireguardProtocol::primaryReady);
connect(m_impl.get(), &ControllerImpl::primaryFailed,
this, &WireguardProtocol::primaryFailed);
m_impl->initialize(nullptr, nullptr);
}
@@ -48,13 +56,7 @@ WireguardProtocol::~WireguardProtocol()
QThread::msleep(200);
}
void WireguardProtocol::stop()
{
stopMzImpl();
return;
}
ErrorCode WireguardProtocol::startMzImpl()
ErrorCode WireguardProtocol::start()
{
QString protocolName = m_rawConfig.value("protocol").toString();
QJsonObject vpnConfigData = m_rawConfig.value(protocolName + "_config_data").toObject();
@@ -62,18 +64,19 @@ ErrorCode WireguardProtocol::startMzImpl()
m_rawConfig.insert(protocolName + "_config_data", vpnConfigData);
m_rawConfig[configKey::hostName] = NetworkUtilities::getIPAddress(m_rawConfig[configKey::hostName].toString());
m_stopped = false;
m_impl->activate(m_rawConfig);
return ErrorCode::NoError;
}
ErrorCode WireguardProtocol::stopMzImpl()
void WireguardProtocol::stop()
{
if (m_stopped) return;
m_stopped = true;
m_impl->deactivate();
return ErrorCode::NoError;
}
ErrorCode WireguardProtocol::start()
void WireguardProtocol::setPrimary(const QJsonObject& config)
{
return startMzImpl();
m_impl->setPrimary(config);
}
+2 -3
View File
@@ -21,11 +21,10 @@ public:
ErrorCode start() override;
void stop() override;
ErrorCode startMzImpl();
ErrorCode stopMzImpl();
void setPrimary(const QJsonObject& config) override;
private:
bool m_stopped = false;
QScopedPointer<ControllerImpl> m_impl;
};
+53 -97
View File
@@ -19,12 +19,6 @@
#include <exception>
#ifdef Q_OS_MACOS
static const QString tunName = "utun22";
#else
static const QString tunName = "tun2";
#endif
XrayProtocol::XrayProtocol(const QJsonObject &configuration, QObject *parent) : VpnProtocol(configuration, parent)
{
m_vpnGateway = amnezia::protocols::xray::defaultLocalAddr;
@@ -34,11 +28,16 @@ XrayProtocol::XrayProtocol(const QJsonObject &configuration, QObject *parent) :
m_routeMode = static_cast<amnezia::RouteMode>(configuration.value(amnezia::configKey::splitTunnelType).toInt());
m_remoteAddress = NetworkUtilities::getIPAddress(m_rawConfig.value(amnezia::configKey::hostName).toString());
const QString primaryDns = configuration.value(amnezia::configKey::dns1).toString();
m_dnsServers.push_back(QHostAddress(primaryDns));
if (primaryDns != amnezia::protocols::dns::amneziaDnsIp) {
const QString secondaryDns = configuration.value(amnezia::configKey::dns2).toString();
m_dnsServers.push_back(QHostAddress(secondaryDns));
m_tunName = configuration.value("tunName").toString();
if (m_tunName.isEmpty()) {
m_tunName = configuration.value("ifname").toString();
}
if (m_tunName.isEmpty()) {
#ifdef Q_OS_MACOS
m_tunName = QStringLiteral("utun22");
#else
m_tunName = QStringLiteral("tun2");
#endif
}
QJsonObject xrayConfiguration = configuration.value(ProtocolUtils::key_proto_config_data(Proto::Xray)).toObject();
@@ -68,6 +67,8 @@ ErrorCode XrayProtocol::start()
{
qDebug() << "XrayProtocol::start()";
m_phase = Phase::Active;
// Inject SOCKS5 auth into the inbound before starting xray.
// Re-uses existing credentials if the config already has them (e.g. imported config).
amnezia::serialization::inbounds::InboundCredentials creds;
@@ -106,7 +107,7 @@ ErrorCode XrayProtocol::start()
return IpcClient::withInterface(
[&](QSharedPointer<IpcInterfaceReplica> iface) {
auto xrayStart = iface->xrayStart(xrayConfigStr);
auto xrayStart = iface->xrayStart(m_tunName, xrayConfigStr);
if (!xrayStart.waitForFinished() || !xrayStart.returnValue()) {
qCritical() << "Failed to start xray";
return ErrorCode::XrayExecutableCrashed;
@@ -120,24 +121,17 @@ void XrayProtocol::stop()
{
qDebug() << "XrayProtocol::stop()";
IpcClient::withInterface([](QSharedPointer<IpcInterfaceReplica> iface) {
auto disableKillSwitch = iface->disableKillSwitch();
if (!disableKillSwitch.waitForFinished() || !disableKillSwitch.returnValue())
qWarning() << "Failed to disable killswitch";
if (m_phase != Phase::Active) {
return;
}
m_phase = Phase::Stopping;
auto StartRoutingIpv6 = iface->StartRoutingIpv6();
if (!StartRoutingIpv6.waitForFinished() || !StartRoutingIpv6.returnValue())
qWarning() << "Failed to start routing ipv6";
auto restoreResolvers = iface->restoreResolvers();
if (!restoreResolvers.waitForFinished() || !restoreResolvers.returnValue())
qWarning() << "Failed to restore resolvers";
auto deleteTun = iface->deleteTun(tunName);
IpcClient::withInterface([this](QSharedPointer<IpcInterfaceReplica> iface) {
auto deleteTun = iface->deleteTun(m_tunName);
if (!deleteTun.waitForFinished() || !deleteTun.returnValue())
qWarning() << "Failed to delete tun";
auto xrayStop = iface->xrayStop();
auto xrayStop = iface->xrayStop(m_tunName);
if (!xrayStop.waitForFinished() || !xrayStop.returnValue())
qWarning() << "Failed to stop xray";
});
@@ -162,9 +156,18 @@ void XrayProtocol::stop()
m_tun2socksProcess.reset();
}
m_phase = Phase::Inactive;
setConnectionState(Vpn::ConnectionState::Disconnected);
}
void XrayProtocol::setPrimary(const QJsonObject &config)
{
Q_UNUSED(config)
QMetaObject::invokeMethod(this, [this]() {
emit primaryReady();
}, Qt::QueuedConnection);
}
ErrorCode XrayProtocol::startTun2Socks()
{
m_tun2socksProcess = IpcClient::CreatePrivilegedProcess();
@@ -175,10 +178,23 @@ ErrorCode XrayProtocol::startTun2Socks()
const QString proxyUrl = QString("socks5://%1:%2@127.0.0.1:%3").arg(m_socksUser, m_socksPassword, QString::number(m_socksPort));
m_tun2socksProcess->setProgram(PermittedProcess::Tun2Socks);
m_tun2socksProcess->setArguments({ "-device", QString("tun://%1").arg(tunName), "-proxy", proxyUrl });
m_tun2socksProcess->setArguments({ "-device", QString("tun://%1").arg(m_tunName), "-proxy", proxyUrl });
connect(
m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::readyReadStandardError, this,
m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::errorOccurred, this,
[this](QProcess::ProcessError error) {
if (error != QProcess::FailedToStart) {
// Other errors are reported via the finished signal or are transient.
return;
}
qCritical() << "Tun2socks failed to start";
stop();
setLastError(ErrorCode::Tun2SockExecutableMissing);
},
Qt::QueuedConnection);
connect(
m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::readyReadStandardError, this,
[this]() {
auto readAllStandardError = m_tun2socksProcess->readAllStandardError();
if (!readAllStandardError.waitForFinished()) {
@@ -217,13 +233,17 @@ ErrorCode XrayProtocol::startTun2Socks()
}
}
if (resourceBusy && m_tun2socksRetryCount < maxTun2SocksRetries) {
if (m_phase == Phase::Active && resourceBusy
&& m_tun2socksRetryCount < maxTun2SocksRetries) {
m_tun2socksRetryCount++;
qWarning() << QString("Tun2socks: TUN resource busy, retrying (%1/%2) in %3ms...")
.arg(m_tun2socksRetryCount)
.arg(maxTun2SocksRetries)
.arg(tun2socksRetryDelayMs);
QTimer::singleShot(tun2socksRetryDelayMs, this, [this]() {
if (m_phase != Phase::Active) {
return;
}
if (ErrorCode err = startTun2Socks(); err != ErrorCode::NoError) {
stop();
setLastError(err);
@@ -252,81 +272,17 @@ ErrorCode XrayProtocol::setupRouting()
{
return IpcClient::withInterface(
[this](QSharedPointer<IpcInterfaceReplica> iface) -> ErrorCode {
#ifdef Q_OS_WIN
const int inetAdapterIndex = NetworkUtilities::AdapterIndexTo(QHostAddress(m_remoteAddress));
#endif
auto createTun = iface->createTun(tunName, amnezia::protocols::xray::defaultLocalAddr);
#ifndef Q_OS_WIN
auto createTun = iface->createTun(m_tunName, amnezia::protocols::xray::defaultLocalAddr);
if (!createTun.waitForFinished() || !createTun.returnValue()) {
qCritical() << "Failed to assign IP address for TUN";
return ErrorCode::InternalError;
}
auto updateResolvers = iface->updateResolvers(tunName, m_dnsServers);
if (!updateResolvers.waitForFinished() || !updateResolvers.returnValue()) {
qCritical() << "Failed to set DNS resolvers for TUN";
return ErrorCode::InternalError;
}
#ifdef Q_OS_WIN
int vpnAdapterIndex = -1;
QList<QNetworkInterface> netInterfaces = QNetworkInterface::allInterfaces();
for (auto &netInterface : netInterfaces) {
for (auto &address : netInterface.addressEntries()) {
if (m_vpnLocalAddress == address.ip().toString())
vpnAdapterIndex = netInterface.index();
}
}
#else
static const int vpnAdapterIndex = 0;
Q_UNUSED(iface)
#endif
const bool killSwitchEnabled = QVariant(m_rawConfig.value(configKey::killSwitchOption).toString()).toBool();
if (killSwitchEnabled) {
if (vpnAdapterIndex != -1) {
QJsonObject config = m_rawConfig;
config.insert("vpnServer", m_remoteAddress);
auto enableKillSwitch = IpcClient::Interface()->enableKillSwitch(config, vpnAdapterIndex);
if (!enableKillSwitch.waitForFinished() || !enableKillSwitch.returnValue()) {
qCritical() << "Failed to enable killswitch";
return ErrorCode::InternalError;
}
} else
qWarning() << "Failed to get vpnAdapterIndex. Killswitch disabled";
}
if (m_routeMode == amnezia::RouteMode::VpnAllSites) {
static const QStringList subnets = { "1.0.0.0/8", "2.0.0.0/7", "4.0.0.0/6", "8.0.0.0/5",
"16.0.0.0/4", "32.0.0.0/3", "64.0.0.0/2", "128.0.0.0/1" };
auto routeAddList = iface->routeAddList(m_vpnGateway, subnets);
if (!routeAddList.waitForFinished() || routeAddList.returnValue() != subnets.count()) {
qCritical() << "Failed to set routes for TUN";
return ErrorCode::InternalError;
}
}
auto StopRoutingIpv6 = iface->StopRoutingIpv6();
if (!StopRoutingIpv6.waitForFinished() || !StopRoutingIpv6.returnValue()) {
qCritical() << "Failed to disable IPv6 routing";
return ErrorCode::InternalError;
}
#ifdef Q_OS_WIN
if (inetAdapterIndex != -1 && vpnAdapterIndex != -1) {
QJsonObject config = m_rawConfig;
config.insert("inetAdapterIndex", inetAdapterIndex);
config.insert("vpnAdapterIndex", vpnAdapterIndex);
config.insert("vpnGateway", m_vpnGateway);
config.insert("vpnServer", m_remoteAddress);
auto enablePeerTraffic = iface->enablePeerTraffic(config);
if (!enablePeerTraffic.waitForFinished() || !enablePeerTraffic.returnValue()) {
qCritical() << "Failed to enable peer traffic";
return ErrorCode::InternalError;
}
} else
qWarning() << "Failed to get adapter indexes. Split-tunneling disabled";
#endif
emit tunnelAddressesUpdated(m_vpnGateway, m_vpnLocalAddress);
return ErrorCode::NoError;
},
[]() { return ErrorCode::AmneziaServiceConnectionFailed; });
+10 -1
View File
@@ -20,14 +20,20 @@ public:
ErrorCode start() override;
void stop() override;
void setPrimary(const QJsonObject &config) override;
private:
enum class Phase {
Inactive,
Active,
Stopping,
};
ErrorCode setupRouting();
ErrorCode startTun2Socks();
QJsonObject m_xrayConfig;
amnezia::RouteMode m_routeMode;
QList<QHostAddress> m_dnsServers;
QString m_remoteAddress;
QString m_socksUser;
@@ -38,6 +44,9 @@ private:
int m_tun2socksRetryCount = 0;
static constexpr int maxTun2SocksRetries = 5;
static constexpr int tun2socksRetryDelayMs = 400;
QString m_tunName;
Phase m_phase = Phase::Inactive;
};
#endif // XRAYPROTOCOL_H
+150
View File
@@ -0,0 +1,150 @@
#include "tunnel.h"
#include <QTimer>
#include "daemon/interfaceconfig.h"
Tunnel::Tunnel(QString ifname,
amnezia::DockerContainer container,
QJsonObject config,
QString remoteAddress,
QObject* parent)
: QObject(parent),
m_ifname(std::move(ifname)),
m_remoteAddress(std::move(remoteAddress)),
m_container(container),
m_config(std::move(config)) {}
Tunnel::~Tunnel() = default;
void Tunnel::prepare() {
if (m_state != State::Idle) {
return;
}
setState(State::Preparing);
m_config.insert("ifname", m_ifname);
m_protocol.reset(VpnProtocol::factory(m_container, m_config));
if (!m_protocol) {
setState(State::Failed);
emit failed(amnezia::ErrorCode::InternalError);
return;
}
connect(m_protocol.data(), &VpnProtocol::connectionStateChanged,
this, &Tunnel::onProtocolStateChanged);
connect(m_protocol.data(), &VpnProtocol::bytesChanged,
this, &Tunnel::bytesChanged);
connect(m_protocol.data(), &VpnProtocol::tunnelAddressesUpdated,
this, &Tunnel::addressesUpdated);
connect(m_protocol.data(), &VpnProtocol::primaryReady,
this, &Tunnel::onPrimaryReady);
connect(m_protocol.data(), &VpnProtocol::primaryFailed,
this, &Tunnel::onPrimaryFailed);
startActivationDeadline(ACTIVATION_TIMEOUT_MSEC);
const amnezia::ErrorCode err = m_protocol->start();
if (err != amnezia::ErrorCode::NoError) {
cancelActivationDeadline();
setState(State::Failed);
emit failed(err);
}
}
void Tunnel::commit() {
if (m_state != State::Prepared) {
return;
}
setState(State::Committing);
startActivationDeadline(ACTIVATION_TIMEOUT_MSEC);
if (m_protocol) {
m_protocol->setPrimary(m_config);
}
}
void Tunnel::onPrimaryReady() {
if (m_state != State::Committing) {
return;
}
cancelActivationDeadline();
setState(State::Active);
emit activated();
}
void Tunnel::onPrimaryFailed() {
if (m_state != State::Committing) {
return;
}
cancelActivationDeadline();
setState(State::Failed);
emit failed(m_protocol ? m_protocol->lastError() : amnezia::ErrorCode::InternalError);
}
void Tunnel::deactivate() {
if (m_state == State::Gone || m_state == State::Idle) {
return;
}
cancelActivationDeadline();
setState(State::Gone);
if (m_protocol) {
m_protocol->stop();
}
}
void Tunnel::restart() {
deactivate();
setState(State::Idle);
prepare();
}
void Tunnel::setState(State next) {
if (m_state == next) {
return;
}
m_state = next;
emit stateChanged(m_state);
}
void Tunnel::startActivationDeadline(int msec) {
if (!m_deadline) {
m_deadline = new QTimer(this);
m_deadline->setSingleShot(true);
connect(m_deadline, &QTimer::timeout, this, [this]() {
if (m_state != State::Preparing && m_state != State::Committing) {
return;
}
setState(State::Failed);
emit failed(amnezia::ErrorCode::InternalError);
});
}
m_deadline->start(msec);
}
void Tunnel::cancelActivationDeadline() {
if (m_deadline) {
m_deadline->stop();
}
}
void Tunnel::onProtocolStateChanged(Vpn::ConnectionState state) {
if (m_state == State::Preparing && state == Vpn::ConnectionState::Connected) {
cancelActivationDeadline();
setState(State::Prepared);
emit prepared();
return;
}
const bool inLiveState = m_state == State::Preparing
|| m_state == State::Prepared
|| m_state == State::Committing
|| m_state == State::Active;
const bool isFailureSignal = state == Vpn::ConnectionState::Disconnected
|| state == Vpn::ConnectionState::Error;
if (inLiveState && isFailureSignal) {
cancelActivationDeadline();
setState(State::Failed);
emit failed(m_protocol ? m_protocol->lastError() : amnezia::ErrorCode::InternalError);
}
}
+82
View File
@@ -0,0 +1,82 @@
#ifndef TUNNEL_H
#define TUNNEL_H
#include <QJsonObject>
#include <QObject>
#include <QSharedPointer>
#include <QString>
#include "core/protocols/vpnProtocol.h"
#include "core/utils/containerEnum.h"
#include "core/utils/errorCodes.h"
class QTimer;
class Tunnel : public QObject {
Q_OBJECT
public:
enum class State {
Idle,
Preparing,
Prepared,
Committing,
Active,
Gone,
Failed,
};
Q_ENUM(State)
Tunnel(QString ifname,
amnezia::DockerContainer container,
QJsonObject config,
QString remoteAddress,
QObject* parent = nullptr);
~Tunnel() override;
const QString& ifname() const { return m_ifname; }
const QString& remoteAddress() const { return m_remoteAddress; }
amnezia::DockerContainer container() const { return m_container; }
const QJsonObject& config() const { return m_config; }
State state() const { return m_state; }
QSharedPointer<VpnProtocol> protocol() const { return m_protocol; }
const QString& handoverIfname() const { return m_handoverIfname; }
void setHandoverIfname(const QString& ifname) { m_handoverIfname = ifname; }
void clearHandoverIfname() { m_handoverIfname.clear(); }
virtual void prepare();
virtual void commit();
virtual void deactivate();
virtual void restart();
signals:
void prepared();
void activated();
void failed(amnezia::ErrorCode);
void stateChanged(Tunnel::State);
void bytesChanged(quint64 rxBytes, quint64 txBytes);
void addressesUpdated(const QString& gateway, const QString& localAddress);
protected:
void setState(State);
void startActivationDeadline(int msec);
void cancelActivationDeadline();
QString m_ifname;
QString m_remoteAddress;
QString m_handoverIfname;
amnezia::DockerContainer m_container;
QJsonObject m_config;
QSharedPointer<VpnProtocol> m_protocol;
private:
void onProtocolStateChanged(Vpn::ConnectionState state);
void onPrimaryReady();
void onPrimaryFailed();
State m_state = State::Idle;
QTimer* m_deadline = nullptr;
};
#endif // TUNNEL_H
+24 -7
View File
@@ -286,7 +286,7 @@ QPair<QString, QNetworkInterface> NetworkUtilities::getGatewayAndIface()
return { resGateway, QNetworkInterface::interfaceFromIndex(resIndex) };
#endif
#ifdef Q_OS_LINUX
constexpr int BUFFER_SIZE = 100;
constexpr int BUFFER_SIZE = 8192;
int received_bytes = 0, msg_len = 0, route_attribute_len = 0;
int sock = -1, msgseq = 0;
struct nlmsghdr *nlh, *nlmsg;
@@ -294,7 +294,7 @@ QPair<QString, QNetworkInterface> NetworkUtilities::getGatewayAndIface()
// This struct contain route attributes (route type)
struct rtattr *route_attribute;
char gateway_address[INET_ADDRSTRLEN], interface[IF_NAMESIZE];
char msgbuf[BUFFER_SIZE], buffer[BUFFER_SIZE];
char msgbuf[100], buffer[BUFFER_SIZE];
char *ptr = buffer;
struct timeval tv;
@@ -339,8 +339,8 @@ QPair<QString, QNetworkInterface> NetworkUtilities::getGatewayAndIface()
nlh = (struct nlmsghdr *) ptr;
/* Check if the header is valid */
if((NLMSG_OK(nlmsg, received_bytes) == 0) ||
(nlmsg->nlmsg_type == NLMSG_ERROR))
if((NLMSG_OK(nlh, received_bytes) == 0) ||
(nlh->nlmsg_type == NLMSG_ERROR))
{
perror("Error in received packet");
return {};
@@ -355,13 +355,15 @@ QPair<QString, QNetworkInterface> NetworkUtilities::getGatewayAndIface()
}
/* Break if its not a multi part message */
if ((nlmsg->nlmsg_flags & NLM_F_MULTI) == 0)
if ((nlh->nlmsg_flags & NLM_F_MULTI) == 0)
break;
}
while ((nlmsg->nlmsg_seq != msgseq) || (nlmsg->nlmsg_pid != getpid()));
while ((nlh->nlmsg_seq != msgseq) || (nlh->nlmsg_pid != getpid()));
/* parse response */
for ( ; NLMSG_OK(nlh, received_bytes); nlh = NLMSG_NEXT(nlh, received_bytes))
int remaining = msg_len + received_bytes;
nlh = (struct nlmsghdr *) buffer;
for ( ; NLMSG_OK(nlh, remaining); nlh = NLMSG_NEXT(nlh, remaining))
{
/* Get the route data */
route_entry = (struct rtmsg *) NLMSG_DATA(nlh);
@@ -370,6 +372,10 @@ QPair<QString, QNetworkInterface> NetworkUtilities::getGatewayAndIface()
if (route_entry->rtm_table != RT_TABLE_MAIN)
continue;
/* Reset per-route to avoid cross-route state pollution */
memset(gateway_address, 0, sizeof(gateway_address));
memset(interface, 0, sizeof(interface));
route_attribute = (struct rtattr *) RTM_RTA(route_entry);
route_attribute_len = RTM_PAYLOAD(nlh);
@@ -391,10 +397,15 @@ QPair<QString, QNetworkInterface> NetworkUtilities::getGatewayAndIface()
}
if ((*gateway_address) && (*interface)) {
if (QString::fromUtf8(interface).startsWith("amn")) {
continue;
}
qDebug() << "Gateway " << gateway_address << " for interface " << interface;
break;
}
}
if (!(*gateway_address) || !(*interface))
qDebug() << "getGatewayAndIface: no gateway found";
close(sock);
return { gateway_address, QNetworkInterface::interfaceFromName(interface) };
#endif
@@ -449,10 +460,15 @@ QPair<QString, QNetworkInterface> NetworkUtilities::getGatewayAndIface()
sa_tab[RTAX_DST]->sa_family == afinet_type[ip_type] &&
sa_tab[RTAX_GATEWAY]->sa_family == afinet_type[ip_type])
{
char ifname[IF_NAMESIZE] = {0};
const bool isTun = if_indextoname(rt->rtm_index, ifname)
&& QString::fromUtf8(ifname).startsWith("utun");
if (afinet_type[ip_type] == AF_INET)
{
if ((reinterpret_cast<struct sockaddr_in*>(sa_tab[RTAX_DST]))->sin_addr.s_addr == 0)
{
if (isTun) continue;
char dstStr4[INET_ADDRSTRLEN];
char srcStr4[INET_ADDRSTRLEN];
memcpy(srcStr4,
@@ -470,6 +486,7 @@ QPair<QString, QNetworkInterface> NetworkUtilities::getGatewayAndIface()
{
if ((reinterpret_cast<struct sockaddr_in*>(sa_tab[RTAX_DST]))->sin_addr.s_addr == 0)
{
if (isTun) continue;
char dstStr6[INET6_ADDRSTRLEN];
char srcStr6[INET6_ADDRSTRLEN];
memcpy(srcStr6,
+569
View File
@@ -0,0 +1,569 @@
#include "vpnTrafficGuard.h"
#include <QDebug>
#include <QEventLoop>
#include <QHostInfo>
#include <QNetworkInterface>
#include <QPointer>
#include <QStringList>
#include <QTimer>
#include <QJsonObject>
#ifdef AMNEZIA_DESKTOP
#include "core/utils/ipcClient.h"
#endif
#include "core/utils/networkUtilities.h"
#include "core/utils/constants/protocolConstants.h"
#include "core/tunnel.h"
#include "mozilla/localsocketcontroller.h"
VpnTrafficGuard::VpnTrafficGuard(SecureAppSettingsRepository* appSettings, QObject *parent)
: QObject(parent), m_appSettingsRepository(appSettings)
{
}
VpnTrafficGuard::~VpnTrafficGuard()
{
}
void VpnTrafficGuard::setConfig(const QJsonObject &config)
{
m_config = config;
}
bool VpnTrafficGuard::allowEndpoint(const QString &remoteAddress, const QString &ifname)
{
#ifdef AMNEZIA_DESKTOP
if (remoteAddress.isEmpty()) {
return false;
}
if (m_allowedEndpoints.contains(remoteAddress)) {
return true;
}
m_allowedEndpoints.append(remoteAddress);
return IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
QRemoteObjectPendingReply<bool> reply = iface->addKillSwitchAllowedRange(ifname, QStringList(remoteAddress));
return reply.waitForFinished(1000) && reply.returnValue();
});
#else
Q_UNUSED(remoteAddress)
Q_UNUSED(ifname)
return true;
#endif
}
void VpnTrafficGuard::setupRoutes(const QJsonObject &vpnConfiguration, const QSharedPointer<VpnProtocol> &protocol, const QString &remoteAddress)
{
#ifdef AMNEZIA_DESKTOP
if (!m_appSettingsRepository) {
qCritical() << "VpnTrafficGuard::setupRoutes: repositories not initialized";
return;
}
IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
iface->resetIpStack();
auto flushDns = iface->flushDns();
if (flushDns.waitForFinished() && flushDns.returnValue())
qDebug() << "VpnTrafficGuard::setupRoutes: Successfully flushed DNS";
else
qWarning() << "VpnTrafficGuard::setupRoutes: Failed to flush DNS";
const QString proto = vpnConfiguration.value(configKey::vpnProto).toString();
const bool isWgBased = (proto == ProtocolUtils::protoToString(Proto::Awg) ||
proto == ProtocolUtils::protoToString(Proto::WireGuard));
if (!isWgBased) {
if (!protocol) {
qWarning() << "VpnTrafficGuard::setupRoutes: protocol is null";
return;
}
QString dns1 = vpnConfiguration.value(configKey::dns1).toString();
QString dns2 = vpnConfiguration.value(configKey::dns2).toString();
const QString xrayIfname = vpnConfiguration.value("ifname").toString();
#ifdef Q_OS_MACOS
if (!m_appSettingsRepository->isSitesSplitTunnelingEnabled() || m_appSettingsRepository->routeMode() != amnezia::RouteMode::VpnAllExceptSites) {
iface->routeAddListVia(xrayIfname, protocol->vpnGateway(), QStringList() << dns1 << dns2);
}
#else
iface->routeAddListVia(xrayIfname, protocol->vpnGateway(), QStringList() << dns1 << dns2);
#endif
if (m_appSettingsRepository->isSitesSplitTunnelingEnabled()) {
iface->routeDeleteList(protocol->vpnGateway(), QStringList() << "0.0.0.0");
if (m_appSettingsRepository->routeMode() == amnezia::route_mode_ns::VpnOnlyForwardSites) {
QPointer<VpnProtocol> protocolPtr(protocol.data());
QTimer::singleShot(1000, protocol.data(),
[this, protocolPtr]() {
if (!protocolPtr) {
return;
}
addSplitTunnelRoutes(protocolPtr->vpnGateway(), m_appSettingsRepository->routeMode());
});
} else if (m_appSettingsRepository->routeMode() == amnezia::route_mode_ns::VpnAllExceptSites) {
iface->routeAddListVia(xrayIfname, protocol->vpnGateway(), QStringList() << "0.0.0.0/1" << "128.0.0.0/1");
iface->routeAddList(protocol->routeGateway(), QStringList() << remoteAddress);
#ifdef Q_OS_MACOS
iface->routeAddList(protocol->routeGateway(), QStringList() << dns1 << dns2);
#endif
addSplitTunnelRoutes(protocol->routeGateway(), m_appSettingsRepository->routeMode());
}
}
}
});
#endif
}
void VpnTrafficGuard::addSplitTunnelRoutes(const QString &gw, amnezia::RouteMode mode)
{
#ifdef AMNEZIA_DESKTOP
if (!m_appSettingsRepository) {
qCritical() << "VpnTrafficGuard::addSplitTunnelRoutes: repositories not initialized";
return;
}
QStringList ips;
QStringList sites;
const QVariantMap &m = m_appSettingsRepository->vpnSites(mode);
for (auto i = m.constBegin(); i != m.constEnd(); ++i) {
if (NetworkUtilities::checkIpSubnetFormat(i.key())) {
ips.append(i.key());
} else {
if (NetworkUtilities::checkIpSubnetFormat(i.value().toString())) {
ips.append(i.value().toString());
}
sites.append(i.key());
}
}
ips.removeDuplicates();
IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
iface->routeAddList(gw, ips);
});
for (const QString &site : sites) {
const auto &cbResolv = [this, site, gw, mode, ips](const QHostInfo &hostInfo) {
for (const QHostAddress &addr : hostInfo.addresses()) {
if (addr.protocol() == QAbstractSocket::NetworkLayerProtocol::IPv4Protocol) {
const QString &ip = addr.toString();
if (!ips.contains(ip)) {
IpcClient::withInterface([gw, ip](QSharedPointer<IpcInterfaceReplica> iface) {
iface->routeAddList(gw, QStringList() << ip);
});
m_appSettingsRepository->addVpnSite(mode, site, ip);
}
IpcClient::withInterface([](QSharedPointer<IpcInterfaceReplica> iface) {
auto reply = iface->flushDns();
if (!reply.waitForFinished() || !reply.returnValue())
qWarning() << "VpnTrafficGuard::addSplitTunnelRoutes: Failed to flush DNS";
});
break;
}
}
};
QHostInfo::lookupHost(site, this, cbResolv);
}
#endif
}
void VpnTrafficGuard::finishFirewallHandover(Tunnel* tunnel)
{
#if defined(AMNEZIA_DESKTOP) && defined(Q_OS_WIN)
if (!tunnel) return;
const QString handoverIfname = tunnel->handoverIfname();
if (handoverIfname.isEmpty() || handoverIfname == tunnel->ifname()) {
tunnel->clearHandoverIfname();
return;
}
IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
iface->disableKillSwitchForTunnel(handoverIfname, QStringList());
});
tunnel->clearHandoverIfname();
#else
Q_UNUSED(tunnel)
#endif
}
void VpnTrafficGuard::applyKillSwitch(Tunnel* tunnel, const QString &gateway, const QString &localAddress)
{
#ifdef AMNEZIA_DESKTOP
finishFirewallHandover(tunnel);
QJsonObject updatedConfig = m_config;
IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
#ifdef Q_OS_WIN
const QString ifname = updatedConfig.value("ifname").toString();
if (!ifname.isEmpty()) {
updatedConfig.insert("vpnGateway", gateway);
updatedConfig.insert("vpnServer", NetworkUtilities::getIPAddress(updatedConfig.value(configKey::hostName).toString()));
if (QVariant(updatedConfig.value(configKey::killSwitchOption).toString()).toBool()) {
iface->enableKillSwitch(updatedConfig, 0);
}
iface->enablePeerTraffic(updatedConfig);
} else {
QList<QNetworkInterface> netInterfaces = QNetworkInterface::allInterfaces();
for (int i = 0; i < netInterfaces.size(); i++) {
for (int j=0; j < netInterfaces.at(i).addressEntries().size(); j++)
{
if (localAddress == netInterfaces.at(i).addressEntries().at(j).ip().toString()) {
updatedConfig.insert("vpnAdapterIndex", netInterfaces.at(i).index());
updatedConfig.insert("vpnGateway", gateway);
updatedConfig.insert("vpnServer", NetworkUtilities::getIPAddress(updatedConfig.value(configKey::hostName).toString()));
if (QVariant(updatedConfig.value(configKey::killSwitchOption).toString()).toBool()) {
iface->enableKillSwitch(updatedConfig, netInterfaces.at(i).index());
}
iface->enablePeerTraffic(updatedConfig);
}
}
}
}
#endif
#if defined(Q_OS_LINUX) || defined(Q_OS_MACOS)
if (QVariant(updatedConfig.value(configKey::killSwitchOption).toString()).toBool()) {
updatedConfig.insert("vpnServer",
NetworkUtilities::getIPAddress(updatedConfig.value(amnezia::configKey::hostName).toString()));
QRemoteObjectPendingReply<bool> reply = iface->enableKillSwitch(updatedConfig, 0);
//TODO: why it takes so long?
if (!reply.waitForFinished(1000) || !reply.returnValue()) {
qWarning() << "VpnTrafficGuard::applyKillSwitch: Failed to enable killswitch";
} else {
qDebug() << "VpnTrafficGuard::applyKillSwitch: Successfully enabled killswitch";
}
}
#endif
const QString proto = updatedConfig.value(configKey::vpnProto).toString();
const bool isXrayBased = (proto == ProtocolUtils::protoToString(Proto::Xray) ||
proto == ProtocolUtils::protoToString(Proto::SSXray));
if (isXrayBased) {
if (updatedConfig.value(configKey::splitTunnelType).toInt() == amnezia::route_mode_ns::VpnAllSites) {
static const QStringList subnets = { "1.0.0.0/8", "2.0.0.0/7", "4.0.0.0/6", "8.0.0.0/5", "16.0.0.0/4", "32.0.0.0/3", "64.0.0.0/2", "128.0.0.0/1" };
const QString xrayIfname = tunnel ? tunnel->ifname() : QString();
auto routeAddList = iface->routeAddListVia(xrayIfname, gateway, subnets);
if (!routeAddList.waitForFinished() || routeAddList.returnValue() != subnets.count()) {
qCritical() << "Failed to set routes for TUN";
}
}
auto StopRoutingIpv6 = iface->StopRoutingIpv6();
if (!StopRoutingIpv6.waitForFinished() || !StopRoutingIpv6.returnValue()) {
qCritical() << "Failed to disable IPv6 routing";
} else {
m_ipv6RoutingStopped = true;
}
}
});
#endif
}
void VpnTrafficGuard::flushAll()
{
#ifdef AMNEZIA_DESKTOP
IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
iface->restoreTunnelResolvers();
QRemoteObjectPendingReply<bool> reply = iface->disableKillSwitch();
m_allowedEndpoints.clear();
//TODO: why it takes so long?
if (!reply.waitForFinished(1000) || !reply.returnValue()) {
qWarning() << "VpnTrafficGuard::flushAll: Failed to disable killswitch";
} else {
qDebug() << "VpnTrafficGuard::flushAll: Successfully disabled killswitch";
}
auto flushDns = iface->flushDns();
if (flushDns.waitForFinished() && flushDns.returnValue())
qDebug() << "VpnTrafficGuard::flushAll: Successfully flushed DNS";
else
qWarning() << "VpnTrafficGuard::flushAll: Failed to flush DNS";
auto clearSavedRoutes = iface->clearSavedRoutes();
if (clearSavedRoutes.waitForFinished() && clearSavedRoutes.returnValue())
qDebug() << "VpnTrafficGuard::flushAll: Successfully cleared saved routes";
else
qWarning() << "VpnTrafficGuard::flushAll: Failed to clear saved routes";
if (m_ipv6RoutingStopped) {
auto StartRoutingIpv6 = iface->StartRoutingIpv6();
if (!StartRoutingIpv6.waitForFinished() || !StartRoutingIpv6.returnValue()) {
qCritical() << "Failed to enable IPv6 routing";
} else {
m_ipv6RoutingStopped = false;
}
}
});
#endif
}
namespace {
QStringList allowedIpPrefixesFor(const QJsonObject& activateJson)
{
QStringList prefixes;
const QJsonArray ranges = activateJson.value("allowedIPAddressRanges").toArray();
for (const QJsonValue& v : ranges) {
const QJsonObject r = v.toObject();
const QString addr = r.value("address").toString();
if (addr.isEmpty()) continue;
prefixes.append(QStringLiteral("%1/%2").arg(addr).arg(r.value("range").toInt()));
}
return prefixes;
}
QStringList excludedAddressesFor(const QJsonObject& activateJson)
{
QStringList addrs;
const QJsonArray excluded = activateJson.value("excludedAddresses").toArray();
for (const QJsonValue& v : excluded) {
const QString s = v.toString();
if (!s.isEmpty()) addrs.append(s);
}
return addrs;
}
QStringList resolversFor(const QJsonObject& activateJson)
{
QStringList dns;
const QString primary = activateJson.value("primaryDnsServer").toString();
if (!primary.isEmpty()) dns.append(primary);
const QString secondary = activateJson.value("secondaryDnsServer").toString();
if (!secondary.isEmpty()) dns.append(secondary);
return dns;
}
}
void VpnTrafficGuard::reserve(Tunnel* tunnel)
{
if (!tunnel) return;
#ifdef AMNEZIA_DESKTOP
allowEndpoint(tunnel->remoteAddress(), tunnel->ifname());
#else
Q_UNUSED(tunnel)
#endif
}
void VpnTrafficGuard::release(Tunnel* tunnel)
{
if (!tunnel) return;
disconnect(tunnel, nullptr, this, nullptr);
#ifdef AMNEZIA_DESKTOP
m_allowedEndpoints.removeAll(tunnel->remoteAddress());
IpcClient::withInterface([this, &tunnel](QSharedPointer<IpcInterfaceReplica> iface) {
iface->disableKillSwitchForTunnel(tunnel->ifname(), m_allowedEndpoints);
});
#else
Q_UNUSED(tunnel)
#endif
}
void VpnTrafficGuard::applyPolicy(Tunnel* tunnel)
{
if (!tunnel) return;
#ifdef AMNEZIA_DESKTOP
const QString ifname = tunnel->ifname();
if (VpnProtocol::isXrayBased(tunnel->container())) {
const QJsonObject cfg = tunnel->config();
const QString primary = cfg.value(amnezia::configKey::dns1).toString();
const QString secondary = cfg.value(amnezia::configKey::dns2).toString();
QList<QHostAddress> dns;
if (!primary.isEmpty()) dns.append(QHostAddress(primary));
if (!secondary.isEmpty() && secondary != primary) dns.append(QHostAddress(secondary));
IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
auto updateRes = iface->updateResolvers(ifname, dns);
if (!updateRes.waitForFinished() || !updateRes.returnValue()) {
qWarning() << "VpnTrafficGuard::applyPolicy: updateResolvers failed for" << ifname;
}
#ifdef Q_OS_MAC
const auto gw = NetworkUtilities::getGatewayAndIface();
const QString uplinkIface = gw.second.name();
const QString uplinkGateway = gw.first;
if (!uplinkIface.isEmpty() && !uplinkGateway.isEmpty()) {
auto add = iface->xrayAddUplinkRoutes(uplinkIface, uplinkGateway);
if (!add.waitForFinished() || !add.returnValue()) {
qWarning() << "VpnTrafficGuard::applyPolicy: xrayAddUplinkRoutes failed on" << uplinkIface;
}
}
#endif
});
return;
}
const QJsonObject activate = LocalSocketController::buildActivateJson(tunnel->config(), tunnel->ifname());
const QStringList prefixes = allowedIpPrefixesFor(activate);
const QStringList excluded = excludedAddressesFor(activate);
const QStringList dns = resolversFor(activate);
const QString peer = tunnel->remoteAddress();
IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
if (!peer.isEmpty()) iface->addExclusionRoute(ifname, peer);
for (const QString& addr : excluded) {
iface->addExclusionRoute(ifname, addr);
}
for (const QString& prefix : prefixes) {
iface->addAllowedIp(ifname, prefix);
}
iface->setTunnelResolvers(ifname, dns);
iface->flushDns();
});
#else
Q_UNUSED(tunnel)
#endif
}
void VpnTrafficGuard::revokePolicy(Tunnel* tunnel)
{
if (!tunnel) return;
#ifdef AMNEZIA_DESKTOP
const QString ifname = tunnel->ifname();
if (VpnProtocol::isXrayBased(tunnel->container())) {
IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
iface->restoreResolvers();
#ifdef Q_OS_MAC
const auto gw = NetworkUtilities::getGatewayAndIface();
const QString uplinkIface = gw.second.name();
const QString uplinkGateway = gw.first;
if (!uplinkIface.isEmpty()) {
iface->xrayRemoveUplinkRoutes(uplinkIface, uplinkGateway);
}
#endif
});
return;
}
const QJsonObject activate = LocalSocketController::buildActivateJson(tunnel->config(), tunnel->ifname());
const QStringList prefixes = allowedIpPrefixesFor(activate);
const QStringList excluded = excludedAddressesFor(activate);
const QString peer = tunnel->remoteAddress();
IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
for (const QString& prefix : prefixes) {
iface->delAllowedIp(ifname, prefix);
}
for (const QString& addr : excluded) {
iface->delExclusionRoute(ifname, addr);
}
if (!peer.isEmpty()) iface->delExclusionRoute(ifname, peer);
});
#else
Q_UNUSED(tunnel)
#endif
}
void VpnTrafficGuard::bringUp(Tunnel* tunnel)
{
if (!tunnel) return;
reserve(tunnel);
tunnel->prepare();
}
void VpnTrafficGuard::commit(Tunnel* tunnel)
{
if (!tunnel) return;
#ifdef Q_OS_WIN
const QString ipv4 = tunnel->config().value(QStringLiteral("deviceIpv4Address")).toString();
const QString ipv6 = tunnel->config().value(QStringLiteral("deviceIpv6Address")).toString();
if (!ipv4.isEmpty() || !ipv6.isEmpty()) {
IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
auto ap = iface->applyAdapterAddress(tunnel->ifname(), ipv4, ipv6);
if (!ap.waitForFinished(15000) || !ap.returnValue()) {
qWarning() << "VpnTrafficGuard::commit: applyAdapterAddress failed for"
<< tunnel->ifname();
}
});
}
#endif
applyPolicy(tunnel);
connect(tunnel, &Tunnel::activated, this, [this, tunnel] {
if (auto p = tunnel->protocol()) {
applyKillSwitch(tunnel, p->vpnGateway(), p->vpnLocalAddress());
}
});
#ifdef Q_OS_WIN
connect(tunnel, &Tunnel::addressesUpdated, this,
[this, tunnel](const QString& gw, const QString& la) {
applyKillSwitch(tunnel, gw, la);
});
#endif
tunnel->commit();
}
void VpnTrafficGuard::tearDown(Tunnel* tunnel)
{
if (!tunnel) return;
revokePolicy(tunnel);
release(tunnel);
tunnel->deactivate();
}
void VpnTrafficGuard::swap(Tunnel* from, Tunnel* to)
{
if (!to) return;
if (from) {
to->setHandoverIfname(from->ifname());
}
#ifdef Q_OS_WIN
if (from) {
const QString fromIpv4 = from->config().value(QStringLiteral("deviceIpv4Address")).toString();
const QString fromIpv6 = from->config().value(QStringLiteral("deviceIpv6Address")).toString();
if (!fromIpv4.isEmpty() || !fromIpv6.isEmpty()) {
IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
auto rm = iface->removeAdapterAddress(from->ifname(), fromIpv4, fromIpv6);
if (!rm.waitForFinished(2000) || !rm.returnValue()) {
qWarning() << "VpnTrafficGuard::swap: removeAdapterAddress failed for"
<< from->ifname();
}
});
}
}
#endif
QEventLoop loop;
QTimer guard;
guard.setSingleShot(true);
const auto activatedConn = connect(to, &Tunnel::activated, &loop, &QEventLoop::quit);
const auto failedConn = connect(to, &Tunnel::failed, &loop, [&loop](amnezia::ErrorCode) { loop.quit(); });
const auto timeoutConn = connect(&guard, &QTimer::timeout, &loop, [&loop]() {
qWarning() << "VpnTrafficGuard::swap: timed out waiting for new tunnel activation";
loop.quit();
});
commit(to);
guard.start(5000);
loop.exec();
guard.stop();
disconnect(activatedConn);
disconnect(failedConn);
disconnect(timeoutConn);
// Service IPCs are processed in order on a single connection. Wait on a trailing
// sync request so the killswitch enable from the activation handlers is fully
// applied before we deactivate the previous tunnel.
IpcClient::withInterface([](QSharedPointer<IpcInterfaceReplica> iface) {
auto reply = iface->flushDns();
if (!reply.waitForFinished(5000) || !reply.returnValue()) {
qWarning() << "VpnTrafficGuard::swap: trailing sync IPC timed out or failed";
}
});
if (from) {
m_allowedEndpoints.removeAll(from->remoteAddress());
#ifndef Q_OS_WIN
IpcClient::withInterface([this](QSharedPointer<IpcInterfaceReplica> iface) {
iface->resetKillSwitchAllowedRange(m_allowedEndpoints);
});
#endif
revokePolicy(from);
from->deactivate();
#ifdef Q_OS_MAC
if (VpnProtocol::isXrayBased(to->container())) {
if (auto p = to->protocol()) {
applyKillSwitch(to, p->vpnGateway(), p->vpnLocalAddress());
setupRoutes(to->config(), p, to->remoteAddress());
}
}
#endif
}
}
+45
View File
@@ -0,0 +1,45 @@
#ifndef VPNTRAFFICGUARD_H
#define VPNTRAFFICGUARD_H
#include <qobject.h>
#include "core/repositories/secureAppSettingsRepository.h"
#include "protocols/vpnProtocol.h"
class Tunnel;
class VpnTrafficGuard : public QObject
{
Q_OBJECT
public:
explicit VpnTrafficGuard(SecureAppSettingsRepository* appSettings, QObject* parent = nullptr);
~VpnTrafficGuard() override;
void setConfig(const QJsonObject &config);
void setupRoutes(const QJsonObject &vpnConfiguration,
const QSharedPointer<VpnProtocol> &protocol,
const QString &remoteAddress);
void flushAll();
bool allowEndpoint(const QString &remoteAddress, const QString &ifname = QString());
void applyKillSwitch(Tunnel* tunnel, const QString &vpnGateway, const QString &vpnLocalAddress);
void reserve(Tunnel* tunnel);
void release(Tunnel* tunnel);
void applyPolicy(Tunnel* tunnel);
void revokePolicy(Tunnel* tunnel);
void bringUp(Tunnel* tunnel);
void commit(Tunnel* tunnel);
void tearDown(Tunnel* tunnel);
void swap(Tunnel* from, Tunnel* to);
private:
void addSplitTunnelRoutes(const QString &gateway, amnezia::RouteMode mode);
void finishFirewallHandover(Tunnel* tunnel);
SecureAppSettingsRepository* m_appSettingsRepository;
QJsonObject m_config;
bool m_ipv6RoutingStopped = false;
QStringList m_allowedEndpoints;
};
#endif // VPNTRAFFICGUARD_H
+178 -216
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,38 @@ 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)) {
return false;
}
if (!dnsutils()->restoreResolvers()) {
return false;
}
if (!maybeUpdateResolvers(config)) {
return false;
}
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;
}
WireguardUtils* wg = m_tunnels.value(ifname);
if (!wg) {
wg = createWgUtils();
if (!wg) {
logger.error() << "Failed to create wireguard utils.";
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;
m_tunnels.insert(ifname, wg);
}
if (m_primaryIfname.isEmpty()) {
m_primaryIfname = ifname;
}
ConnectionState& cs = m_connections[ifname];
cs.m_config = config;
cs.m_date = QDateTime();
cs.m_deadline = QDateTime::currentDateTime().addMSecs(ACTIVATION_TIMEOUT_MSEC);
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()) {
InterfaceConfig bringupConfig = config;
bringupConfig.m_deferAddressSetup = (m_primaryIfname != ifname);
if (!wg->addInterface(bringupConfig)) {
logger.error() << "Interface creation failed.";
return false;
}
@@ -131,60 +103,71 @@ bool Daemon::activate(const InterfaceConfig& config) {
}
}
// Configure routing for excluded addresses.
for (const QString& i : config.m_excludedAddresses) {
addExclusionRoute(IPAddress(i));
}
// Add the peer to this interface.
if (!wgutils()->updatePeer(config)) {
if (!wg->updatePeer(config)) {
logger.error() << "Peer creation failed.";
return false;
}
if (!maybeUpdateResolvers(config)) {
if (!m_activationTimer.isActive()) {
m_activationTimer.start(HANDSHAKE_POLL_MSEC);
}
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;
});
if (!run(Up, config)) {
return false;
}
// set routing
for (const IPAddress& ip : config.m_allowedIPAddressRanges) {
if (!wgutils()->updateRoutePrefix(ip)) {
logger.debug() << "Routing configuration failed for" << ip.toString();
return false;
}
}
m_connections[ifname].m_config = config;
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();
return true;
}
return false;
failure_guard.dismiss();
return true;
}
bool Daemon::maybeUpdateResolvers(const InterfaceConfig& config) {
if ((config.m_hopType == InterfaceConfig::MultiHopExit) ||
(config.m_hopType == InterfaceConfig::SingleHop)) {
QList<QHostAddress> resolvers;
resolvers.append(QHostAddress(config.m_primaryDnsServer));
if (!config.m_secondaryDnsServer.isEmpty()) {
resolvers.append(QHostAddress(config.m_secondaryDnsServer));
}
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);
const bool isLastTunnel = wg && m_tunnels.size() == 1;
// If the DNS is not the Gateway, it's a user defined DNS
// thus, not add any other :)
if (config.m_primaryDnsServer == config.m_serverIpv4Gateway) {
resolvers.append(QHostAddress(config.m_serverIpv6Gateway));
}
if (!dnsutils()->updateResolvers(wgutils()->interfaceName(), resolvers)) {
return false;
if (wg) {
logger.debug() << "deactivateTunnel" << wg->interfaceName();
if (isLastTunnel) {
for (const IPAddress& prefix : m_excludedAddrSet.keys()) {
wg->deleteExclusionRoute(prefix);
}
m_excludedAddrSet.clear();
}
wg->deletePeer(config);
wg->deleteInterface();
m_tunnels.remove(ifname);
delete wg;
}
m_connections.remove(ifname);
if (wasPrimary) {
m_primaryIfname.clear();
}
return true;
}
@@ -209,26 +192,60 @@ bool Daemon::parseStringList(const QJsonObject& obj, const QString& name,
return true;
}
bool Daemon::addExclusionRoute(const IPAddress& prefix) {
bool Daemon::addExclusionRoute(const QString &ifname, const QString &addr) {
IPAddress prefix(addr);
if (m_excludedAddrSet.contains(prefix)) {
m_excludedAddrSet[prefix]++;
return true;
}
if (!wgutils()->addExclusionRoute(prefix)) {
WireguardUtils* wg = wgutilsFor(ifname);
if (!wg) wg = primaryWgutils();
if (!wg || !wg->addExclusionRoute(prefix)) {
return false;
}
m_excludedAddrSet[prefix] = 1;
return true;
}
bool Daemon::delExclusionRoute(const IPAddress& prefix) {
Q_ASSERT(m_excludedAddrSet.contains(prefix));
bool Daemon::delExclusionRoute(const QString &ifname, const QString &addr) {
IPAddress prefix(addr);
if (!m_excludedAddrSet.contains(prefix)) {
return false;
}
if (m_excludedAddrSet[prefix] > 1) {
m_excludedAddrSet[prefix]--;
return true;
}
m_excludedAddrSet.remove(prefix);
return wgutils()->deleteExclusionRoute(prefix);
WireguardUtils* wg = wgutilsFor(ifname);
if (!wg) wg = primaryWgutils();
return wg && wg->deleteExclusionRoute(prefix);
}
bool Daemon::addAllowedIp(const QString &ifname, const QString &prefix) {
WireguardUtils* wg = wgutilsFor(ifname);
return wg && wg->updateRoutePrefix(IPAddress(prefix));
}
bool Daemon::delAllowedIp(const QString &ifname, const QString &prefix) {
WireguardUtils* wg = wgutilsFor(ifname);
return wg && wg->deleteRoutePrefix(IPAddress(prefix));
}
bool Daemon::setTunnelResolvers(const QString &ifname, const QStringList &resolvers) {
WireguardUtils* wg = wgutilsFor(ifname);
if (!wg || !dnsutils()) {
return false;
}
QList<QHostAddress> hostAddrs;
for (const QString& r : resolvers) {
hostAddrs.append(QHostAddress(r));
}
return dnsutils()->updateResolvers(wg->interfaceName(), hostAddrs);
}
bool Daemon::restoreTunnelResolvers() {
return dnsutils() && dnsutils()->restoreResolvers();
}
// static
@@ -440,17 +457,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();
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;
}
}
@@ -459,31 +475,26 @@ 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);
}
// Cleanup routing for excluded addresses.
for (auto iterator = m_excludedAddrSet.constBegin();
iterator != m_excludedAddrSet.constEnd(); ++iterator) {
wgutils()->deleteExclusionRoute(iterator.key());
if (auto* wg = primaryWgutils()) {
for (const IPAddress& prefix : m_excludedAddrSet.keys()) {
wg->deleteExclusionRoute(prefix);
}
}
m_excludedAddrSet.clear();
m_connections.clear();
// Delete the interface
return wgutils()->deleteInterface();
if (m_tunnels.contains(primary)) {
deactivateTunnel(primary);
}
m_activationTimer.stop();
return true;
}
QString Daemon::logs() {
@@ -492,79 +503,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;
@@ -584,38 +534,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...";
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" << cs.m_config.m_serverPublicKey;
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()) {
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) {
cs.m_date.setMSecsSinceEpoch(status.m_handshake);
emit tunnelConnected(ifname, status.m_pubkey);
handshaked = true;
}
}
}
if (handshaked) {
continue;
}
logger.debug() << "awaiting" << config.m_serverPublicKey;
// Check if the handshake has completed.
for (const WireguardUtils::PeerStatus& status : peers) {
if (config.m_serverPublicKey != status.m_pubkey) {
continue;
}
if (status.m_handshake != 0) {
connection.m_date.setMSecsSinceEpoch(status.m_handshake);
emit connected(status.m_pubkey);
}
}
if (!connection.m_date.isValid()) {
pendingHandshakes++;
if (cs.m_deadline.isValid() && now > cs.m_deadline) {
timedOut.append(ifname);
} else {
anyPending = true;
}
}
// Check again if there were connections that haven't completed a handshake.
if (pendingHandshakes > 0) {
m_handshakeTimer.start(HANDSHAKE_POLL_MSEC);
for (const QString& ifname : timedOut) {
logger.warning() << "Tunnel handshake timed out:" << m_tunnels.value(ifname)->interfaceName();
emit tunnelHandshakeFailed(ifname);
deactivateTunnel(ifname);
}
if (!anyPending) {
m_activationTimer.stop();
}
}
+25 -18
View File
@@ -22,7 +22,6 @@ class Daemon : public QObject {
enum Op {
Up,
Down,
Switch,
};
explicit Daemon(QObject* parent);
@@ -32,10 +31,22 @@ 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();
bool addExclusionRoute(const QString &ifname, const QString &addr);
bool delExclusionRoute(const QString &ifname, const QString &addr);
bool addAllowedIp(const QString &ifname, const QString &prefix);
bool delAllowedIp(const QString &ifname, const QString &prefix);
bool setTunnelResolvers(const QString &ifname, const QStringList &resolvers);
bool restoreTunnelResolvers();
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,19 +57,15 @@ 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);
private:
bool maybeUpdateResolvers(const InterfaceConfig& config);
bool addExclusionRoute(const IPAddress& address);
bool delExclusionRoute(const IPAddress& address);
void checkActivations();
WireguardUtils* primaryWgutils() const { return m_tunnels.value(m_primaryIfname); }
QTimer m_activationTimer;
protected:
virtual bool run(Op op, const InterfaceConfig& config) {
@@ -66,9 +73,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;
QMap<QString, WireguardUtils*> m_tunnels;
QString m_primaryIfname;
virtual bool supportIPUtils() const { return false; }
virtual IPUtils* iputils() { return nullptr; }
virtual DnsUtils* dnsutils() { return nullptr; }
@@ -76,18 +85,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
+45 -9
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") {
Daemon::instance()->deactivate(true);
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
@@ -62,6 +62,8 @@ QJsonObject InterfaceConfig::toJson() const {
}
json.insert("vpnDisabledApps", disabledApps);
json.insert("ifname", m_ifname);
return json;
}
+4
View File
@@ -13,6 +13,8 @@
class QJsonObject;
constexpr int ACTIVATION_TIMEOUT_MSEC = 30000;
class InterfaceConfig {
Q_GADGET
@@ -57,6 +59,8 @@ class InterfaceConfig {
QString m_underloadPacketMagicHeader;
QString m_transportPacketMagicHeader;
QMap<QString, QString> m_specialJunk;
QString m_ifname;
bool m_deferAddressSetup = false;
QJsonObject toJson() const;
QString toWgConf(
+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;
+1
View File
@@ -1,5 +1,6 @@
#include <QDebug>
#include <QTimer>
#include <QLocalSocket>
#include <libssh/libssh.h>
#include "amneziaApplication.h"
+5 -1
View File
@@ -44,6 +44,8 @@ class ControllerImpl : public QObject {
// "disconnecting" state until the "disconnected" signal is received.
virtual void deactivate() = 0;
virtual void setPrimary(const QJsonObject& config) { Q_UNUSED(config) }
// This method is used to retrieve the VPN tunnel status (mainly the number
// of bytes sent and received). It's called always when the VPN tunnel is
// active.
@@ -71,11 +73,13 @@ class ControllerImpl : public QObject {
void initialized(bool status, bool connected,
const QDateTime& connectionDate);
// These 2 signals can be dispatched at any time.
void connected(const QString& pubkey,
const QDateTime& connectionTimestamp = QDateTime());
void disconnected();
void primaryReady();
void primaryFailed();
// This method should be emitted after a checkStatus() call.
// "serverIpv4Gateway" is the current VPN tunnel gateway.
// "deviceIpv4Address" is the address of the VPN client.
+42 -5
View File
@@ -39,7 +39,8 @@ namespace {
Logger logger("LocalSocketController");
}
LocalSocketController::LocalSocketController() {
LocalSocketController::LocalSocketController(const QString& ifname)
: m_ifname(ifname) {
MZ_COUNT_CTOR(LocalSocketController);
m_socket = new QLocalSocket(this);
@@ -121,7 +122,8 @@ void LocalSocketController::daemonConnected() {
checkStatus();
}
void LocalSocketController::activate(const QJsonObject &rawConfig) {
QJsonObject LocalSocketController::buildActivateJson(const QJsonObject& rawConfig,
const QString& ifname) {
QString protocolName = rawConfig.value("protocol").toString();
int splitTunnelType = rawConfig.value("splitTunnelType").toInt();
@@ -134,11 +136,9 @@ void LocalSocketController::activate(const QJsonObject &rawConfig) {
QJsonObject wgConfig = rawConfig.value(protocolName + "_config_data").toObject();
QJsonObject json;
json.insert("type", "activate");
// json.insert("hopindex", QJsonValue((double)hop.m_hopindex));
json.insert("privateKey", wgConfig.value(amnezia::configKey::clientPrivKey));
json.insert("deviceIpv4Address", wgConfig.value(amnezia::configKey::clientIp));
m_deviceIpv4 = wgConfig.value(amnezia::configKey::clientIp).toString();
// set up IPv6 unique-local-address, ULA, with "fd00::/8" prefix, not globally routable.
// this will be default IPv6 gateway, OS recognizes that IPv6 link is local and switches to IPv4.
@@ -230,7 +230,6 @@ void LocalSocketController::activate(const QJsonObject &rawConfig) {
json.insert("allowedIPAddressRanges", jsAllowedIPAddesses);
QJsonArray jsExcludedAddresses;
jsExcludedAddresses.append(wgConfig.value(amnezia::configKey::hostName));
if (splitTunnelType == 2) {
for (auto v : splitTunnelSites) {
QString ipRange = v.toString();
@@ -292,6 +291,23 @@ void LocalSocketController::activate(const QJsonObject &rawConfig) {
json.insert(amnezia::configKey::specialJunk5, wgConfig.value(amnezia::configKey::specialJunk5));
}
json.insert("ifname", ifname);
return json;
}
void LocalSocketController::activate(const QJsonObject& rawConfig) {
const QString protocolName = rawConfig.value("protocol").toString();
const QJsonObject wgConfig = rawConfig.value(protocolName + "_config_data").toObject();
m_deviceIpv4 = wgConfig.value(amnezia::configKey::clientIp).toString();
QJsonObject json = buildActivateJson(rawConfig, m_ifname);
json.insert("type", "activate");
write(json);
}
void LocalSocketController::setPrimary(const QJsonObject& rawConfig) {
QJsonObject json = buildActivateJson(rawConfig, m_ifname);
json.insert("type", "setPrimary");
write(json);
}
@@ -306,6 +322,7 @@ void LocalSocketController::deactivate() {
QJsonObject json;
json.insert("type", "deactivate");
json.insert("ifname", m_ifname);
write(json);
emit disconnected();
}
@@ -471,12 +488,20 @@ void LocalSocketController::parseCommand(const QByteArray& command) {
return;
}
auto belongsToThisTunnel = [this, &obj]() {
const QJsonValue val = obj.value("ifname");
return !val.isString() || val.toString() == m_ifname;
};
if (type == "disconnected") {
if (!belongsToThisTunnel()) return;
disconnectInternal();
return;
}
if (type == "connected") {
if (!belongsToThisTunnel()) return;
QJsonValue pubkey = obj.value("pubkey");
if (!pubkey.isString()) {
logger.error() << "Unexpected pubkey value";
@@ -494,6 +519,18 @@ void LocalSocketController::parseCommand(const QByteArray& command) {
return;
}
if (type == "primaryReady") {
if (!belongsToThisTunnel()) return;
emit primaryReady();
return;
}
if (type == "primaryFailed") {
if (!belongsToThisTunnel()) return;
emit primaryFailed();
return;
}
if (type == "backendFailure") {
if (!obj.contains("errorCode")) {
// report a generic error if we dont know what it is.
+8 -1
View File
@@ -19,7 +19,7 @@ class LocalSocketController final : public ControllerImpl {
Q_DISABLE_COPY_MOVE(LocalSocketController)
public:
LocalSocketController();
explicit LocalSocketController(const QString& ifname);
~LocalSocketController();
void initialize(const Device* device, const Keys* keys) override;
@@ -28,6 +28,8 @@ class LocalSocketController final : public ControllerImpl {
void deactivate() override;
void setPrimary(const QJsonObject& rawConfig) override;
void checkStatus() override;
void getBackendLogs(std::function<void(const QString&)>&& callback) override;
@@ -36,6 +38,10 @@ class LocalSocketController final : public ControllerImpl {
bool multihopSupported() override { return true; }
public:
static QJsonObject buildActivateJson(const QJsonObject& rawConfig,
const QString& ifname);
private:
void initializeInternal();
void disconnectInternal();
@@ -59,6 +65,7 @@ class LocalSocketController final : public ControllerImpl {
QByteArray m_buffer;
QString m_ifname;
QString m_deviceIpv4;
std::function<void(const QString&)> m_logCallback = nullptr;
@@ -47,7 +47,7 @@ bool IPUtilsLinux::setMTUAndUp(const InterfaceConfig& config) {
// Setup the interface to interact with
struct ifreq ifr;
strncpy(ifr.ifr_name, WG_INTERFACE, IFNAMSIZ);
strncpy(ifr.ifr_name, config.m_ifname.toUtf8().constData(), IFNAMSIZ);
// MTU
// FIXME: We need to know how many layers deep this particular
@@ -76,7 +76,7 @@ bool IPUtilsLinux::addIP4AddressToDevice(const InterfaceConfig& config) {
struct sockaddr_in* ifrAddr = (struct sockaddr_in*)&ifr.ifr_addr;
// Name the interface and set family
strncpy(ifr.ifr_name, WG_INTERFACE, IFNAMSIZ);
strncpy(ifr.ifr_name, config.m_ifname.toUtf8().constData(), IFNAMSIZ);
ifr.ifr_addr.sa_family = AF_INET;
// Get the device address to add to interface
@@ -126,7 +126,7 @@ bool IPUtilsLinux::addIP6AddressToDevice(const InterfaceConfig& config) {
// Get the index of named ifr and link with ifr6
struct ifreq ifr;
strncpy(ifr.ifr_name, WG_INTERFACE, IFNAMSIZ);
strncpy(ifr.ifr_name, config.m_ifname.toUtf8().constData(), IFNAMSIZ);
ifr.ifr_addr.sa_family = AF_INET6;
int ret = ioctl(sockfd, SIOGIFINDEX, &ifr);
if (ret) {
@@ -28,7 +28,6 @@ LinuxDaemon::LinuxDaemon() : Daemon(nullptr) {
logger.debug() << "Daemon created";
m_wgutils = new WireguardUtilsLinux(this);
m_dnsutils = new DnsUtilsLinux(this);
m_iputils = new IPUtilsLinux(this);
@@ -50,3 +49,4 @@ LinuxDaemon* LinuxDaemon::instance() {
Q_ASSERT(s_daemon);
return s_daemon;
}
+4 -4
View File
@@ -12,8 +12,6 @@
#include "wireguardutilslinux.h"
class LinuxDaemon final : public Daemon {
friend class IPUtilsMacos;
public:
LinuxDaemon();
~LinuxDaemon();
@@ -21,13 +19,15 @@ class LinuxDaemon final : public Daemon {
static LinuxDaemon* instance();
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; }
WireguardUtils* createWgUtils() override {
return new WireguardUtilsLinux(this);
}
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);
@@ -55,6 +53,8 @@ LinuxRouteMonitor::LinuxRouteMonitor(const QString& ifname, QObject* parent)
MZ_COUNT_CTOR(LinuxRouteMonitor);
logger.debug() << "LinuxRouteMonitor created.";
m_physicalGateway = NetworkUtilities::getGatewayAndIface().first;
m_nlsock = socket(AF_NETLINK, SOCK_DGRAM, NETLINK_ROUTE);
if (m_nlsock < 0) {
logger.warning() << "Failed to create netlink socket:" << strerror(errno);
@@ -63,7 +63,7 @@ LinuxRouteMonitor::LinuxRouteMonitor(const QString& ifname, QObject* parent)
struct sockaddr_nl nladdr;
memset(&nladdr, 0, sizeof(nladdr));
nladdr.nl_family = AF_NETLINK;
nladdr.nl_pid = getpid();
nladdr.nl_pid = 0;
if (bind(m_nlsock, (struct sockaddr*)&nladdr, sizeof(nladdr)) != 0) {
logger.warning() << "Failed to bind netlink socket:" << strerror(errno);
}
@@ -153,7 +153,7 @@ bool LinuxRouteMonitor::rtmSendRoute(int action, int flags, int type,
}
if (rtm->rtm_type == RTN_UNICAST) {
int index = if_nametoindex(WG_INTERFACE);
int index = if_nametoindex(m_ifname.toUtf8().constData());
if (index <= 0) {
logger.error() << "if_nametoindex() failed:" << strerror(errno);
@@ -164,14 +164,15 @@ bool LinuxRouteMonitor::rtmSendRoute(int action, int flags, int type,
}
if (rtm->rtm_type == RTN_THROW) {
QString gateway = NetworkUtilities::getGatewayAndIface().first;
if (gateway.isEmpty()) {
logger.warning() << "No default gateway available, skipping exclusion route";
return false;
if (action == RTM_NEWROUTE) {
if (m_physicalGateway.isEmpty()) {
logger.warning() << "No physical gateway available, skipping exclusion route";
return false;
}
struct in_addr ip4;
inet_pton(AF_INET, m_physicalGateway.toUtf8(), &ip4);
nlmsg_append_attr(nlmsg, sizeof(buf), RTA_GATEWAY, &ip4, sizeof(ip4));
}
struct in_addr ip4;
inet_pton(AF_INET, gateway.toUtf8(), &ip4);
nlmsg_append_attr(nlmsg, sizeof(buf), RTA_GATEWAY, &ip4, sizeof(ip4));
nlmsg_append_attr32(nlmsg, sizeof(buf), RTA_PRIORITY, 0);
rtm->rtm_type = RTN_UNICAST;
}
@@ -32,6 +32,7 @@ class LinuxRouteMonitor final : public QObject {
bool rtmSendRoute(int action, int flags, int type,
const IPAddress& prefix);
QString m_ifname;
QString m_physicalGateway;
unsigned int m_ifindex = 0;
int m_nlsock = -1;
int m_nlseq = 0;
@@ -8,17 +8,15 @@
#include <QByteArray>
#include <QDir>
#include <QElapsedTimer>
#include <QFile>
#include <QLocalSocket>
#include <QTimer>
#include <QThread>
#include "linuxfirewall.h"
#include "leakdetector.h"
#include "logger.h"
#include "killswitch.h"
constexpr const int WG_TUN_PROC_TIMEOUT = 5000;
constexpr const char* WG_RUNTIME_DIR = "/var/run/amneziawg";
@@ -59,19 +57,20 @@ void WireguardUtilsLinux::tunnelErrorOccurred(QProcess::ProcessError error) {
}
bool WireguardUtilsLinux::addInterface(const InterfaceConfig& config) {
Q_UNUSED(config);
if (m_tunnel.state() != QProcess::NotRunning) {
logger.warning() << "Unable to start: tunnel process already running";
return false;
}
const QString ifname = config.m_ifname;
QDir wgRuntimeDir(WG_RUNTIME_DIR);
if (!wgRuntimeDir.exists()) {
wgRuntimeDir.mkpath(".");
}
QProcessEnvironment pe = QProcessEnvironment::systemEnvironment();
QString wgNameFile = wgRuntimeDir.filePath(QString(WG_INTERFACE) + ".sock");
QString wgNameFile = wgRuntimeDir.filePath(ifname + ".sock");
pe.insert("WG_TUN_NAME_FILE", wgNameFile);
#ifdef MZ_DEBUG
pe.insert("LOG_LEVEL", "debug");
@@ -79,7 +78,7 @@ bool WireguardUtilsLinux::addInterface(const InterfaceConfig& config) {
m_tunnel.setProcessEnvironment(pe);
QDir appPath(QCoreApplication::applicationDirPath());
QStringList wgArgs = {"-f", "amn0"};
QStringList wgArgs = {"-f", ifname};
m_tunnel.start(appPath.filePath("amneziawg-go"), wgArgs);
if (!m_tunnel.waitForStarted(WG_TUN_PROC_TIMEOUT)) {
logger.error() << "Unable to start tunnel process due to timeout";
@@ -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);
@@ -194,10 +170,8 @@ bool WireguardUtilsLinux::deleteInterface() {
// Garbage collect.
QDir wgRuntimeDir(WG_RUNTIME_DIR);
QFile::remove(wgRuntimeDir.filePath(QString(WG_INTERFACE) + ".name"));
QFile::remove(wgRuntimeDir.filePath(m_ifname + ".name"));
// double-check + ensure our firewall is installed and enabled
KillSwitch::instance()->disableKillSwitch();
return true;
}
@@ -234,13 +208,6 @@ bool WireguardUtilsLinux::updatePeer(const InterfaceConfig& config) {
out << "allowed_ip=" << ip.toString() << "\n";
}
// Exclude the server address, except for multihop exit servers.
if ((config.m_hopType != InterfaceConfig::MultiHopExit) &&
(m_rtmonitor != nullptr)) {
m_rtmonitor->addExclusionRoute(IPAddress(config.m_serverIpv4AddrIn));
m_rtmonitor->addExclusionRoute(IPAddress(config.m_serverIpv6AddrIn));
}
int err = uapiErrno(uapiCommand(message));
if (err != 0) {
logger.error() << "Peer configuration failed:" << strerror(err);
@@ -252,13 +219,6 @@ bool WireguardUtilsLinux::deletePeer(const InterfaceConfig& config) {
QByteArray publicKey =
QByteArray::fromBase64(qPrintable(config.m_serverPublicKey));
// Clear exclustion routes for this peer.
if ((config.m_hopType != InterfaceConfig::MultiHopExit) &&
(m_rtmonitor != nullptr)) {
m_rtmonitor->deleteExclusionRoute(IPAddress(config.m_serverIpv4AddrIn));
m_rtmonitor->deleteExclusionRoute(IPAddress(config.m_serverIpv6AddrIn));
}
QString message;
QTextStream out(&message);
out << "set=1\n";
@@ -338,7 +298,7 @@ bool WireguardUtilsLinux::deleteRoutePrefix(const IPAddress& prefix) {
return false;
}
if (prefix.prefixLength() > 0) {
return m_rtmonitor->insertRoute(prefix);
return m_rtmonitor->deleteRoute(prefix);
}
// Ensure that we do not replace the default route.
@@ -389,13 +349,9 @@ bool WireguardUtilsLinux::excludeLocalNetworks(const QList<IPAddress>& routes) {
QString WireguardUtilsLinux::uapiCommand(const QString& command) {
QLocalSocket socket;
QTimer uapiTimeout;
QDir wgRuntimeDir(WG_RUNTIME_DIR);
QString wgSocketFile = wgRuntimeDir.filePath(m_ifname + ".sock");
uapiTimeout.setSingleShot(true);
uapiTimeout.start(WG_TUN_PROC_TIMEOUT);
socket.connectToServer(wgSocketFile, QIODevice::ReadWrite);
if (!socket.waitForConnected(WG_TUN_PROC_TIMEOUT)) {
logger.error() << "QLocalSocket::waitForConnected() failed:"
@@ -410,13 +366,15 @@ QString WireguardUtilsLinux::uapiCommand(const QString& command) {
}
socket.write(message);
QElapsedTimer elapsed;
elapsed.start();
QByteArray reply;
while (!reply.contains("\n\n")) {
if (!uapiTimeout.isActive()) {
const qint64 remaining = WG_TUN_PROC_TIMEOUT - elapsed.elapsed();
if (remaining <= 0 || !socket.waitForReadyRead(static_cast<int>(remaining))) {
logger.error() << "UAPI command timed out";
return QString();
}
QCoreApplication::processEvents(QEventLoop::AllEvents, 100);
reply.append(socket.readAll());
}
@@ -442,45 +400,15 @@ QString WireguardUtilsLinux::waitForTunnelName(const QString& filename) {
timeout.setSingleShot(true);
timeout.start(WG_TUN_PROC_TIMEOUT);
QFile file(filename);
while ((m_tunnel.state() == QProcess::Running) && timeout.isActive()) {
QCoreApplication::processEvents(QEventLoop::AllEvents, 100);
QString ifname = "amn0";
// Test-connect to the UAPI socket.
QLocalSocket sock;
QDir wgRuntimeDir(WG_RUNTIME_DIR);
QString sockName = wgRuntimeDir.filePath(ifname + ".sock");
sock.connectToServer(sockName, QIODevice::ReadWrite);
sock.connectToServer(filename, QIODevice::ReadWrite);
if (sock.waitForConnected(100)) {
return ifname;
return QFileInfo(filename).baseName();
}
}
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();
@@ -185,6 +185,9 @@ bool DnsUtilsMacos::restoreResolvers() {
}
void DnsUtilsMacos::backupService(const QString& uuid) {
if (m_prevServices.contains(uuid)) {
return;
}
DnsBackup backup;
CFStringRef path = CFStringCreateWithFormat(
kCFAllocatorSystemDefault, nullptr,
+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
@@ -28,7 +28,6 @@ MacOSDaemon::MacOSDaemon() : Daemon(nullptr) {
logger.debug() << "Daemon created";
m_wgutils = new WireguardUtilsMacos(this);
m_dnsutils = new DnsUtilsMacos(this);
m_iputils = new IPUtilsMacos(this);
@@ -50,3 +49,4 @@ MacOSDaemon* MacOSDaemon::instance() {
Q_ASSERT(s_daemon);
return s_daemon;
}
+4 -4
View File
@@ -11,8 +11,6 @@
#include "wireguardutilsmacos.h"
class MacOSDaemon final : public Daemon {
friend class IPUtilsMacos;
public:
MacOSDaemon();
~MacOSDaemon();
@@ -20,13 +18,15 @@ class MacOSDaemon final : public Daemon {
static MacOSDaemon* instance();
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; }
WireguardUtils* createWgUtils() override {
return new WireguardUtilsMacos(this);
}
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
{
@@ -51,7 +51,6 @@ MacosRouteMonitor::MacosRouteMonitor(const QString& ifname, QObject* parent)
MacosRouteMonitor::~MacosRouteMonitor() {
MZ_COUNT_DTOR(MacosRouteMonitor);
flushExclusionRoutes();
if (m_rtsock >= 0) {
close(m_rtsock);
}
@@ -436,7 +435,15 @@ bool MacosRouteMonitor::rtmSendRoute(int action, const IPAddress& prefix,
return true;
}
if ((action == RTM_ADD) && (errno == EEXIST)) {
return true;
rtm->rtm_type = RTM_DELETE;
rtm->rtm_seq = m_rtseq++;
write(m_rtsock, rtm, rtm->rtm_msglen);
rtm->rtm_type = RTM_ADD;
rtm->rtm_seq = m_rtseq++;
len = write(m_rtsock, rtm, rtm->rtm_msglen);
if (len == rtm->rtm_msglen) {
return true;
}
}
if ((action == RTM_DELETE) && (errno == ESRCH)) {
return true;
@@ -9,6 +9,7 @@
#include <QByteArray>
#include <QDir>
#include <QElapsedTimer>
#include <QFile>
#include <QLocalSocket>
#include <QTimer>
@@ -16,8 +17,6 @@
#include "leakdetector.h"
#include "logger.h"
#include "killswitch.h"
constexpr const int WG_TUN_PROC_TIMEOUT = 5000;
constexpr const char* WG_RUNTIME_DIR = "/var/run/amneziawg";
@@ -58,19 +57,20 @@ void WireguardUtilsMacos::tunnelErrorOccurred(QProcess::ProcessError error) {
}
bool WireguardUtilsMacos::addInterface(const InterfaceConfig& config) {
Q_UNUSED(config);
if (m_tunnel.state() != QProcess::NotRunning) {
logger.warning() << "Unable to start: tunnel process already running";
return false;
}
const QString ifname = config.m_ifname;
QDir wgRuntimeDir(WG_RUNTIME_DIR);
if (!wgRuntimeDir.exists()) {
wgRuntimeDir.mkpath(".");
}
QProcessEnvironment pe = QProcessEnvironment::systemEnvironment();
QString wgNameFile = wgRuntimeDir.filePath(QString(WG_INTERFACE) + ".name");
QString wgNameFile = wgRuntimeDir.filePath(ifname + ".name");
pe.insert("WG_TUN_NAME_FILE", wgNameFile);
#ifdef MZ_DEBUG
pe.insert("LOG_LEVEL", "debug");
@@ -92,6 +92,7 @@ bool WireguardUtilsMacos::addInterface(const InterfaceConfig& config) {
m_tunnel.kill();
return false;
}
QFile::remove(wgNameFile);
logger.debug() << "Created wireguard interface" << m_ifname;
// Start the routing table monitor.
@@ -145,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);
}
@@ -190,13 +167,6 @@ bool WireguardUtilsMacos::deleteInterface() {
m_tunnel.waitForFinished(WG_TUN_PROC_TIMEOUT);
}
// Garbage collect.
QDir wgRuntimeDir(WG_RUNTIME_DIR);
QFile::remove(wgRuntimeDir.filePath(QString(WG_INTERFACE) + ".name"));
// double-check + ensure our firewall is installed and enabled
KillSwitch::instance()->disableKillSwitch();
return true;
}
@@ -234,13 +204,6 @@ bool WireguardUtilsMacos::updatePeer(const InterfaceConfig& config) {
out << "allowed_ip=" << ip.toString() << "\n";
}
// Exclude the server address, except for multihop exit servers.
if ((config.m_hopType != InterfaceConfig::MultiHopExit) &&
(m_rtmonitor != nullptr)) {
m_rtmonitor->addExclusionRoute(IPAddress(config.m_serverIpv4AddrIn));
m_rtmonitor->addExclusionRoute(IPAddress(config.m_serverIpv6AddrIn));
}
int err = uapiErrno(uapiCommand(message));
if (err != 0) {
logger.error() << "Peer configuration failed:" << strerror(err);
@@ -252,13 +215,6 @@ bool WireguardUtilsMacos::deletePeer(const InterfaceConfig& config) {
QByteArray publicKey =
QByteArray::fromBase64(qPrintable(config.m_serverPublicKey));
// Clear exclustion routes for this peer.
if ((config.m_hopType != InterfaceConfig::MultiHopExit) &&
(m_rtmonitor != nullptr)) {
m_rtmonitor->deleteExclusionRoute(IPAddress(config.m_serverIpv4AddrIn));
m_rtmonitor->deleteExclusionRoute(IPAddress(config.m_serverIpv6AddrIn));
}
QString message;
QTextStream out(&message);
out << "set=1\n";
@@ -389,13 +345,9 @@ bool WireguardUtilsMacos::excludeLocalNetworks(const QList<IPAddress>& routes) {
QString WireguardUtilsMacos::uapiCommand(const QString& command) {
QLocalSocket socket;
QTimer uapiTimeout;
QDir wgRuntimeDir(WG_RUNTIME_DIR);
QString wgSocketFile = wgRuntimeDir.filePath(m_ifname + ".sock");
uapiTimeout.setSingleShot(true);
uapiTimeout.start(WG_TUN_PROC_TIMEOUT);
socket.connectToServer(wgSocketFile, QIODevice::ReadWrite);
if (!socket.waitForConnected(WG_TUN_PROC_TIMEOUT)) {
logger.error() << "QLocalSocket::waitForConnected() failed:"
@@ -410,13 +362,15 @@ QString WireguardUtilsMacos::uapiCommand(const QString& command) {
}
socket.write(message);
QElapsedTimer elapsed;
elapsed.start();
QByteArray reply;
while (!reply.contains("\n\n")) {
if (!uapiTimeout.isActive()) {
const qint64 remaining = WG_TUN_PROC_TIMEOUT - elapsed.elapsed();
if (remaining <= 0 || !socket.waitForReadyRead(static_cast<int>(remaining))) {
logger.error() << "UAPI command timed out";
return QString();
}
QCoreApplication::processEvents(QEventLoop::AllEvents, 100);
reply.append(socket.readAll());
}
@@ -463,28 +417,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() {
@@ -112,3 +106,11 @@ void WindowsDaemon::monitorBackendFailure() {
emit backendFailure();
deactivate();
}
WireguardUtils* WindowsDaemon::createWgUtils() {
auto utils = WireguardUtilsWindows::create(m_firewallManager, this);
if (!utils) return nullptr;
connect(utils.get(), &WireguardUtilsWindows::backendFailure, this,
&WindowsDaemon::monitorBackendFailure);
return utils.release();
}
@@ -28,8 +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;
private:
void monitorBackendFailure();
@@ -42,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;
@@ -37,11 +37,14 @@ int WindowsDaemonTunnel::run(QStringList& tokens) {
QCoreApplication::setApplicationName("Amnezia VPN Tunnel");
QCoreApplication::setApplicationVersion(Constants::versionString());
if (tokens.length() != 2) {
logger.error() << "Expected 1 parameter only: the config file.";
if (tokens.length() < 2 || tokens.length() > 3) {
logger.error() << "Expected: <config> [<ifname>]";
return 1;
}
QString maybeConfig = tokens.at(1);
QString name = tokens.length() == 3 && !tokens.at(2).isEmpty()
? tokens.at(2)
: WireguardUtilsWindows::s_defaultInterfaceName();
if (!maybeConfig.startsWith("[Interface]")) {
logger.error() << "parameter Does not seem to be a config";
@@ -64,7 +67,6 @@ int WindowsDaemonTunnel::run(QStringList& tokens) {
WindowsUtils::windowsLog("Failed to get WireGuardTunnelService function");
return 1;
}
auto name = WireguardUtilsWindows::s_interfaceName();
if (!tunnelProc(maybeConfig.utf16(), name.utf16())) {
logger.error() << "Failed to activate the tunnel service";
return 1;
@@ -159,7 +159,7 @@ bool WindowsFirewall::initSublayer() {
return true;
}
bool WindowsFirewall::enableInterface(int vpnAdapterIndex) {
bool WindowsFirewall::enableInterface(int vpnAdapterIndex, const QString& ifname) {
// Checks if the FW_Rule was enabled succesfully,
// disables the whole killswitch and returns false if not.
#define FW_OK(rule) \
@@ -182,31 +182,39 @@ bool WindowsFirewall::enableInterface(int vpnAdapterIndex) {
} \
}
logger.info() << "Enabling Killswitch Using Adapter:" << vpnAdapterIndex;
if (vpnAdapterIndex < 0)
{
logger.info() << "Enabling Killswitch Using Adapter:" << vpnAdapterIndex
<< "ifname:" << ifname;
QList<uint64_t>& perTunnel = ifname.isEmpty() ? m_globalRules
: m_tunnelRules[ifname];
if (vpnAdapterIndex < 0) {
IPAddress allv4("0.0.0.0/0");
if (!blockTrafficTo(allv4, MED_WEIGHT,
"Block Internet", "killswitch")) {
return false;
}
IPAddress allv6("::/0");
if (!blockTrafficTo(allv6, MED_WEIGHT,
"Block Internet", "killswitch")) {
if (!blockTrafficTo(allv4, MED_WEIGHT, "Block Internet", perTunnel)) {
return false;
}
} else
FW_OK(allowTrafficOfAdapter(vpnAdapterIndex, MED_WEIGHT,
"Allow usage of VPN Adapter"));
FW_OK(allowDHCPTraffic(MED_WEIGHT, "Allow DHCP Traffic"));
FW_OK(allowHyperVTraffic(MAX_WEIGHT, "Allow Hyper-V Traffic"));
FW_OK(allowTrafficForAppOnAll(getCurrentPath(), MAX_WEIGHT,
"Allow all for AmneziaVPN.exe"));
FW_OK(blockTrafficOnPort(53, MED_WEIGHT, "Block all DNS"));
FW_OK(allowLoopbackTraffic(MED_WEIGHT,
"Allow Loopback traffic on device %1"));
IPAddress allv6("::/0");
if (!blockTrafficTo(allv6, MED_WEIGHT, "Block Internet", perTunnel)) {
return false;
}
} else {
FW_OK(allowTrafficOfAdapter(vpnAdapterIndex, MED_WEIGHT,
"Allow usage of VPN Adapter", perTunnel));
}
logger.debug() << "Killswitch on! Rules:" << m_activeRules.length();
if (m_globalRules.isEmpty()) {
FW_OK(allowDHCPTraffic(MED_WEIGHT, "Allow DHCP Traffic", m_globalRules));
FW_OK(allowHyperVTraffic(MAX_WEIGHT, "Allow Hyper-V Traffic", m_globalRules));
FW_OK(allowTrafficForAppOnAll(getCurrentPath(), MAX_WEIGHT,
"Allow all for AmneziaVPN.exe", m_globalRules));
FW_OK(blockTrafficOnPort(53, MED_WEIGHT, "Block all DNS", m_globalRules));
FW_OK(allowLoopbackTraffic(MED_WEIGHT,
"Allow Loopback traffic on device %1",
m_globalRules));
}
logger.debug() << "Killswitch on! Globals:" << m_globalRules.length()
<< "Tunnel[" << ifname
<< "]:" << m_tunnelRules.value(ifname).length();
return true;
#undef FW_OK
}
@@ -226,7 +234,8 @@ bool WindowsFirewall::enableLanBypass(const QList<IPAddress>& ranges) {
// Blocking unprotected traffic
for (const IPAddress& prefix : ranges) {
if (!allowTrafficTo(prefix, LOW_WEIGHT + 1, "Allow LAN bypass traffic")) {
if (!allowTrafficTo(prefix, LOW_WEIGHT + 1, "Allow LAN bypass traffic",
m_globalRules)) {
return false;
}
}
@@ -242,7 +251,10 @@ bool WindowsFirewall::enableLanBypass(const QList<IPAddress>& ranges) {
}
// Allow unprotected traffic sent to the following address ranges.
bool WindowsFirewall::allowTrafficRange(const QStringList& ranges) {
bool WindowsFirewall::allowTrafficRange(const QStringList& ranges, const QString& ifname) {
QList<uint64_t>& target = ifname.isEmpty() ? m_globalRules
: m_tunnelRules[ifname];
// Start the firewall transaction
auto result = FwpmTransactionBegin(m_sessionHandle, NULL);
if (result != ERROR_SUCCESS) {
@@ -255,8 +267,9 @@ bool WindowsFirewall::allowTrafficRange(const QStringList& ranges) {
});
for (const QString& addr : ranges) {
logger.debug() << "Allow killswitch exclude: " << addr;
if (!allowTrafficTo(QHostAddress(addr), HIGH_WEIGHT, "Allow killswitch bypass traffic")) {
logger.debug() << "Allow killswitch exclude: " << addr << "ifname:" << ifname;
if (!allowTrafficTo(QHostAddress(addr), HIGH_WEIGHT,
"Allow killswitch bypass traffic", target)) {
return false;
}
}
@@ -273,6 +286,10 @@ bool WindowsFirewall::allowTrafficRange(const QStringList& ranges) {
bool WindowsFirewall::enablePeerTraffic(const InterfaceConfig& config) {
QList<uint64_t>& target = config.m_ifname.isEmpty()
? m_globalRules
: m_tunnelRules[config.m_ifname];
// Start the firewall transaction
auto result = FwpmTransactionBegin(m_sessionHandle, NULL);
if (result != ERROR_SUCCESS) {
@@ -288,12 +305,12 @@ bool WindowsFirewall::enablePeerTraffic(const InterfaceConfig& config) {
logger.info() << "Enabling traffic for peer"
<< config.m_serverPublicKey;
if (!blockTrafficTo(config.m_allowedIPAddressRanges, LOW_WEIGHT,
"Block Internet", config.m_serverPublicKey)) {
"Block Internet", target)) {
return false;
}
if (!config.m_primaryDnsServer.isEmpty()) {
if (!allowTrafficTo(QHostAddress(config.m_primaryDnsServer), 53, HIGH_WEIGHT,
"Allow DNS-Server", config.m_serverPublicKey)) {
"Allow DNS-Server", target)) {
return false;
}
// In some cases, we might configure a 2nd DNS server for IPv6, however
@@ -302,7 +319,7 @@ bool WindowsFirewall::enablePeerTraffic(const InterfaceConfig& config) {
if (config.m_primaryDnsServer == config.m_serverIpv4Gateway) {
if (!allowTrafficTo(QHostAddress(config.m_serverIpv6Gateway), 53,
HIGH_WEIGHT, "Allow extra IPv6 DNS-Server",
config.m_serverPublicKey)) {
target)) {
return false;
}
}
@@ -310,7 +327,7 @@ bool WindowsFirewall::enablePeerTraffic(const InterfaceConfig& config) {
if (!config.m_secondaryDnsServer.isEmpty()) {
if (!allowTrafficTo(QHostAddress(config.m_secondaryDnsServer), 53, HIGH_WEIGHT,
"Allow DNS-Server", config.m_serverPublicKey)) {
"Allow DNS-Server", target)) {
return false;
}
// In some cases, we might configure a 2nd DNS server for IPv6, however
@@ -319,7 +336,7 @@ bool WindowsFirewall::enablePeerTraffic(const InterfaceConfig& config) {
if (config.m_secondaryDnsServer == config.m_serverIpv4Gateway) {
if (!allowTrafficTo(QHostAddress(config.m_serverIpv6Gateway), 53,
HIGH_WEIGHT, "Allow extra IPv6 DNS-Server",
config.m_serverPublicKey)) {
target)) {
return false;
}
}
@@ -328,7 +345,7 @@ bool WindowsFirewall::enablePeerTraffic(const InterfaceConfig& config) {
for (const QString& dns : config.m_allowedDnsServers) {
logger.debug() << "Allow DNS: " << dns;
if (!allowTrafficTo(QHostAddress(dns), 53, HIGH_WEIGHT,
"Allow DNS-Server", config.m_serverPublicKey)) {
"Allow DNS-Server", target)) {
return false;
}
}
@@ -338,7 +355,7 @@ bool WindowsFirewall::enablePeerTraffic(const InterfaceConfig& config) {
logger.debug() << "excludedAddresses range: " << i;
if (!allowTrafficTo(i, HIGH_WEIGHT,
"Allow Ecxlude route", config.m_serverPublicKey)) {
"Allow Ecxlude route", target)) {
return false;
}
}
@@ -354,35 +371,6 @@ bool WindowsFirewall::enablePeerTraffic(const InterfaceConfig& config) {
return true;
}
bool WindowsFirewall::disablePeerTraffic(const QString& pubkey) {
auto result = FwpmTransactionBegin(m_sessionHandle, NULL);
auto cleanup = qScopeGuard([&] {
if (result != ERROR_SUCCESS) {
FwpmTransactionAbort0(m_sessionHandle);
}
});
if (result != ERROR_SUCCESS) {
logger.error() << "FwpmTransactionBegin0 failed. Return value:.\n"
<< result;
return false;
}
logger.info() << "Disabling traffic for peer" << pubkey;
for (const auto& filterID : m_peerRules.values(pubkey)) {
FwpmFilterDeleteById0(m_sessionHandle, filterID);
m_peerRules.remove(pubkey, filterID);
}
// Commit!
result = FwpmTransactionCommit0(m_sessionHandle);
if (result != ERROR_SUCCESS) {
logger.error() << "FwpmTransactionCommit0 failed. Return value:.\n"
<< result;
return false;
}
return true;
}
bool WindowsFirewall::disableKillSwitch() {
return KillSwitch::instance()->disableKillSwitch();
}
@@ -400,11 +388,13 @@ bool WindowsFirewall::allowAllTraffic() {
return false;
}
for (const auto& filterID : m_peerRules.values()) {
FwpmFilterDeleteById0(m_sessionHandle, filterID);
for (const auto& bucket : qAsConst(m_tunnelRules)) {
for (const auto& filterID : bucket) {
FwpmFilterDeleteById0(m_sessionHandle, filterID);
}
}
for (const auto& filterID : qAsConst(m_activeRules)) {
for (const auto& filterID : qAsConst(m_globalRules)) {
FwpmFilterDeleteById0(m_sessionHandle, filterID);
}
@@ -415,15 +405,42 @@ bool WindowsFirewall::allowAllTraffic() {
<< result;
return false;
}
m_peerRules.clear();
m_activeRules.clear();
m_tunnelRules.clear();
m_globalRules.clear();
logger.debug() << "Firewall Disabled!";
return true;
}
bool WindowsFirewall::disableKillSwitchForTunnel(const QString& ifname) {
if (ifname.isEmpty() || !m_tunnelRules.contains(ifname)) {
return true;
}
auto result = FwpmTransactionBegin(m_sessionHandle, NULL);
if (result != ERROR_SUCCESS) {
logger.error() << "FwpmTransactionBegin0 failed. Return value:" << result;
return false;
}
const QList<uint64_t> filters = m_tunnelRules.take(ifname);
logger.info() << "Disabling killswitch filters for tunnel" << ifname
<< "count:" << filters.length();
for (const auto& filterID : filters) {
FwpmFilterDeleteById0(m_sessionHandle, filterID);
}
result = FwpmTransactionCommit0(m_sessionHandle);
if (result != ERROR_SUCCESS) {
logger.error() << "FwpmTransactionCommit0 failed. Return value:" << result;
return false;
}
return true;
}
bool WindowsFirewall::allowTrafficForAppOnAll(const QString& exePath,
int weight,
const QString& title) {
const QString& title,
QList<uint64_t>& target) {
DWORD result = ERROR_SUCCESS;
Q_ASSERT(weight <= 15);
@@ -460,7 +477,7 @@ bool WindowsFirewall::allowTrafficForAppOnAll(const QString& exePath,
{
QString desc("Permit (out) IPv4 Traffic of: " + appName);
filter.layerKey = FWPM_LAYER_ALE_AUTH_CONNECT_V4;
if (!enableFilter(&filter, title, desc)) {
if (!enableFilter(&filter, title, desc, target)) {
return false;
}
}
@@ -468,7 +485,7 @@ bool WindowsFirewall::allowTrafficForAppOnAll(const QString& exePath,
{
QString desc("Permit (in) IPv4 Traffic of: " + appName);
filter.layerKey = FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4;
if (!enableFilter(&filter, title, desc)) {
if (!enableFilter(&filter, title, desc, target)) {
return false;
}
}
@@ -476,7 +493,8 @@ bool WindowsFirewall::allowTrafficForAppOnAll(const QString& exePath,
}
bool WindowsFirewall::allowTrafficOfAdapter(int networkAdapter, uint8_t weight,
const QString& title) {
const QString& title,
QList<uint64_t>& target) {
FWPM_FILTER_CONDITION0 conds;
// Condition: Request must be targeting the TUN interface
conds.fieldKey = FWPM_CONDITION_INTERFACE_INDEX;
@@ -498,25 +516,25 @@ bool WindowsFirewall::allowTrafficOfAdapter(int networkAdapter, uint8_t weight,
// #1 Permit outbound IPv4 traffic.
filter.layerKey = FWPM_LAYER_ALE_AUTH_CONNECT_V4;
if (!enableFilter(&filter, title,
description.arg("out").arg(networkAdapter))) {
description.arg("out").arg(networkAdapter), target)) {
return false;
}
// #2 Permit inbound IPv4 traffic.
filter.layerKey = FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4;
if (!enableFilter(&filter, title,
description.arg("in").arg(networkAdapter))) {
description.arg("in").arg(networkAdapter), target)) {
return false;
}
// #3 Permit outbound IPv6 traffic.
filter.layerKey = FWPM_LAYER_ALE_AUTH_CONNECT_V6;
if (!enableFilter(&filter, title,
description.arg("out").arg(networkAdapter))) {
description.arg("out").arg(networkAdapter), target)) {
return false;
}
// #4 Permit inbound IPv6 traffic.
filter.layerKey = FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V6;
if (!enableFilter(&filter, title,
description.arg("in").arg(networkAdapter))) {
description.arg("in").arg(networkAdapter), target)) {
return false;
}
return true;
@@ -524,7 +542,7 @@ bool WindowsFirewall::allowTrafficOfAdapter(int networkAdapter, uint8_t weight,
bool WindowsFirewall::allowTrafficTo(const IPAddress& addr, int weight,
const QString& title,
const QString& peer) {
QList<uint64_t>& target) {
GUID layerKeyOut;
GUID layerKeyIn;
if (addr.type() == QAbstractSocket::IPv4Protocol) {
@@ -562,11 +580,11 @@ bool WindowsFirewall::allowTrafficTo(const IPAddress& addr, int weight,
// Send the filters down to the firewall.
QString description = "Permit traffic %1 " + addr.toString();
filter.layerKey = layerKeyOut;
if (!enableFilter(&filter, title, description.arg("to"), peer)) {
if (!enableFilter(&filter, title, description.arg("to"), target)) {
return false;
}
filter.layerKey = layerKeyIn;
if (!enableFilter(&filter, title, description.arg("from"), peer)) {
if (!enableFilter(&filter, title, description.arg("from"), target)) {
return false;
}
return true;
@@ -574,7 +592,7 @@ bool WindowsFirewall::allowTrafficTo(const IPAddress& addr, int weight,
bool WindowsFirewall::allowTrafficTo(const QHostAddress& targetIP, uint port,
int weight, const QString& title,
const QString& peer) {
QList<uint64_t>& target) {
bool isIPv4 = targetIP.protocol() == QAbstractSocket::IPv4Protocol;
GUID layerOut =
isIPv4 ? FWPM_LAYER_ALE_AUTH_CONNECT_V4 : FWPM_LAYER_ALE_AUTH_CONNECT_V6;
@@ -623,19 +641,20 @@ bool WindowsFirewall::allowTrafficTo(const QHostAddress& targetIP, uint port,
filter.layerKey = layerOut;
if (!enableFilter(&filter, title,
description.arg("to").arg(targetIP.toString()).arg(port),
peer)) {
target)) {
return false;
}
filter.layerKey = layerIn;
if (!enableFilter(&filter, title,
description.arg("from").arg(targetIP.toString()).arg(port),
peer)) {
target)) {
return false;
}
return true;
}
bool WindowsFirewall::allowDHCPTraffic(uint8_t weight, const QString& title) {
bool WindowsFirewall::allowDHCPTraffic(uint8_t weight, const QString& title,
QList<uint64_t>& target) {
// Allow outbound DHCPv4
{
FWPM_FILTER_CONDITION0 conds[4];
@@ -672,7 +691,7 @@ bool WindowsFirewall::allowDHCPTraffic(uint8_t weight, const QString& title) {
filter.layerKey = FWPM_LAYER_ALE_AUTH_CONNECT_V4;
if (!enableFilter(&filter, title, "Allow Outbound DHCP")) {
if (!enableFilter(&filter, title, "Allow Outbound DHCP", target)) {
return false;
}
}
@@ -705,7 +724,7 @@ bool WindowsFirewall::allowDHCPTraffic(uint8_t weight, const QString& title) {
filter.subLayerKey = ST_FW_WINFW_BASELINE_SUBLAYER_KEY;
filter.layerKey = FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4;
if (!enableFilter(&filter, title, "Allow inbound DHCP")) {
if (!enableFilter(&filter, title, "Allow inbound DHCP", target)) {
return false;
}
}
@@ -740,7 +759,7 @@ bool WindowsFirewall::allowDHCPTraffic(uint8_t weight, const QString& title) {
filter.subLayerKey = ST_FW_WINFW_BASELINE_SUBLAYER_KEY;
filter.layerKey = FWPM_LAYER_ALE_AUTH_CONNECT_V6;
if (!enableFilter(&filter, title, "Allow outbound DHCPv6")) {
if (!enableFilter(&filter, title, "Allow outbound DHCPv6", target)) {
return false;
}
}
@@ -773,7 +792,7 @@ bool WindowsFirewall::allowDHCPTraffic(uint8_t weight, const QString& title) {
filter.weight.uint8 = weight;
filter.subLayerKey = ST_FW_WINFW_BASELINE_SUBLAYER_KEY;
filter.layerKey = FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V6;
if (!enableFilter(&filter, title, "Allow inbound DHCPv6")) {
if (!enableFilter(&filter, title, "Allow inbound DHCPv6", target)) {
return false;
}
}
@@ -781,7 +800,8 @@ bool WindowsFirewall::allowDHCPTraffic(uint8_t weight, const QString& title) {
}
// Allows the internal Hyper-V Switches to work.
bool WindowsFirewall::allowHyperVTraffic(uint8_t weight, const QString& title) {
bool WindowsFirewall::allowHyperVTraffic(uint8_t weight, const QString& title,
QList<uint64_t>& target) {
FWPM_FILTER_CONDITION0 cond;
// Condition: Request must be targeting the TUN interface
cond.fieldKey = FWPM_CONDITION_L2_FLAGS;
@@ -801,12 +821,12 @@ bool WindowsFirewall::allowHyperVTraffic(uint8_t weight, const QString& title) {
// #1 Permit Hyper-V => Hyper-V outbound.
filter.layerKey = FWPM_LAYER_OUTBOUND_MAC_FRAME_NATIVE;
if (!enableFilter(&filter, title, "Permit Hyper-V => Hyper-V outbound")) {
if (!enableFilter(&filter, title, "Permit Hyper-V => Hyper-V outbound", target)) {
return false;
}
// #2 Permit Hyper-V => Hyper-V inbound.
filter.layerKey = FWPM_LAYER_INBOUND_MAC_FRAME_NATIVE;
if (!enableFilter(&filter, title, "Permit Hyper-V => Hyper-V inbound")) {
if (!enableFilter(&filter, title, "Permit Hyper-V => Hyper-V inbound", target)) {
return false;
}
return true;
@@ -814,7 +834,7 @@ bool WindowsFirewall::allowHyperVTraffic(uint8_t weight, const QString& title) {
bool WindowsFirewall::blockTrafficTo(const IPAddress& addr, uint8_t weight,
const QString& title,
const QString& peer) {
QList<uint64_t>& target) {
QString description("Block traffic %1 %2 ");
auto lower = addr.address();
@@ -852,12 +872,12 @@ bool WindowsFirewall::blockTrafficTo(const IPAddress& addr, uint8_t weight,
filter.layerKey = layerKeyOut;
if (!enableFilter(&filter, title, description.arg("to").arg(addr.toString()),
peer)) {
target)) {
return false;
}
filter.layerKey = layerKeyIn;
if (!enableFilter(&filter, title,
description.arg("from").arg(addr.toString()), peer)) {
description.arg("from").arg(addr.toString()), target)) {
return false;
}
return true;
@@ -865,9 +885,9 @@ bool WindowsFirewall::blockTrafficTo(const IPAddress& addr, uint8_t weight,
bool WindowsFirewall::blockTrafficTo(const QList<IPAddress>& rangeList,
uint8_t weight, const QString& title,
const QString& peer) {
QList<uint64_t>& target) {
for (auto range : rangeList) {
if (!blockTrafficTo(range, weight, title, peer)) {
if (!blockTrafficTo(range, weight, title, target)) {
logger.info() << "Setting Range of" << range.toString() << "failed";
return false;
}
@@ -923,7 +943,8 @@ void WindowsFirewall::importAddress(const QHostAddress& addr,
}
bool WindowsFirewall::blockTrafficOnPort(uint port, uint8_t weight,
const QString& title) {
const QString& title,
QList<uint64_t>& target) {
// Allow Traffic to IP with PORT using any protocol
FWPM_FILTER_CONDITION0 conds[3];
conds[0].fieldKey = FWPM_CONDITION_IP_PROTOCOL;
@@ -953,20 +974,20 @@ bool WindowsFirewall::blockTrafficOnPort(uint port, uint8_t weight,
QString description("Block %1 on Port %2");
filter.layerKey = FWPM_LAYER_ALE_AUTH_CONNECT_V6;
if (!enableFilter(&filter, title, description.arg("outgoing v6").arg(port))) {
if (!enableFilter(&filter, title, description.arg("outgoing v6").arg(port), target)) {
return false;
}
filter.layerKey = FWPM_LAYER_ALE_AUTH_CONNECT_V4;
if (!enableFilter(&filter, title, description.arg("outgoing v4").arg(port))) {
if (!enableFilter(&filter, title, description.arg("outgoing v4").arg(port), target)) {
return false;
}
filter.layerKey = FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4;
if (!enableFilter(&filter, title, description.arg("incoming v4").arg(port))) {
if (!enableFilter(&filter, title, description.arg("incoming v4").arg(port), target)) {
return false;
}
filter.layerKey = FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V6;
if (!enableFilter(&filter, title, description.arg("incoming v6").arg(port))) {
if (!enableFilter(&filter, title, description.arg("incoming v6").arg(port), target)) {
return false;
}
return true;
@@ -974,7 +995,7 @@ bool WindowsFirewall::blockTrafficOnPort(uint port, uint8_t weight,
bool WindowsFirewall::enableFilter(FWPM_FILTER0* filter, const QString& title,
const QString& description,
const QString& peer) {
QList<uint64_t>& target) {
uint64_t filterID = 0;
auto name = title.toStdWString();
auto desc = description.toStdWString();
@@ -987,16 +1008,12 @@ bool WindowsFirewall::enableFilter(FWPM_FILTER0* filter, const QString& title,
return false;
}
logger.info() << "Filter added: " << title << ":" << description;
if (peer.isEmpty()) {
m_activeRules.append(filterID);
} else {
m_peerRules.insert(peer, filterID);
}
target.append(filterID);
return true;
}
bool WindowsFirewall::allowLoopbackTraffic(uint8_t weight,
const QString& title) {
bool WindowsFirewall::allowLoopbackTraffic(uint8_t weight, const QString& title,
QList<uint64_t>& target) {
QList<QNetworkInterface> networkInterfaces =
QNetworkInterface::allInterfaces();
for (const auto& iface : networkInterfaces) {
@@ -1004,7 +1021,7 @@ bool WindowsFirewall::allowLoopbackTraffic(uint8_t weight,
continue;
}
if (!allowTrafficOfAdapter(iface.index(), weight,
title.arg(iface.name()))) {
title.arg(iface.name()), target)) {
return false;
}
}
@@ -15,6 +15,7 @@
#include <QByteArray>
#include <QHostAddress>
#include <QMap>
#include <QObject>
#include <QString>
@@ -38,38 +39,42 @@ class WindowsFirewall final : public QObject {
static WindowsFirewall* create(QObject* parent);
~WindowsFirewall() override;
bool enableInterface(int vpnAdapterIndex);
bool enableInterface(int vpnAdapterIndex, const QString& ifname = QString());
bool enableLanBypass(const QList<IPAddress>& ranges);
bool enablePeerTraffic(const InterfaceConfig& config);
bool disablePeerTraffic(const QString& pubkey);
bool disableKillSwitch();
bool disableKillSwitchForTunnel(const QString& ifname);
bool allowAllTraffic();
bool allowTrafficRange(const QStringList& ranges);
bool allowTrafficRange(const QStringList& ranges, const QString& ifname = QString());
private:
static bool initSublayer();
WindowsFirewall(HANDLE session, QObject* parent);
HANDLE m_sessionHandle;
bool m_init = false;
QList<uint64_t> m_activeRules;
QMultiMap<QString, uint64_t> m_peerRules;
QList<uint64_t> m_globalRules;
QMap<QString, QList<uint64_t>> m_tunnelRules;
bool allowTrafficForAppOnAll(const QString& exePath, int weight,
const QString& title);
const QString& title, QList<uint64_t>& target);
bool blockTrafficTo(const QList<IPAddress>& range, uint8_t weight,
const QString& title, const QString& peer = QString());
const QString& title, QList<uint64_t>& target);
bool blockTrafficTo(const IPAddress& addr, uint8_t weight,
const QString& title, const QString& peer = QString());
bool blockTrafficOnPort(uint port, uint8_t weight, const QString& title);
const QString& title, QList<uint64_t>& target);
bool blockTrafficOnPort(uint port, uint8_t weight, const QString& title,
QList<uint64_t>& target);
bool allowTrafficTo(const IPAddress& addr, int weight, const QString& title,
const QString& peer = QString());
QList<uint64_t>& target);
bool allowTrafficTo(const QHostAddress& targetIP, uint port, int weight,
const QString& title, const QString& peer = QString());
const QString& title, QList<uint64_t>& target);
bool allowTrafficOfAdapter(int networkAdapter, uint8_t weight,
const QString& title);
bool allowDHCPTraffic(uint8_t weight, const QString& title);
bool allowHyperVTraffic(uint8_t weight, const QString& title);
bool allowLoopbackTraffic(uint8_t weight, const QString& title);
const QString& title, QList<uint64_t>& target);
bool allowDHCPTraffic(uint8_t weight, const QString& title,
QList<uint64_t>& target);
bool allowHyperVTraffic(uint8_t weight, const QString& title,
QList<uint64_t>& target);
bool allowLoopbackTraffic(uint8_t weight, const QString& title,
QList<uint64_t>& target);
// Utils
QString getCurrentPath();
@@ -78,8 +83,7 @@ class WindowsFirewall final : public QObject {
void importAddress(const QHostAddress& addr, OUT FWP_CONDITION_VALUE0_& value,
OUT QByteArray* v6DataBuffer);
bool enableFilter(FWPM_FILTER0* filter, const QString& title,
const QString& description,
const QString& peer = QString());
const QString& description, QList<uint64_t>& target);
};
#endif // WINDOWSFIREWALL_H
@@ -58,9 +58,12 @@ static int prefixcmp(const void* a, const void* b, size_t bits) {
return 0;
}
QSet<quint64> WindowsRouteMonitor::s_vpnLuids;
WindowsRouteMonitor::WindowsRouteMonitor(quint64 luid, QObject* parent)
: QObject(parent), m_luid(luid) {
MZ_COUNT_CTOR(WindowsRouteMonitor);
s_vpnLuids.insert(luid);
logger.debug() << "WindowsRouteMonitor created.";
NotifyRouteChange2(AF_INET, routeChangeCallback, this, FALSE, &m_routeHandle);
@@ -69,8 +72,8 @@ WindowsRouteMonitor::WindowsRouteMonitor(quint64 luid, QObject* parent)
WindowsRouteMonitor::~WindowsRouteMonitor() {
MZ_COUNT_DTOR(WindowsRouteMonitor);
CancelMibChangeNotify2(m_routeHandle);
s_vpnLuids.remove(m_luid);
flushRouteTable(m_exclusionRoutes);
flushRouteTable(m_clonedRoutes);
logger.debug() << "WindowsRouteMonitor destroyed.";
}
@@ -95,7 +98,8 @@ void WindowsRouteMonitor::updateInterfaceMetrics(int family) {
// Rebuild the list of interfaces that are valid for routing.
for (ULONG i = 0; i < table->NumEntries; i++) {
MIB_IPINTERFACE_ROW* row = &table->Table[i];
if (row->InterfaceLuid.Value == m_luid) {
// Skip any VPN wintun (own or sibling) so exclusion routes never pick one.
if (s_vpnLuids.contains(row->InterfaceLuid.Value)) {
continue;
}
if (!row->Connected) {
@@ -126,8 +130,8 @@ void WindowsRouteMonitor::updateExclusionRoute(MIB_IPFORWARD_ROW2* data,
nexthop.si_family = data->DestinationPrefix.Prefix.si_family;
for (ULONG i = 0; i < table->NumEntries; i++) {
MIB_IPFORWARD_ROW2* row = &table->Table[i];
// Ignore routes into the VPN interface.
if (row->InterfaceLuid.Value == m_luid) {
// Skip any VPN wintun (own or sibling).
if (s_vpnLuids.contains(row->InterfaceLuid.Value)) {
continue;
}
if (row->DestinationPrefix.PrefixLength < bestMatch) {
@@ -239,14 +243,16 @@ QHostAddress WindowsRouteMonitor::prefixToAddress(
}
}
bool WindowsRouteMonitor::isRouteExcluded(const IP_ADDRESS_PREFIX* dest) const {
auto i = m_exclusionRoutes.constBegin();
while (i != m_exclusionRoutes.constEnd()) {
const MIB_IPFORWARD_ROW2* row = i.value();
bool WindowsRouteMonitor::isRouteExcluded(void* ptable,
const IP_ADDRESS_PREFIX* dest) const {
PMIB_IPFORWARD_TABLE2 table = reinterpret_cast<PMIB_IPFORWARD_TABLE2>(ptable);
for (ULONG i = 0; i < table->NumEntries; i++) {
const MIB_IPFORWARD_ROW2* row = &table->Table[i];
if (row->Protocol != MIB_IPPROTO_NETMGMT) continue;
if (row->Metric != EXCLUSION_ROUTE_METRIC) continue;
if (routeContainsDest(&row->DestinationPrefix, dest)) {
return true;
}
i++;
}
return false;
}
@@ -272,8 +278,8 @@ void WindowsRouteMonitor::updateCapturedRoutes(int family, void* ptable) {
for (ULONG i = 0; i < table->NumEntries; i++) {
MIB_IPFORWARD_ROW2* row = &table->Table[i];
// Ignore routes into the VPN interface.
if (row->InterfaceLuid.Value == m_luid) {
// Skip any VPN wintun (own or sibling).
if (s_vpnLuids.contains(row->InterfaceLuid.Value)) {
continue;
}
// Ignore the default route
@@ -286,7 +292,7 @@ void WindowsRouteMonitor::updateCapturedRoutes(int family, void* ptable) {
continue;
}
// Ignore routes which should be excluded.
if (isRouteExcluded(&row->DestinationPrefix)) {
if (isRouteExcluded(table, &row->DestinationPrefix)) {
continue;
}
QHostAddress destination = prefixToAddress(&row->DestinationPrefix);
@@ -375,11 +381,6 @@ bool WindowsRouteMonitor::addExclusionRoute(const IPAddress& prefix) {
return true;
}
if (m_exclusionRoutes.contains(prefix)) {
logger.warning() << "Exclusion route already exists";
return false;
}
// Allocate and initialize the MIB routing table row.
MIB_IPFORWARD_ROW2* data = new MIB_IPFORWARD_ROW2;
InitializeIpForwardEntry(data);
@@ -427,8 +428,8 @@ bool WindowsRouteMonitor::addExclusionRoute(const IPAddress& prefix) {
updateCapturedRoutes(family, table);
updateExclusionRoute(data, table);
FreeMibTable(table);
m_exclusionRoutes[prefix] = data;
delete data;
m_ownedExclusionRoutes.insert(prefix);
return true;
}
@@ -436,23 +437,39 @@ bool WindowsRouteMonitor::deleteExclusionRoute(const IPAddress& prefix) {
logger.debug() << "Deleting exclusion route for"
<< prefix.address().toString();
MIB_IPFORWARD_ROW2* data = m_exclusionRoutes.take(prefix);
if (data == nullptr) {
return true;
m_ownedExclusionRoutes.remove(prefix);
PMIB_IPFORWARD_TABLE2 table;
DWORD result = GetIpForwardTable2(AF_UNSPEC, &table);
if (result != NO_ERROR) {
logger.error() << "Failed to fetch routing table:" << result;
return false;
}
DWORD result = DeleteIpForwardEntry2(data);
if ((result != ERROR_NOT_FOUND) && (result != NO_ERROR)) {
logger.error() << "Failed to delete route to"
<< prefix.toString()
<< "result:" << result;
const bool isV4 = prefix.address().protocol() == QAbstractSocket::IPv4Protocol;
const ADDRESS_FAMILY addrFamily =
isV4 ? static_cast<ADDRESS_FAMILY>(AF_INET)
: static_cast<ADDRESS_FAMILY>(AF_INET6);
bool deleted = false;
for (ULONG i = 0; i < table->NumEntries; i++) {
MIB_IPFORWARD_ROW2* row = &table->Table[i];
if (row->Protocol != MIB_IPPROTO_NETMGMT) continue;
if (row->Metric != EXCLUSION_ROUTE_METRIC) continue;
if (row->DestinationPrefix.Prefix.si_family != addrFamily) continue;
if (row->DestinationPrefix.PrefixLength != prefix.prefixLength()) continue;
if (prefixToAddress(&row->DestinationPrefix) != prefix.address()) continue;
DWORD r = DeleteIpForwardEntry2(row);
if (r == NO_ERROR || r == ERROR_NOT_FOUND) {
deleted = true;
} else {
logger.error() << "Failed to delete route to" << prefix.toString()
<< "result:" << r;
}
break;
}
// Captured routes might have changed.
updateCapturedRoutes(data->DestinationPrefix.Prefix.si_family);
delete data;
return true;
FreeMibTable(table);
updateCapturedRoutes(addrFamily);
return deleted;
}
void WindowsRouteMonitor::flushRouteTable(
@@ -492,8 +509,24 @@ void WindowsRouteMonitor::routeChanged() {
updateInterfaceMetrics(AF_UNSPEC);
updateCapturedRoutes(AF_UNSPEC, table);
for (MIB_IPFORWARD_ROW2* data : m_exclusionRoutes) {
updateExclusionRoute(data, table);
for (const IPAddress& prefix : m_ownedExclusionRoutes) {
const bool isV4 =
prefix.address().protocol() == QAbstractSocket::IPv4Protocol;
const ADDRESS_FAMILY addrFamily =
isV4 ? static_cast<ADDRESS_FAMILY>(AF_INET)
: static_cast<ADDRESS_FAMILY>(AF_INET6);
for (ULONG i = 0; i < table->NumEntries; i++) {
MIB_IPFORWARD_ROW2* row = &table->Table[i];
if (row->Protocol != MIB_IPPROTO_NETMGMT) continue;
if (row->Metric != EXCLUSION_ROUTE_METRIC) continue;
if (row->DestinationPrefix.Prefix.si_family != addrFamily) continue;
if (row->DestinationPrefix.PrefixLength != prefix.prefixLength()) continue;
if (prefixToAddress(&row->DestinationPrefix) != prefix.address()) continue;
MIB_IPFORWARD_ROW2 copy = *row;
updateExclusionRoute(&copy, table);
break;
}
}
FreeMibTable(table);
@@ -14,6 +14,7 @@
#include <QHash>
#include <QMap>
#include <QObject>
#include <QSet>
#include "ipaddress.h"
@@ -28,7 +29,6 @@ class WindowsRouteMonitor final : public QObject {
bool addExclusionRoute(const IPAddress& prefix);
bool deleteExclusionRoute(const IPAddress& prefix);
void flushExclusionRoutes() { return flushRouteTable(m_exclusionRoutes); };
quint64 getLuid() const { return m_luid; }
@@ -36,7 +36,7 @@ class WindowsRouteMonitor final : public QObject {
void routeChanged();
private:
bool isRouteExcluded(const IP_ADDRESS_PREFIX* dest) const;
bool isRouteExcluded(void* table, const IP_ADDRESS_PREFIX* dest) const;
static bool routeContainsDest(const IP_ADDRESS_PREFIX* route,
const IP_ADDRESS_PREFIX* dest);
static QHostAddress prefixToAddress(const IP_ADDRESS_PREFIX* dest);
@@ -47,7 +47,7 @@ class WindowsRouteMonitor final : public QObject {
void updateCapturedRoutes(int family);
void updateCapturedRoutes(int family, void* table);
QHash<IPAddress, MIB_IPFORWARD_ROW2*> m_exclusionRoutes;
QSet<IPAddress> m_ownedExclusionRoutes;
QMap<quint64, ULONG> m_interfaceMetricsIpv4;
QMap<quint64, ULONG> m_interfaceMetricsIpv6;
@@ -57,6 +57,8 @@ class WindowsRouteMonitor final : public QObject {
const quint64 m_luid = 0;
HANDLE m_routeHandle = INVALID_HANDLE_VALUE;
static QSet<quint64> s_vpnLuids;
};
#endif /* WINDOWSROUTEMONITOR_H */
@@ -15,9 +15,8 @@
#include "platforms/windows/windowsutils.h"
#include "windowsdaemon.h"
#define TUNNEL_NAMED_PIPE \
"\\\\." \
"\\pipe\\ProtectedPrefix\\Administrators\\AmneziaWG\\AmneziaVPN"
#define TUNNEL_NAMED_PIPE_PREFIX \
"\\\\.\\pipe\\ProtectedPrefix\\Administrators\\AmneziaWG\\"
constexpr uint32_t WINDOWS_TUNNEL_MONITOR_TIMEOUT_MSEC = 2000;
@@ -28,6 +27,10 @@ Logger logger("WindowsTunnelService");
static bool stopAndDeleteTunnelService(SC_HANDLE service);
static bool waitForServiceStatus(SC_HANDLE service, DWORD expectedStatus);
std::wstring WindowsTunnelService::serviceNameForIfname(const QString& ifname) {
return (QStringLiteral("AmneziaWGTunnel$") + ifname).toStdWString();
}
WindowsTunnelService::WindowsTunnelService(QObject* parent) : QObject(parent) {
MZ_COUNT_CTOR(WindowsTunnelService);
logger.debug() << "WindowsTunnelService created.";
@@ -37,7 +40,7 @@ WindowsTunnelService::WindowsTunnelService(QObject* parent) : QObject(parent) {
WindowsUtils::windowsLog("Failed to open SCManager");
}
// Is the service already running? Terminate it.
// Is the legacy single-tunnel service still around? Terminate it.
SC_HANDLE service =
OpenService((SC_HANDLE)m_scm, TUNNEL_SERVICE_NAME, SERVICE_ALL_ACCESS);
if (service != nullptr) {
@@ -108,8 +111,11 @@ void WindowsTunnelService::timeout() {
emit backendFailure();
}
bool WindowsTunnelService::start(const QString& configData) {
logger.debug() << "Starting the tunnel service";
bool WindowsTunnelService::start(const QString& configData, const QString& ifname) {
logger.debug() << "Starting the tunnel service for" << ifname;
m_ifname = ifname;
const std::wstring serviceName = serviceNameForIfname(ifname);
m_logworker = new WindowsTunnelLogger(WindowsCommons::tunnelLogFile());
m_logworker->moveToThread(&m_logthread);
@@ -128,10 +134,9 @@ bool WindowsTunnelService::start(const QString& configData) {
m_logworker = nullptr;
});
// Let's see if we have to delete a previous instance.
service = OpenService(scm, TUNNEL_SERVICE_NAME, SERVICE_ALL_ACCESS);
service = OpenService(scm, serviceName.c_str(), SERVICE_ALL_ACCESS);
if (service) {
logger.debug() << "An existing service has been detected. Let's close it.";
logger.debug() << "A stale service was detected. Cleaning it up.";
if (!stopAndDeleteTunnelService(service)) {
return false;
}
@@ -143,12 +148,12 @@ bool WindowsTunnelService::start(const QString& configData) {
{
QTextStream out(&serviceCmdline);
out << "\"" << qApp->applicationFilePath() << "\" tunneldaemon \""
<< configData << "\"";
<< configData << "\" \"" << ifname << "\"";
}
logger.debug() << "Service:" << qApp->applicationFilePath();
service = CreateService(scm, TUNNEL_SERVICE_NAME, L"Amnezia VPN (tunnel)",
service = CreateService(scm, serviceName.c_str(), L"Amnezia VPN (tunnel)",
SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS,
SERVICE_DEMAND_START, SERVICE_ERROR_NORMAL,
(const wchar_t*)serviceCmdline.utf16(), nullptr, 0,
@@ -236,8 +241,9 @@ static bool stopAndDeleteTunnelService(SC_HANDLE service) {
}
QString WindowsTunnelService::uapiCommand(const QString& command) {
// Create a pipe to the tunnel service.
LPTSTR tunnelName = (LPTSTR)TEXT(TUNNEL_NAMED_PIPE);
const std::wstring pipeName = std::wstring(TEXT(TUNNEL_NAMED_PIPE_PREFIX))
+ m_ifname.toStdWString();
LPCWSTR tunnelName = pipeName.c_str();
HANDLE pipe = CreateFile(tunnelName, GENERIC_READ | GENERIC_WRITE, 0, nullptr,
OPEN_EXISTING, 0, nullptr);
if (pipe == INVALID_HANDLE_VALUE) {
@@ -9,6 +9,7 @@
#include <QObject>
#include <QThread>
#include <QTimer>
#include <string>
#include "windowstunnellogger.h"
@@ -20,11 +21,13 @@ class WindowsTunnelService final : public QObject {
WindowsTunnelService(QObject* parent = nullptr);
~WindowsTunnelService();
bool start(const QString& configData);
bool start(const QString& configData, const QString& ifname);
void stop();
bool isRunning();
QString uapiCommand(const QString& command);
static std::wstring serviceNameForIfname(const QString& ifname);
signals:
void backendFailure();
@@ -36,6 +39,7 @@ class WindowsTunnelService final : public QObject {
QTimer m_timer;
QThread m_logthread;
WindowsTunnelLogger* m_logworker = nullptr;
QString m_ifname;
// These are really SC_HANDLEs in disguise.
void* m_scm = nullptr;
@@ -102,23 +102,36 @@ bool WireguardUtilsWindows::addInterface(const InterfaceConfig& config) {
return false;
}
// We don't want to pass a peer just yet, that will happen later with
// a UAPI command in WireguardUtilsWindows::updatePeer(), so truncate
// the config file to remove the [Peer] section.
qsizetype peerStart = configString.indexOf("[Peer]", 0, Qt::CaseSensitive);
if (peerStart >= 0) {
configString.truncate(peerStart);
}
if (!m_tunnel.start(configString)) {
auto stripLine = [&](const QString& key) {
qsizetype start = configString.startsWith(key + " = ")
? 0
: configString.indexOf("\n" + key + " = ");
if (start < 0) return;
if (start != 0) start += 1;
qsizetype end = configString.indexOf('\n', start);
if (end < 0) return;
configString.remove(start, end - start + 1);
};
stripLine("DNS");
if (config.m_deferAddressSetup) {
// Wintun rejects duplicate IPv4; daemon will assign at swap time.
stripLine("Address");
}
m_ifname = config.m_ifname.isEmpty() ? s_defaultInterfaceName() : config.m_ifname;
if (!m_tunnel.start(configString, m_ifname)) {
logger.error() << "Failed to activate the tunnel service";
return false;
}
// Determine the interface LUID
NET_LUID luid;
QString ifAlias = interfaceName();
DWORD result = ConvertInterfaceAliasToLuid((wchar_t*)ifAlias.utf16(), &luid);
DWORD result = ConvertInterfaceAliasToLuid((wchar_t*)m_ifname.utf16(), &luid);
if (result != 0) {
logger.error() << "Failed to lookup LUID:" << result;
return false;
@@ -126,14 +139,6 @@ bool WireguardUtilsWindows::addInterface(const InterfaceConfig& config) {
m_luid = luid.Value;
m_routeMonitor = new WindowsRouteMonitor(luid.Value, this);
if (config.m_killSwitchEnabled) {
// Enable the windows firewall
NET_IFINDEX ifindex;
ConvertInterfaceLuidToIndex(&luid, &ifindex);
m_firewall->allowAllTraffic();
m_firewall->enableInterface(ifindex);
}
logger.debug() << "Registration completed";
return true;
}
@@ -143,7 +148,6 @@ bool WireguardUtilsWindows::deleteInterface() {
m_routeMonitor->deleteLater();
}
m_firewall->disableKillSwitch();
m_tunnel.stop();
return true;
}
@@ -154,10 +158,6 @@ bool WireguardUtilsWindows::updatePeer(const InterfaceConfig& config) {
QByteArray pskKey =
QByteArray::fromBase64(qPrintable(config.m_serverPskKey));
if (config.m_killSwitchEnabled) {
// Enable the windows firewall for this peer.
m_firewall->enablePeerTraffic(config);
}
logger.debug() << "Configuring peer" << publicKey.toHex()
<< "via" << config.m_serverIpv4AddrIn;
@@ -185,12 +185,6 @@ bool WireguardUtilsWindows::updatePeer(const InterfaceConfig& config) {
out << "allowed_ip=" << ip.toString() << "\n";
}
// Exclude the server address, except for multihop exit servers.
if (m_routeMonitor && config.m_hopType != InterfaceConfig::MultiHopExit) {
m_routeMonitor->addExclusionRoute(IPAddress(config.m_serverIpv4AddrIn));
m_routeMonitor->addExclusionRoute(IPAddress(config.m_serverIpv6AddrIn));
}
QString reply = m_tunnel.uapiCommand(message);
logger.debug() << "DATA:" << reply;
return true;
@@ -200,15 +194,6 @@ bool WireguardUtilsWindows::deletePeer(const InterfaceConfig& config) {
QByteArray publicKey =
QByteArray::fromBase64(qPrintable(config.m_serverPublicKey));
// Clear exclustion routes for this peer.
if (m_routeMonitor && config.m_hopType != InterfaceConfig::MultiHopExit) {
m_routeMonitor->deleteExclusionRoute(IPAddress(config.m_serverIpv4AddrIn));
m_routeMonitor->deleteExclusionRoute(IPAddress(config.m_serverIpv6AddrIn));
}
// Disable the windows firewall for this peer.
m_firewall->disablePeerTraffic(config.m_serverPublicKey);
QString message;
QTextStream out(&message);
out << "set=1\n";
@@ -27,10 +27,8 @@ class WireguardUtilsWindows final : public WireguardUtils {
~WireguardUtilsWindows();
bool interfaceExists() override { return m_tunnel.isRunning(); }
QString interfaceName() override {
return WireguardUtilsWindows::s_interfaceName();
}
static const QString s_interfaceName() { return "AmneziaVPN"; }
QString interfaceName() override { return m_ifname; }
static const QString s_defaultInterfaceName() { return "AmneziaVPN"; }
bool addInterface(const InterfaceConfig& config) override;
bool deleteInterface() override;
@@ -54,6 +52,7 @@ class WireguardUtilsWindows final : public WireguardUtils {
void buildMibForwardRow(const IPAddress& prefix, void* row);
quint64 m_luid = 0;
QString m_ifname;
WindowsTunnelService m_tunnel;
QPointer<WindowsRouteMonitor> m_routeMonitor;
QPointer<WindowsFirewall> m_firewall;
@@ -418,7 +418,8 @@ bool SubscriptionUiController::updateServiceFromGateway(const QString &serverId,
{
bool isConnectEvent = newCountryCode.isEmpty() && newCountryName.isEmpty() && !reloadServiceConfig;
bool wasSubscriptionExpired = false;
if (const auto oldApiV2 = m_serversController->apiV2Config(serverId)) {
const auto oldApiV2 = m_serversController->apiV2Config(serverId);
if (oldApiV2) {
wasSubscriptionExpired = oldApiV2->apiConfig.subscriptionExpiredByServer
|| oldApiV2->apiConfig.isSubscriptionExpired();
}
@@ -426,6 +427,10 @@ bool SubscriptionUiController::updateServiceFromGateway(const QString &serverId,
ErrorCode errorCode = m_subscriptionController->updateServiceFromGateway(serverId, newCountryCode, isConnectEvent);
if (errorCode == ErrorCode::NoError) {
if (!newCountryCode.isEmpty() && oldApiV2) {
m_previousCountryServerId = serverId;
m_previousApiV2Config = oldApiV2;
}
if (wasSubscriptionExpired) {
emit subscriptionRefreshNeeded();
}
@@ -447,6 +452,20 @@ bool SubscriptionUiController::updateServiceFromGateway(const QString &serverId,
}
}
void SubscriptionUiController::revertLastCountryChange()
{
if (m_previousCountryServerId.isEmpty() || !m_previousApiV2Config) {
return;
}
const QString serverId = m_previousCountryServerId;
const ApiV2ServerConfig cfg = *m_previousApiV2Config;
m_previousCountryServerId.clear();
m_previousApiV2Config.reset();
m_subscriptionController->restoreApiV2Config(serverId, cfg);
m_apiCountryModel->updateModel(cfg.apiConfig.availableCountries,
cfg.apiConfig.serverCountryCode);
}
bool SubscriptionUiController::deactivateDevice(const QString &serverId)
{
@@ -50,6 +50,7 @@ public slots:
bool importTrialFromGateway(const QString &email);
bool updateServiceFromGateway(const QString &serverId, const QString &newCountryCode, const QString &newCountryName,
bool reloadServiceConfig = false);
void revertLastCountryChange();
bool deactivateDevice(const QString &serverId);
bool deactivateExternalDevice(const QString &serverId, const QString &uuid, const QString &serverCountryCode);
@@ -125,6 +126,9 @@ private:
ApiDevicesModel* m_apiDevicesModel;
SettingsController* m_settingsController;
ConnectionController* m_connectionController;
QString m_previousCountryServerId;
std::optional<ApiV2ServerConfig> m_previousApiV2Config;
};
#endif // SUBSCRIPTIONUICONTROLLER_H
@@ -19,6 +19,7 @@ ConnectionUiController::ConnectionUiController(ConnectionController* connectionC
m_serversController(serversController)
{
connect(m_connectionController, &ConnectionController::connectionStateChanged, this, &ConnectionUiController::onConnectionStateChanged);
connect(m_connectionController, &ConnectionController::serverSwitchFailed, this, &ConnectionUiController::serverSwitchFailed);
connect(this, &ConnectionUiController::connectButtonClicked, this, &ConnectionUiController::toggleConnection, Qt::QueuedConnection);
@@ -65,6 +66,12 @@ void ConnectionUiController::onConnectionStateChanged(Vpn::ConnectionState state
m_connectionStateText = tr("Connected");
break;
}
case Vpn::ConnectionState::Switching: {
m_isConnectionInProgress = true;
m_isConnected = true;
m_connectionStateText = tr("Switching...");
break;
}
case Vpn::ConnectionState::Connecting: {
m_isConnectionInProgress = true;
break;
@@ -52,6 +52,7 @@ signals:
void prepareConfig();
void unsupportedConnectDrawerRequested();
void noInstalledContainers();
void serverSwitchFailed();
private:
Vpn::ConnectionState getCurrentConnectionState();
+7 -4
View File
@@ -74,19 +74,22 @@ ListViewType {
: AmneziaStyle.color.mutedGray
checked: index === root.selectedIndex
checkable: !ConnectionController.isConnected
checkable: !ConnectionController.isConnectionInProgress
ButtonGroup.group: serversRadioButtonGroup
onClicked: {
if (ConnectionController.isConnected) {
PageController.showNotificationMessage(qsTr("Unable change server while there is an active connection"))
if (ConnectionController.isConnectionInProgress) {
PageController.showNotificationMessage(qsTr("Unable to change server while connection is in progress"))
return
}
root.selectedIndex = index
ServersUiController.setDefaultServerAtIndex(index)
if (ConnectionController.isConnected) {
ConnectionController.openConnection()
}
}
Keys.onEnterPressed: serverRadioButton.clicked()
@@ -36,7 +36,10 @@ PageType {
}
PageController.showBusyIndicator(true)
SubscriptionUiController.updateServiceFromGateway(ServersUiController.processedServerId, countryCode, countryName)
if (SubscriptionUiController.updateServiceFromGateway(ServersUiController.processedServerId, countryCode, countryName)
&& ConnectionController.isConnected) {
ConnectionController.openConnection()
}
PageController.showBusyIndicator(false)
}
@@ -202,15 +205,11 @@ PageType {
imageSource: "qrc:/images/controls/download.svg"
checked: index === ApiCountryModel.currentIndex
checkable: !ConnectionController.isConnected
checkable: !ConnectionController.isConnectionInProgress
onClicked: {
if (ConnectionController.isConnectionInProgress) {
PageController.showNotificationMessage(qsTr("Unable change server location while trying to make an active connection"))
return
}
if (ConnectionController.isConnected) {
PageController.showNotificationMessage(qsTr("Unable change server location while there is an active connection"))
PageController.showNotificationMessage(qsTr("Unable to change server location while connection is in progress"))
return
}
@@ -110,6 +110,7 @@ void SystemTrayNotificationHandler::setTrayState(Vpn::ConnectionState state)
m_trayActionDisconnect->setEnabled(true);
break;
case Vpn::ConnectionState::Connected:
case Vpn::ConnectionState::Switching:
setTrayIcon(QString(resourcesPath).arg(ConnectedTrayIconName));
m_trayActionConnect->setEnabled(false);
m_trayActionDisconnect->setEnabled(true);
+296 -205
View File
@@ -29,14 +29,13 @@
#include "platforms/ios/ios_controller.h"
#endif
#include "core/utils/constants/protocolConstants.h"
#include "core/utils/networkUtilities.h"
#include "core/utils/serverConfigUtils.h"
#include "vpnConnection.h"
using namespace ProtocolUtils;
VpnConnection::VpnConnection(SecureServersRepository* serversRepository, SecureAppSettingsRepository* appSettingsRepository, QObject *parent)
: QObject(parent), m_serversRepository(serversRepository), m_appSettingsRepository(appSettingsRepository), m_checkTimer(this)
: QObject(parent), m_serversRepository(serversRepository), m_appSettingsRepository(appSettingsRepository), m_checkTimer(this), m_trafficGuard(new VpnTrafficGuard(appSettingsRepository, this))
{
#if defined(Q_OS_IOS) || defined(MACOS_NE)
m_checkTimer.setInterval(1000);
@@ -70,112 +69,13 @@ void VpnConnection::onKillSwitchModeChanged(bool enabled)
void VpnConnection::onConnectionStateChanged(Vpn::ConnectionState state)
{
#ifdef AMNEZIA_DESKTOP
if (!m_serversRepository || !m_appSettingsRepository) {
qCritical() << "VpnConnection::onConnectionStateChanged: repositories not initialized";
return;
switch (state) {
case Vpn::ConnectionState::Connected: {
m_trafficGuard->setupRoutes(m_vpnConfiguration, vpnProtocol(), m_remoteAddress);
} break;
default:
break;
}
const QString defaultServerId = m_serversRepository->defaultServerId();
DockerContainer container = DockerContainer::None;
switch (m_serversRepository->serverKind(defaultServerId)) {
case serverConfigUtils::ConfigType::SelfHostedAdmin: {
const auto cfg = m_serversRepository->selfHostedAdminConfig(defaultServerId);
if (cfg.has_value()) {
container = cfg->defaultContainer;
}
break;
}
case serverConfigUtils::ConfigType::SelfHostedUser: {
const auto cfg = m_serversRepository->selfHostedUserConfig(defaultServerId);
if (cfg.has_value()) {
container = cfg->defaultContainer;
}
break;
}
case serverConfigUtils::ConfigType::Native: {
const auto cfg = m_serversRepository->nativeConfig(defaultServerId);
if (cfg.has_value()) {
container = cfg->defaultContainer;
}
break;
}
case serverConfigUtils::ConfigType::AmneziaPremiumV2:
case serverConfigUtils::ConfigType::AmneziaFreeV3:
case serverConfigUtils::ConfigType::ExternalPremium: {
const auto cfg = m_serversRepository->apiV2Config(defaultServerId);
if (cfg.has_value()) {
container = cfg->defaultContainer;
}
break;
}
case serverConfigUtils::ConfigType::AmneziaPremiumV1:
case serverConfigUtils::ConfigType::AmneziaFreeV2:
break;
case serverConfigUtils::ConfigType::Invalid:
default:
break;
}
IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
switch (state) {
case Vpn::ConnectionState::Connected: {
iface->resetIpStack();
auto flushDns = iface->flushDns();
if (flushDns.waitForFinished() && flushDns.returnValue())
qDebug() << "VpnConnection::onConnectionStateChanged: Successfully flushed DNS";
else
qWarning() << "VpnConnection::onConnectionStateChanged: Failed to flush DNS";
if (!ContainerUtils::isAwgContainer(container) && container != DockerContainer::WireGuard) {
QString dns1 = m_vpnConfiguration.value(configKey::dns1).toString();
QString dns2 = m_vpnConfiguration.value(configKey::dns2).toString();
#ifdef Q_OS_MACOS
if (!m_appSettingsRepository->isSitesSplitTunnelingEnabled() || m_appSettingsRepository->routeMode() != amnezia::RouteMode::VpnAllExceptSites) {
iface->routeAddList(m_vpnProtocol->vpnGateway(), QStringList() << dns1 << dns2);
}
#else
iface->routeAddList(m_vpnProtocol->vpnGateway(), QStringList() << dns1 << dns2);
#endif
if (m_appSettingsRepository->isSitesSplitTunnelingEnabled()) {
iface->routeDeleteList(m_vpnProtocol->vpnGateway(), QStringList() << "0.0.0.0");
RouteMode routeMode = m_appSettingsRepository->routeMode();
if (routeMode == amnezia::RouteMode::VpnOnlyForwardSites) {
QTimer::singleShot(1000, m_vpnProtocol.data(),
[this, routeMode]() { addSitesRoutes(m_vpnProtocol->vpnGateway(), routeMode); });
} else if (routeMode == amnezia::RouteMode::VpnAllExceptSites) {
iface->routeAddList(m_vpnProtocol->vpnGateway(), QStringList() << "0.0.0.0/1");
iface->routeAddList(m_vpnProtocol->vpnGateway(), QStringList() << "128.0.0.0/1");
iface->routeAddList(m_vpnProtocol->routeGateway(), QStringList() << remoteAddress());
#ifdef Q_OS_MACOS
iface->routeAddList(m_vpnProtocol->routeGateway(), QStringList() << dns1 << dns2);
#endif
addSitesRoutes(m_vpnProtocol->routeGateway(), routeMode);
}
}
}
} break;
case Vpn::ConnectionState::Disconnected:
case Vpn::ConnectionState::Error: {
auto flushDns = iface->flushDns();
if (flushDns.waitForFinished() && flushDns.returnValue())
qDebug() << "VpnConnection::onConnectionStateChanged: Successfully flushed DNS";
else
qWarning() << "VpnConnection::onConnectionStateChanged: Failed to flush DNS";
auto clearSavedRoutes = iface->clearSavedRoutes();
if (clearSavedRoutes.waitForFinished() && clearSavedRoutes.returnValue())
qDebug() << "VpnConnection::onConnectionStateChanged: Successfully cleared saved routes";
else
qWarning() << "VpnConnection::onConnectionStateChanged: Failed to clear saved routes";
} break;
default:
break;
}
});
#endif
#if defined(Q_OS_IOS) || defined(MACOS_NE)
@@ -189,82 +89,22 @@ void VpnConnection::onConnectionStateChanged(Vpn::ConnectionState state)
#endif
}
const QString &VpnConnection::remoteAddress() const
{
return m_remoteAddress;
}
void VpnConnection::setRepositories(SecureServersRepository* serversRepository, SecureAppSettingsRepository* appSettingsRepository)
{
m_serversRepository = serversRepository;
m_appSettingsRepository = appSettingsRepository;
}
void VpnConnection::addSitesRoutes(const QString &gw, amnezia::RouteMode mode)
{
#ifdef AMNEZIA_DESKTOP
if (!m_appSettingsRepository) {
qCritical() << "VpnConnection::addSitesRoutes: repositories not initialized";
return;
}
QStringList ips;
QStringList sites;
const QVariantMap &m = m_appSettingsRepository->vpnSites(mode);
for (auto i = m.constBegin(); i != m.constEnd(); ++i) {
if (NetworkUtilities::checkIpSubnetFormat(i.key())) {
ips.append(i.key());
} else {
if (NetworkUtilities::checkIpSubnetFormat(i.value().toString())) {
ips.append(i.value().toString());
}
sites.append(i.key());
}
}
ips.removeDuplicates();
IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
iface->routeAddList(gw, ips);
});
// re-resolve domains
for (const QString &site : sites) {
const auto &cbResolv = [this, site, gw, mode, ips](const QHostInfo &hostInfo) {
const QList<QHostAddress> &addresses = hostInfo.addresses();
QString ipv4Addr;
for (const QHostAddress &addr : hostInfo.addresses()) {
if (addr.protocol() == QAbstractSocket::NetworkLayerProtocol::IPv4Protocol) {
const QString &ip = addr.toString();
// qDebug() << "VpnConnection::addSitesRoutes updating site" << site << ip;
if (!ips.contains(ip)) {
IpcClient::withInterface([&gw, &ip](QSharedPointer<IpcInterfaceReplica> iface) {
iface->routeAddList(gw, QStringList() << ip);
});
m_appSettingsRepository->addVpnSite(mode, site, ip);
}
IpcClient::withInterface([](QSharedPointer<IpcInterfaceReplica> iface) {
auto reply = iface->flushDns();
if (reply.waitForFinished() || !reply.returnValue())
qWarning() << "VpnConnection::addSitesRoutes: Failed to flush DNS";
});
break;
}
}
};
QHostInfo::lookupHost(site, this, cbResolv);
}
#endif
m_trafficGuard.reset(new VpnTrafficGuard(appSettingsRepository, this));
}
QSharedPointer<VpnProtocol> VpnConnection::vpnProtocol() const
{
return m_vpnProtocol;
return m_active ? m_active->protocol() : m_vpnProtocol;
}
void VpnConnection::disconnectSlots()
{
if (m_vpnProtocol) {
m_vpnProtocol->disconnect();
if (auto proto = vpnProtocol()) {
proto->disconnect();
}
}
@@ -274,11 +114,8 @@ ErrorCode VpnConnection::lastError() const
return ErrorCode::AndroidError;
#endif
if (m_vpnProtocol.isNull()) {
return ErrorCode::InternalError;
}
return m_vpnProtocol.data()->lastError();
auto proto = vpnProtocol();
return proto.isNull() ? ErrorCode::InternalError : proto->lastError();
}
Vpn::ConnectionState VpnConnection::connectionState() const
@@ -286,6 +123,49 @@ Vpn::ConnectionState VpnConnection::connectionState() const
return m_connectionState;
}
QString VpnConnection::allocateIfname()
{
#ifdef Q_OS_MACOS
QString kernelAssigned;
IpcClient::withInterface([&kernelAssigned](QSharedPointer<IpcInterfaceReplica> iface) {
auto reply = iface->reserveUtunName();
if (reply.waitForFinished(2000)) {
kernelAssigned = reply.returnValue();
}
});
if (kernelAssigned.isEmpty() || m_ifnamesInUse.contains(kernelAssigned)) {
qCritical() << "allocateIfname: kernel utun reservation failed";
return QString();
}
m_ifnamesInUse.insert(kernelAssigned);
return kernelAssigned;
#else
for (int i = 0; ; ++i) {
const QString name = QStringLiteral("amn%1").arg(i);
if (!m_ifnamesInUse.contains(name)) {
m_ifnamesInUse.insert(name);
return name;
}
}
#endif
}
void VpnConnection::releaseIfname(const QString& ifname)
{
m_ifnamesInUse.remove(ifname);
}
void VpnConnection::wireTunnelSignals(Tunnel* tunnel, bool isActive)
{
connect(tunnel, &Tunnel::prepared, this, &VpnConnection::onTunnelPrepared);
connect(tunnel, &Tunnel::activated, this, &VpnConnection::onTunnelActivated);
connect(tunnel, &Tunnel::failed, this, &VpnConnection::onTunnelFailed);
if (isActive) {
connect(tunnel, &Tunnel::bytesChanged, this, &VpnConnection::onBytesChanged);
}
}
void VpnConnection::connectToVpn(const QString &serverId, DockerContainer container, const QJsonObject &vpnConfiguration)
{
if (!m_appSettingsRepository || !m_serversRepository) {
@@ -299,29 +179,96 @@ void VpnConnection::connectToVpn(const QString &serverId, DockerContainer contai
.arg(ContainerUtils::containerToString(container))
<< m_appSettingsRepository->routeMode();
m_remoteAddress = NetworkUtilities::getIPAddress(vpnConfiguration.value(configKey::hostName).toString());
setConnectionState(Vpn::ConnectionState::Connecting);
m_vpnConfiguration = vpnConfiguration;
const QString resolvedRemote =
NetworkUtilities::getIPAddress(vpnConfiguration.value(configKey::hostName).toString());
#ifdef AMNEZIA_DESKTOP
const bool isWg = VpnProtocol::isWireGuardBased(container);
const bool isXray = VpnProtocol::isXrayBased(container);
const bool useTunnelPath = isWg || isXray;
const QString preAllocatedIfname = useTunnelPath ? allocateIfname() : QString();
if (useTunnelPath && preAllocatedIfname.isEmpty()) {
setConnectionState(Vpn::ConnectionState::Error);
emit vpnProtocolError(ErrorCode::AmneziaServiceConnectionFailed);
return;
}
if (m_active
&& m_connectionState == Vpn::ConnectionState::Connected
&& useTunnelPath) {
if (!m_trafficGuard->allowEndpoint(resolvedRemote, preAllocatedIfname)) {
releaseIfname(preAllocatedIfname);
setConnectionState(Vpn::ConnectionState::Error);
emit vpnProtocolError(ErrorCode::AmneziaServiceConnectionFailed);
return;
}
startTunnelSwitch(container, vpnConfiguration, resolvedRemote, preAllocatedIfname);
return;
}
if (!m_trafficGuard->allowEndpoint(resolvedRemote, preAllocatedIfname)) {
if (useTunnelPath) releaseIfname(preAllocatedIfname);
setConnectionState(Vpn::ConnectionState::Error);
emit vpnProtocolError(ErrorCode::AmneziaServiceConnectionFailed);
return;
}
#endif
setConnectionState(Vpn::ConnectionState::Connecting);
QJsonObject config = vpnConfiguration;
#ifdef AMNEZIA_DESKTOP
if (m_active) {
const QString oldIfname = m_active->ifname();
m_trafficGuard->tearDown(m_active);
delete m_active;
m_active = nullptr;
releaseIfname(oldIfname);
}
if (m_vpnProtocol) {
disconnect(m_vpnProtocol.data(), &VpnProtocol::protocolError, this, &VpnConnection::vpnProtocolError);
m_trafficGuard->flushAll();
m_vpnProtocol->stop();
m_vpnProtocol.reset();
}
appendKillSwitchConfig();
#endif
appendSplitTunnelingConfig();
appendKillSwitchConfig(config);
appendSplitTunnelingConfig(config);
m_vpnConfiguration = config;
m_remoteAddress = resolvedRemote;
#ifdef AMNEZIA_DESKTOP
if (useTunnelPath) {
config.insert("ifname", preAllocatedIfname);
if (isXray) {
config.insert("tunName", preAllocatedIfname);
config.insert("deviceIpv4Address", amnezia::protocols::xray::defaultLocalAddr);
} else if (isWg) {
const QString protoName = config.value("protocol").toString();
const QJsonObject wgConfig = config.value(protoName + "_config_data").toObject();
const QString clientIp = wgConfig.value(amnezia::configKey::clientIp).toString();
if (!clientIp.isEmpty()) {
config.insert("deviceIpv4Address", clientIp);
}
}
m_vpnConfiguration = config;
m_active = new Tunnel(preAllocatedIfname, container, config, resolvedRemote, this);
wireTunnelSignals(m_active, /*isActive=*/true);
wireDaemonReconnectSignals();
m_trafficGuard->setConfig(config);
m_trafficGuard->bringUp(m_active);
return;
}
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
m_vpnProtocol.reset(VpnProtocol::factory(container, m_vpnConfiguration));
if (!m_vpnProtocol) {
setConnectionState(Vpn::ConnectionState::Error);
return;
}
m_vpnProtocol->prepare();
m_trafficGuard->setConfig(m_vpnConfiguration);
#elif defined Q_OS_ANDROID
androidVpnProtocol = createDefaultAndroidVpnProtocol();
createAndroidConnections();
@@ -347,27 +294,42 @@ void VpnConnection::createProtocolConnections()
connect(m_vpnProtocol.data(), &VpnProtocol::protocolError, this, &VpnConnection::vpnProtocolError);
connect(m_vpnProtocol.data(), &VpnProtocol::connectionStateChanged, this, &VpnConnection::setConnectionState);
connect(m_vpnProtocol.data(), SIGNAL(bytesChanged(quint64, quint64)), this, SLOT(onBytesChanged(quint64, quint64)));
connect(m_vpnProtocol.data(), &VpnProtocol::tunnelAddressesUpdated, this,
[this](const QString& gateway, const QString& localAddress) {
m_trafficGuard->applyKillSwitch(nullptr, gateway, localAddress);
});
wireDaemonReconnectSignals();
}
void VpnConnection::wireDaemonReconnectSignals()
{
#ifdef AMNEZIA_DESKTOP
IpcClient::withInterface([this](QSharedPointer<IpcInterfaceReplica> rep) {
connect(rep.data(), &IpcInterfaceReplica::networkChanged, this, &VpnConnection::reconnectToVpn, Qt::QueuedConnection);
connect(rep.data(), &IpcInterfaceReplica::wakeup, this, &VpnConnection::reconnectToVpn, Qt::QueuedConnection);
connect(rep.data(), &IpcInterfaceReplica::networkChanged, this, &VpnConnection::reconnectToVpn,
static_cast<Qt::ConnectionType>(Qt::QueuedConnection | Qt::UniqueConnection));
connect(rep.data(), &IpcInterfaceReplica::wakeup, this, &VpnConnection::reconnectToVpn,
static_cast<Qt::ConnectionType>(Qt::QueuedConnection | Qt::UniqueConnection));
});
#endif
}
void VpnConnection::appendKillSwitchConfig()
void VpnConnection::appendKillSwitchConfig(QJsonObject &config)
{
#ifdef AMNEZIA_DESKTOP
if (!m_appSettingsRepository) {
qCritical() << "VpnConnection::appendKillSwitchConfig: repositories not initialized";
return;
}
m_vpnConfiguration.insert(configKey::killSwitchOption, QVariant(m_appSettingsRepository->isKillSwitchEnabled()).toString());
m_vpnConfiguration.insert(configKey::allowedDnsServers, QVariant(m_appSettingsRepository->getAllowedDnsServers()).toJsonValue());
config.insert(configKey::killSwitchOption, QVariant(m_appSettingsRepository->isKillSwitchEnabled()).toString());
config.insert(configKey::allowedDnsServers, QVariant(m_appSettingsRepository->getAllowedDnsServers()).toJsonValue());
#else
Q_UNUSED(config)
#endif
}
void VpnConnection::appendSplitTunnelingConfig()
void VpnConnection::appendSplitTunnelingConfig(QJsonObject &config)
{
if (!m_appSettingsRepository) {
qCritical() << "VpnConnection::appendSplitTunnelingConfig: repositories not initialized";
@@ -377,14 +339,14 @@ void VpnConnection::appendSplitTunnelingConfig()
bool allowSiteBasedSplitTunneling = true;
// this block is for old native configs and for old self-hosted configs
auto protocolName = m_vpnConfiguration.value(configKey::vpnProto).toString();
auto protocolName = config.value(configKey::vpnProto).toString();
if (protocolName == ProtocolUtils::protoToString(Proto::Awg) || protocolName == ProtocolUtils::protoToString(Proto::WireGuard)) {
allowSiteBasedSplitTunneling = false;
auto configData = m_vpnConfiguration.value(protocolName + "_config_data").toObject();
auto configData = config.value(protocolName + "_config_data").toObject();
if (configData.value(configKey::allowedIps).isString()) {
QJsonArray allowedIpsJsonArray = QJsonArray::fromStringList(configData.value(configKey::allowedIps).toString().split(", "));
configData.insert(configKey::allowedIps, allowedIpsJsonArray);
m_vpnConfiguration.insert(protocolName + "_config_data", configData);
config.insert(protocolName + "_config_data", configData);
} else if (configData.value(configKey::allowedIps).isUndefined()) {
auto nativeConfig = configData.value(configKey::config).toString();
auto nativeConfigLines = nativeConfig.split("\n");
@@ -396,7 +358,7 @@ void VpnConnection::appendSplitTunnelingConfig()
}
QJsonArray allowedIpsJsonArray = QJsonArray::fromStringList(allowedIpsString.at(1).split(", "));
configData.insert(configKey::allowedIps, allowedIpsJsonArray);
m_vpnConfiguration.insert(protocolName + "_config_data", configData);
config.insert(protocolName + "_config_data", configData);
break;
}
}
@@ -412,7 +374,7 @@ void VpnConnection::appendSplitTunnelingConfig()
break;
}
configData.insert(configKey::persistentKeepAlive, persistentKeepaliveString.at(1));
m_vpnConfiguration.insert(protocolName + "_config_data", configData);
config.insert(protocolName + "_config_data", configData);
break;
}
}
@@ -448,14 +410,14 @@ void VpnConnection::appendSplitTunnelingConfig()
routeMode = amnezia::RouteMode::VpnAllSites;
} else if (routeMode == amnezia::RouteMode::VpnOnlyForwardSites) {
// Allow traffic to Amnezia DNS
sitesJsonArray.append(m_vpnConfiguration.value(configKey::dns1).toString());
sitesJsonArray.append(m_vpnConfiguration.value(configKey::dns2).toString());
sitesJsonArray.append(config.value(configKey::dns1).toString());
sitesJsonArray.append(config.value(configKey::dns2).toString());
}
}
}
m_vpnConfiguration.insert(configKey::splitTunnelType, routeMode);
m_vpnConfiguration.insert(configKey::splitTunnelSites, sitesJsonArray);
config.insert(configKey::splitTunnelType, routeMode);
config.insert(configKey::splitTunnelSites, sitesJsonArray);
amnezia::AppsRouteMode appsRouteMode = amnezia::AppsRouteMode::VpnAllApps;
QJsonArray appsJsonArray;
@@ -472,8 +434,8 @@ void VpnConnection::appendSplitTunnelingConfig()
}
}
m_vpnConfiguration.insert(configKey::appSplitTunnelType, appsRouteMode);
m_vpnConfiguration.insert(configKey::splitTunnelApps, appsJsonArray);
config.insert(configKey::appSplitTunnelType, appsRouteMode);
config.insert(configKey::splitTunnelApps, appsJsonArray);
qDebug() << QString("Site split tunneling is %1, route mode is %2")
.arg(m_appSettingsRepository->isSitesSplitTunnelingEnabled() ? "enabled" : "disabled")
@@ -515,9 +477,6 @@ QString VpnConnection::bytesPerSecToText(quint64 bytes)
}
void VpnConnection::reconnectToVpn() {
if (m_vpnProtocol.isNull())
return;
if (m_connectionState != Vpn::ConnectionState::Connected) {
qWarning() << QString("Reconnect triggered on %1 during inappropriate state: %2; ignoring slot")
.arg(QMetaEnum::fromType<Vpn::ConnectionState>().valueToKey(m_connectionState));
@@ -528,6 +487,16 @@ void VpnConnection::reconnectToVpn() {
setConnectionState(Vpn::ConnectionState::Reconnecting);
#ifdef AMNEZIA_DESKTOP
if (m_active) {
m_active->restart();
return;
}
#endif
if (m_vpnProtocol.isNull())
return;
m_vpnProtocol->stop();
if (ErrorCode err = m_vpnProtocol->start(); err != ErrorCode::NoError) {
setConnectionState(Vpn::ConnectionState::Error);
@@ -543,6 +512,26 @@ void VpnConnection::disconnectFromVpn()
disconnect(&m_checkTimer, &QTimer::timeout, IosController::Instance(), &IosController::checkStatus);
#endif
#ifdef AMNEZIA_DESKTOP
if (m_staging) {
m_trafficGuard->tearDown(m_staging);
releaseIfname(m_staging->ifname());
delete m_staging;
m_staging = nullptr;
}
if (m_active) {
setConnectionState(Vpn::ConnectionState::Disconnecting);
m_trafficGuard->tearDown(m_active);
m_trafficGuard->flushAll();
releaseIfname(m_active->ifname());
delete m_active;
m_active = nullptr;
setConnectionState(Vpn::ConnectionState::Disconnected);
return;
}
#endif
if (m_vpnProtocol.isNull()) {
setConnectionState(Vpn::ConnectionState::Disconnected);
return;
@@ -561,7 +550,9 @@ void VpnConnection::disconnectFromVpn()
}
});
#endif
#ifdef AMNEZIA_DESKTOP
m_trafficGuard->flushAll();
#endif
m_vpnProtocol->stop();
#if !defined(Q_OS_ANDROID) && !defined(AMNEZIA_DESKTOP)
@@ -580,3 +571,103 @@ void VpnConnection::setConnectionState(Vpn::ConnectionState state) {
m_connectionState = state;
emit connectionStateChanged(state);
}
void VpnConnection::startTunnelSwitch(DockerContainer container,
const QJsonObject &vpnConfiguration,
const QString &resolvedRemote,
const QString &stagingIfname)
{
QJsonObject config = vpnConfiguration;
config.insert("ifname", stagingIfname);
if (VpnProtocol::isXrayBased(container)) {
config.insert("tunName", stagingIfname);
config.insert("deviceIpv4Address", amnezia::protocols::xray::defaultLocalAddr);
} else if (VpnProtocol::isWireGuardBased(container)) {
const QString protoName = config.value("protocol").toString();
const QJsonObject wgConfig = config.value(protoName + "_config_data").toObject();
const QString clientIp = wgConfig.value(amnezia::configKey::clientIp).toString();
if (!clientIp.isEmpty()) {
config.insert("deviceIpv4Address", clientIp);
}
}
appendKillSwitchConfig(config);
appendSplitTunnelingConfig(config);
m_staging = new Tunnel(stagingIfname, container, config, resolvedRemote, this);
if (m_active) {
m_staging->setHandoverIfname(m_active->ifname());
}
wireTunnelSignals(m_staging, /*isActive=*/false);
setConnectionState(Vpn::ConnectionState::Switching);
m_trafficGuard->bringUp(m_staging);
}
void VpnConnection::onTunnelPrepared()
{
Tunnel* tunnel = qobject_cast<Tunnel*>(sender());
if (!tunnel) return;
if (tunnel == m_staging && m_active) {
Tunnel* oldTunnel = m_active;
const QString oldIfname = oldTunnel->ifname();
m_active = m_staging;
m_staging = nullptr;
connect(m_active, &Tunnel::bytesChanged, this, &VpnConnection::onBytesChanged);
m_vpnConfiguration = m_active->config();
m_remoteAddress = m_active->remoteAddress();
m_trafficGuard->setConfig(m_vpnConfiguration);
// Run the swap from a clean event-loop tick so the nested QEventLoop inside
// VpnTrafficGuard::swap does not deadlock the LSC.readData stack frame that
// delivered Tunnel::prepared.
QMetaObject::invokeMethod(this, [this, oldTunnel, oldIfname]() {
m_trafficGuard->swap(oldTunnel, m_active);
delete oldTunnel;
releaseIfname(oldIfname);
}, Qt::QueuedConnection);
return;
}
m_trafficGuard->commit(tunnel);
}
void VpnConnection::onTunnelActivated()
{
Tunnel* tunnel = qobject_cast<Tunnel*>(sender());
if (!tunnel) return;
if (tunnel == m_active) {
setConnectionState(Vpn::ConnectionState::Connected);
}
}
void VpnConnection::onTunnelFailed(amnezia::ErrorCode error)
{
Tunnel* tunnel = qobject_cast<Tunnel*>(sender());
if (!tunnel) return;
if (tunnel == m_staging) {
m_trafficGuard->release(m_staging);
m_staging->deactivate();
releaseIfname(m_staging->ifname());
m_staging->deleteLater();
m_staging = nullptr;
setConnectionState(Vpn::ConnectionState::Connected);
emit serverSwitchFailed();
return;
}
if (tunnel == m_active) {
m_trafficGuard->tearDown(m_active);
m_trafficGuard->flushAll();
releaseIfname(m_active->ifname());
m_active->deleteLater();
m_active = nullptr;
setConnectionState(Vpn::ConnectionState::Error);
if (error != ErrorCode::NoError) {
emit vpnProtocolError(error);
}
}
}
+27 -8
View File
@@ -3,6 +3,7 @@
#include <QObject>
#include <QMetaObject>
#include <QSet>
#include <QString>
#include <QScopedPointer>
#include <QRemoteObjectNode>
@@ -15,9 +16,8 @@
#include "core/repositories/secureServersRepository.h"
#include "core/repositories/secureAppSettingsRepository.h"
#ifdef AMNEZIA_DESKTOP
#include "core/utils/ipcClient.h"
#endif
#include "core/vpnTrafficGuard.h"
#include "core/tunnel.h"
#ifdef Q_OS_ANDROID
#include "core/protocols/androidVpnProtocol.h"
@@ -40,8 +40,7 @@ public:
QSharedPointer<VpnProtocol> vpnProtocol() const;
const QString &remoteAddress() const;
void addSitesRoutes(const QString &gw, amnezia::RouteMode mode);
const QString &remoteAddress() const { return m_remoteAddress; }
#ifdef Q_OS_ANDROID
void restoreConnection();
@@ -62,6 +61,7 @@ signals:
void bytesChanged(quint64 receivedBytes, quint64 sentBytes);
void connectionStateChanged(Vpn::ConnectionState state);
void vpnProtocolError(amnezia::ErrorCode error);
void serverSwitchFailed();
void serviceIsNotReady();
@@ -75,11 +75,15 @@ protected:
private:
SecureServersRepository* m_serversRepository;
SecureAppSettingsRepository* m_appSettingsRepository;
QScopedPointer<VpnTrafficGuard> m_trafficGuard;
QJsonObject m_vpnConfiguration;
QJsonObject m_routeMode;
QString m_remoteAddress;
Tunnel* m_active = nullptr;
Tunnel* m_staging = nullptr;
QSet<QString> m_ifnamesInUse;
// Only for iOS for now, check counters
QTimer m_checkTimer;
@@ -93,9 +97,24 @@ private:
Vpn::ConnectionState m_connectionState;
void createProtocolConnections();
void wireTunnelSignals(Tunnel* tunnel, bool isActive);
void wireDaemonReconnectSignals();
void appendSplitTunnelingConfig();
void appendKillSwitchConfig();
QString allocateIfname();
void releaseIfname(const QString& ifname);
void appendSplitTunnelingConfig(QJsonObject &config);
void appendKillSwitchConfig(QJsonObject &config);
void startTunnelSwitch(DockerContainer container,
const QJsonObject &vpnConfiguration,
const QString &resolvedRemote,
const QString &stagingIfname);
private slots:
void onTunnelPrepared();
void onTunnelActivated();
void onTunnelFailed(amnezia::ErrorCode error);
};
#endif // VPNCONNECTION_H
+19 -3
View File
@@ -10,8 +10,15 @@ class IpcInterface
// Route functions
SLOT( int routeAddList(const QString &gw, const QStringList &ips) );
SLOT( int routeAddListVia(const QString &ifname, const QString &gw, const QStringList &ips) );
SLOT( bool clearSavedRoutes() );
SLOT( bool routeDeleteList(const QString &gw, const QStringList &ip) );
SLOT( bool addExclusionRoute(const QString &ifname, const QString &addr) );
SLOT( bool delExclusionRoute(const QString &ifname, const QString &addr) );
SLOT( bool addAllowedIp(const QString &ifname, const QString &prefix) );
SLOT( bool delAllowedIp(const QString &ifname, const QString &prefix) );
SLOT( bool setTunnelResolvers(const QString &ifname, const QStringList &resolvers) );
SLOT( bool restoreTunnelResolvers() );
SLOT( bool flushDns() );
SLOT( void resetIpStack() );
@@ -25,21 +32,30 @@ class IpcInterface
SLOT( bool createTun(const QString &dev, const QString &subnet) );
SLOT( bool deleteTun(const QString &dev) );
SLOT( QString reserveUtunName() );
SLOT( bool applyAdapterAddress(const QString &ifname, const QString &ipv4, const QString &ipv6) );
SLOT( bool removeAdapterAddress(const QString &ifname, const QString &ipv4, const QString &ipv6) );
SLOT( bool StartRoutingIpv6() );
SLOT( bool StopRoutingIpv6() );
SLOT( bool disableKillSwitch() );
SLOT( bool disableKillSwitchForTunnel( const QString &ifname, const QStringList &remainingRanges ) );
SLOT( bool disableAllTraffic() );
SLOT( bool refreshKillSwitch( bool enabled ) );
SLOT( bool addKillSwitchAllowedRange( const QStringList ranges ) );
SLOT( bool addKillSwitchAllowedRange( const QString &ifname, const QStringList ranges ) );
SLOT( bool resetKillSwitchAllowedRange( const QStringList ranges ) );
SLOT( bool enablePeerTraffic( const QJsonObject &configStr) );
SLOT( bool enableKillSwitch( const QJsonObject &excludeAddr, int vpnAdapterIndex) );
SLOT( bool updateResolvers(const QString& ifname, const QList<QHostAddress>& resolvers) );
SLOT( bool restoreResolvers() );
SLOT(bool xrayStart(const QString &config));
SLOT(bool xrayStop());
SLOT(bool xrayStart(const QString &ifname, const QString &config));
SLOT(bool xrayStop(const QString &ifname));
SLOT(bool xrayAddUplinkRoutes(const QString &uplinkIface, const QString &uplinkGateway));
SLOT(bool xrayRemoveUplinkRoutes(const QString &uplinkIface, const QString &uplinkGateway));
SLOT( bool startNetworkCheck(const QString& serverIpv4Gateway, const QString& deviceIpv4Address) );
SLOT( bool stopNetworkCheck() );
+354 -13
View File
@@ -1,9 +1,12 @@
#include "ipcserver.h"
#include <QCoreApplication>
#include <QDateTime>
#include <QDebug>
#include <QEventLoop>
#include <QFileInfo>
#include <QHostAddress>
#include <QJsonDocument>
#include <QJsonObject>
#include <QLocalServer>
#include <QLocalSocket>
@@ -16,12 +19,33 @@
#include "logger.h"
#include "router.h"
#include "killswitch.h"
#include "xray.h"
#include "../client/daemon/daemon.h"
#ifdef Q_OS_MAC
#include "router_mac.h"
#include "core/utils/networkUtilities.h"
#include <QNetworkInterface>
#endif
#ifdef Q_OS_WIN
#include <winsock2.h>
#include <ws2tcpip.h>
#include <iphlpapi.h>
#include "tapcontroller_win.h"
#endif
#ifdef Q_OS_MAC
#include <sys/socket.h>
#include <sys/sys_domain.h>
#include <sys/kern_control.h>
#include <sys/ioctl.h>
#include <net/if.h>
#include <net/if_utun.h>
#include <unistd.h>
#include <cstring>
#endif
IpcServer::IpcServer(QObject *parent) : IpcInterfaceSource(parent)
{
@@ -73,6 +97,15 @@ int IpcServer::routeAddList(const QString &gw, const QStringList &ips)
return Router::routeAddList(gw, ips);
}
int IpcServer::routeAddListVia(const QString &ifname, const QString &gw, const QStringList &ips)
{
#ifdef MZ_DEBUG
qDebug() << "IpcServer::routeAddListVia" << ifname;
#endif
return Router::routeAddListVia(ifname, gw, ips);
}
bool IpcServer::clearSavedRoutes()
{
#ifdef MZ_DEBUG
@@ -91,6 +124,36 @@ bool IpcServer::routeDeleteList(const QString &gw, const QStringList &ips)
return Router::routeDeleteList(gw, ips);
}
bool IpcServer::addExclusionRoute(const QString &ifname, const QString &addr)
{
return Daemon::instance() && Daemon::instance()->addExclusionRoute(ifname, addr);
}
bool IpcServer::delExclusionRoute(const QString &ifname, const QString &addr)
{
return Daemon::instance() && Daemon::instance()->delExclusionRoute(ifname, addr);
}
bool IpcServer::addAllowedIp(const QString &ifname, const QString &prefix)
{
return Daemon::instance() && Daemon::instance()->addAllowedIp(ifname, prefix);
}
bool IpcServer::delAllowedIp(const QString &ifname, const QString &prefix)
{
return Daemon::instance() && Daemon::instance()->delAllowedIp(ifname, prefix);
}
bool IpcServer::setTunnelResolvers(const QString &ifname, const QStringList &resolvers)
{
return Daemon::instance() && Daemon::instance()->setTunnelResolvers(ifname, resolvers);
}
bool IpcServer::restoreTunnelResolvers()
{
return Daemon::instance() && Daemon::instance()->restoreTunnelResolvers();
}
bool IpcServer::flushDns()
{
#ifdef MZ_DEBUG
@@ -172,6 +235,126 @@ bool IpcServer::deleteTun(const QString &dev)
return Router::deleteTun(dev);
}
QString IpcServer::reserveUtunName()
{
#ifdef Q_OS_MAC
int fd = socket(PF_SYSTEM, SOCK_DGRAM, SYSPROTO_CONTROL);
if (fd < 0) {
qWarning() << "reserveUtunName: socket() failed:" << strerror(errno);
return QString();
}
struct ctl_info info;
std::memset(&info, 0, sizeof(info));
std::strncpy(info.ctl_name, UTUN_CONTROL_NAME, sizeof(info.ctl_name) - 1);
if (ioctl(fd, CTLIOCGINFO, &info) < 0) {
qWarning() << "reserveUtunName: CTLIOCGINFO failed:" << strerror(errno);
::close(fd);
return QString();
}
struct sockaddr_ctl addr;
std::memset(&addr, 0, sizeof(addr));
addr.sc_len = sizeof(addr);
addr.sc_family = AF_SYSTEM;
addr.ss_sysaddr = AF_SYS_CONTROL;
addr.sc_id = info.ctl_id;
addr.sc_unit = 0;
if (::connect(fd, reinterpret_cast<struct sockaddr*>(&addr), sizeof(addr)) < 0) {
qWarning() << "reserveUtunName: connect() failed:" << strerror(errno);
::close(fd);
return QString();
}
char ifname[IFNAMSIZ] = {0};
socklen_t len = sizeof(ifname);
if (getsockopt(fd, SYSPROTO_CONTROL, UTUN_OPT_IFNAME, ifname, &len) < 0) {
qWarning() << "reserveUtunName: getsockopt UTUN_OPT_IFNAME failed:" << strerror(errno);
::close(fd);
return QString();
}
::close(fd);
return QString::fromUtf8(ifname);
#else
return QString();
#endif
}
bool IpcServer::applyAdapterAddress(const QString &ifname, const QString &ipv4, const QString &ipv6)
{
#ifdef Q_OS_WIN
bool ok = true;
// Router::createTun on Windows assigns the address and blocks until it
// becomes live on the adapter (NotifyUnicastIpAddressChange callback).
if (!ipv4.isEmpty()) {
ok &= Router::createTun(ifname, ipv4);
}
if (!ipv6.isEmpty()) {
NET_LUID luid;
if (ConvertInterfaceAliasToLuid(reinterpret_cast<const wchar_t*>(ifname.utf16()), &luid) != NO_ERROR) {
qWarning() << "IpcServer::applyAdapterAddress: cannot resolve" << ifname;
return false;
}
const QByteArray ip = ipv6.section('/', 0, 0).toUtf8();
MIB_UNICASTIPADDRESS_ROW row;
InitializeUnicastIpAddressEntry(&row);
row.InterfaceLuid.Value = luid.Value;
row.Address.si_family = AF_INET6;
row.OnLinkPrefixLength = 128;
row.DadState = IpDadStatePreferred;
if (InetPtonA(AF_INET6, ip.toStdString().c_str(), &row.Address.Ipv6.sin6_addr) != 1) {
qWarning() << "IpcServer::applyAdapterAddress: cannot parse" << ipv6;
return false;
}
DWORD r = CreateUnicastIpAddressEntry(&row);
ok &= (r == NO_ERROR || r == ERROR_OBJECT_ALREADY_EXISTS);
}
return ok;
#else
Q_UNUSED(ifname)
Q_UNUSED(ipv4)
Q_UNUSED(ipv6)
return true;
#endif
}
bool IpcServer::removeAdapterAddress(const QString &ifname, const QString &ipv4, const QString &ipv6)
{
#ifdef Q_OS_WIN
NET_LUID luid;
if (ConvertInterfaceAliasToLuid(reinterpret_cast<const wchar_t*>(ifname.utf16()), &luid) != NO_ERROR) {
qWarning() << "IpcServer::removeAdapterAddress: cannot resolve" << ifname;
return false;
}
auto removeOne = [&](const QString& addr, int family) -> bool {
if (addr.isEmpty()) return true;
const QByteArray ip = addr.section('/', 0, 0).toUtf8();
MIB_UNICASTIPADDRESS_ROW row;
InitializeUnicastIpAddressEntry(&row);
row.InterfaceLuid.Value = luid.Value;
row.Address.si_family = static_cast<ADDRESS_FAMILY>(family);
void* dst = (family == AF_INET)
? static_cast<void*>(&row.Address.Ipv4.sin_addr)
: static_cast<void*>(&row.Address.Ipv6.sin6_addr);
if (InetPtonA(family, ip.toStdString().c_str(), dst) != 1) return false;
DWORD r = DeleteUnicastIpAddressEntry(&row);
return r == NO_ERROR || r == ERROR_NOT_FOUND;
};
bool ok = removeOne(ipv4, AF_INET);
ok &= removeOne(ipv6, AF_INET6);
return ok;
#else
Q_UNUSED(ifname)
Q_UNUSED(ipv4)
Q_UNUSED(ipv6)
return true;
#endif
}
bool IpcServer::updateResolvers(const QString &ifname, const QList<QHostAddress> &resolvers)
{
#ifdef MZ_DEBUG
@@ -187,6 +370,9 @@ bool IpcServer::restoreResolvers()
qDebug() << "IpcServer::restoreResolvers";
#endif
if (m_xrayWorkers.size() > 1) {
return true;
}
return Router::restoreResolvers();
}
@@ -250,13 +436,13 @@ bool IpcServer::resetKillSwitchAllowedRange(QStringList ranges)
return KillSwitch::instance()->resetAllowedRange(ranges);
}
bool IpcServer::addKillSwitchAllowedRange(QStringList ranges)
bool IpcServer::addKillSwitchAllowedRange(const QString &ifname, QStringList ranges)
{
#ifdef MZ_DEBUG
qDebug() << "IpcServer::addKillSwitchAllowedRange";
qDebug() << "IpcServer::addKillSwitchAllowedRange" << ifname;
#endif
return KillSwitch::instance()->addAllowedRange(ranges);
return KillSwitch::instance()->addAllowedRange(ifname, ranges);
}
bool IpcServer::disableAllTraffic()
@@ -286,6 +472,15 @@ bool IpcServer::disableKillSwitch()
return KillSwitch::instance()->disableKillSwitch();
}
bool IpcServer::disableKillSwitchForTunnel(const QString &ifname, const QStringList &remainingRanges)
{
#ifdef MZ_DEBUG
qDebug() << "IpcServer::disableKillSwitchForTunnel" << ifname;
#endif
return KillSwitch::instance()->disableKillSwitchForTunnel(ifname, remainingRanges);
}
bool IpcServer::enablePeerTraffic(const QJsonObject &configStr)
{
#ifdef MZ_DEBUG
@@ -304,20 +499,166 @@ bool IpcServer::refreshKillSwitch(bool enabled)
return KillSwitch::instance()->refresh(enabled);
}
bool IpcServer::xrayStart(const QString& cfg)
void IpcServer::onXrayWorkerLine(const QString& ifname, const QByteArray& line)
{
#ifdef MZ_DEBUG
qDebug() << "IpcServer::xrayStart";
#endif
return Xray::getInstance().startXray(cfg);
const QJsonObject ev = QJsonDocument::fromJson(line).object();
const QString name = ev.value("ev").toString();
if (name == "log") {
const QString prefix = QStringLiteral("[xray-worker:%1]").arg(ifname);
const QString level = ev.value("level").toString();
const QString msg = ev.value("msg").toString();
if (level == QLatin1String("warn")) {
qWarning().noquote() << prefix << msg;
} else if (level == QLatin1String("error") || level == QLatin1String("fatal")) {
qCritical().noquote() << prefix << msg;
} else if (level == QLatin1String("info")) {
qInfo().noquote() << prefix << msg;
} else {
qDebug().noquote() << prefix << msg;
}
} else if (name == "ready" || name == "failed") {
auto it = m_xrayWorkers.find(ifname);
if (it != m_xrayWorkers.end() && it->startLoop) {
it->startResult = (name == "ready");
it->startLoop->quit();
}
}
}
bool IpcServer::xrayStop()
bool IpcServer::xrayStart(const QString& ifname, const QString& cfg)
{
#ifdef MZ_DEBUG
qDebug() << "IpcServer::xrayStop";
qDebug() << "IpcServer::xrayStart" << ifname;
#endif
return Xray::getInstance().stopXray();
XrayWorker& w = m_xrayWorkers[ifname];
if (!w.process || w.process->state() == QProcess::NotRunning) {
w.process = QSharedPointer<QProcess>::create();
w.stdoutBuf.clear();
QObject::connect(w.process.data(), &QProcess::readyReadStandardOutput, this, [this, ifname]() {
auto it = m_xrayWorkers.find(ifname);
if (it == m_xrayWorkers.end() || !it->process) return;
it->stdoutBuf.append(it->process->readAllStandardOutput());
int nl;
while ((nl = it->stdoutBuf.indexOf('\n')) >= 0) {
const QByteArray line = it->stdoutBuf.left(nl);
it->stdoutBuf.remove(0, nl + 1);
onXrayWorkerLine(ifname, line);
}
});
QObject::connect(w.process.data(), &QProcess::errorOccurred, this,
[this, ifname](QProcess::ProcessError err) {
const QString prefix = QStringLiteral("[xray-worker:%1]").arg(ifname);
qCritical().noquote().nospace() << prefix << " process error: " << err;
auto it = m_xrayWorkers.find(ifname);
if (it != m_xrayWorkers.end() && it->startLoop) {
it->startResult = false;
it->startLoop->quit();
}
});
QObject::connect(w.process.data(),
QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
this, [this, ifname](int code, QProcess::ExitStatus status) {
const QString prefix = QStringLiteral("[xray-worker:%1]").arg(ifname);
qDebug().noquote().nospace() << prefix << " finished, code=" << code << " status=" << status;
auto it = m_xrayWorkers.find(ifname);
if (it != m_xrayWorkers.end() && it->startLoop) {
it->startResult = false;
it->startLoop->quit();
}
});
w.process->setProgram(QCoreApplication::applicationFilePath());
w.process->setArguments({QStringLiteral("--xray-worker")});
w.process->start();
if (!w.process->waitForStarted(5000)) {
qCritical().noquote() << QStringLiteral("[xray-worker:%1] failed to start").arg(ifname);
m_xrayWorkers.remove(ifname);
return false;
}
}
const QJsonObject startCmd{{QStringLiteral("op"), QStringLiteral("start")},
{QStringLiteral("config"), cfg}};
w.process->write(QJsonDocument(startCmd).toJson(QJsonDocument::Compact) + '\n');
QEventLoop loop;
w.startLoop = &loop;
w.startResult = false;
loop.exec();
auto it = m_xrayWorkers.find(ifname);
if (it == m_xrayWorkers.end()) {
return false;
}
it->startLoop.clear();
const bool ok = it->startResult;
if (!ok) {
m_xrayWorkers.remove(ifname);
}
return ok;
}
bool IpcServer::xrayStop(const QString& ifname)
{
#ifdef MZ_DEBUG
qDebug() << "IpcServer::xrayStop" << ifname;
#endif
auto it = m_xrayWorkers.find(ifname);
if (it == m_xrayWorkers.end()) {
return true;
}
if (it->process && it->process->state() != QProcess::NotRunning) {
const QJsonObject stopCmd{{QStringLiteral("op"), QStringLiteral("stop")}};
it->process->write(QJsonDocument(stopCmd).toJson(QJsonDocument::Compact) + '\n');
if (!it->process->waitForFinished(3000)) {
qWarning().noquote() << QStringLiteral("[xray-worker:%1] did not exit after stop, killing").arg(ifname);
it->process->kill();
it->process->waitForFinished(1000);
}
}
m_xrayWorkers.remove(ifname);
return true;
}
bool IpcServer::xrayAddUplinkRoutes(const QString& uplinkIface, const QString& uplinkGateway)
{
#ifdef Q_OS_MAC
if (uplinkIface.isEmpty() || uplinkGateway.isEmpty()) {
return false;
}
return RouterMac::Instance().routeAddXray(uplinkIface, uplinkGateway);
#else
Q_UNUSED(uplinkIface)
Q_UNUSED(uplinkGateway)
return true;
#endif
}
bool IpcServer::xrayRemoveUplinkRoutes(const QString& uplinkIface, const QString& uplinkGateway)
{
#ifdef Q_OS_MAC
if (uplinkIface.isEmpty()) {
return false;
}
if (m_xrayWorkers.size() > 1) {
return true;
}
return RouterMac::Instance().routeDeleteXray(uplinkIface, uplinkGateway);
#else
Q_UNUSED(uplinkIface)
Q_UNUSED(uplinkGateway)
return true;
#endif
}
+32 -3
View File
@@ -1,9 +1,14 @@
#ifndef IPCSERVER_H
#define IPCSERVER_H
#include <QByteArray>
#include <QEventLoop>
#include <QLocalServer>
#include <QObject>
#include <QPointer>
#include <QProcess>
#include <QRemoteObjectNode>
#include <QSharedPointer>
#include <QJsonObject>
#include "../client/daemon/interfaceconfig.h"
#include "../client/mozilla/pinghelper.h"
@@ -17,11 +22,19 @@ class IpcServer : public IpcInterfaceSource
{
public:
explicit IpcServer(QObject *parent = nullptr);
virtual int createPrivilegedProcess() override;
virtual int routeAddList(const QString &gw, const QStringList &ips) override;
virtual int routeAddListVia(const QString &ifname, const QString &gw, const QStringList &ips) override;
virtual bool clearSavedRoutes() override;
virtual bool routeDeleteList(const QString &gw, const QStringList &ips) override;
virtual bool addExclusionRoute(const QString &ifname, const QString &addr) override;
virtual bool delExclusionRoute(const QString &ifname, const QString &addr) override;
virtual bool addAllowedIp(const QString &ifname, const QString &prefix) override;
virtual bool delAllowedIp(const QString &ifname, const QString &prefix) override;
virtual bool setTunnelResolvers(const QString &ifname, const QStringList &resolvers) override;
virtual bool restoreTunnelResolvers() override;
virtual bool flushDns() override;
virtual void resetIpStack() override;
virtual bool checkAndInstallDriver() override;
@@ -31,19 +44,25 @@ public:
virtual void setLogsEnabled(bool enabled) override;
virtual bool createTun(const QString &dev, const QString &subnet) override;
virtual bool deleteTun(const QString &dev) override;
virtual QString reserveUtunName() override;
virtual bool applyAdapterAddress(const QString &ifname, const QString &ipv4, const QString &ipv6) override;
virtual bool removeAdapterAddress(const QString &ifname, const QString &ipv4, const QString &ipv6) override;
virtual bool StartRoutingIpv6() override;
virtual bool StopRoutingIpv6() override;
virtual bool disableAllTraffic() override;
virtual bool addKillSwitchAllowedRange(QStringList ranges) override;
virtual bool addKillSwitchAllowedRange(const QString &ifname, QStringList ranges) override;
virtual bool resetKillSwitchAllowedRange(QStringList ranges) override;
virtual bool enablePeerTraffic(const QJsonObject &configStr) override;
virtual bool enableKillSwitch(const QJsonObject &excludeAddr, int vpnAdapterIndex) override;
virtual bool disableKillSwitch() override;
virtual bool disableKillSwitchForTunnel(const QString &ifname, const QStringList &remainingRanges) override;
virtual bool refreshKillSwitch( bool enabled ) override;
virtual bool updateResolvers(const QString& ifname, const QList<QHostAddress>& resolvers) override;
virtual bool restoreResolvers() override;
virtual bool xrayStart(const QString& cfg) override;
virtual bool xrayStop() override;
virtual bool xrayStart(const QString& ifname, const QString& cfg) override;
virtual bool xrayStop(const QString& ifname) override;
virtual bool xrayAddUplinkRoutes(const QString& uplinkIface, const QString& uplinkGateway) override;
virtual bool xrayRemoveUplinkRoutes(const QString& uplinkIface, const QString& uplinkGateway) override;
virtual bool startNetworkCheck(const QString& serverIpv4Gateway, const QString& deviceIpv4Address) override;
virtual bool stopNetworkCheck() override;
@@ -64,6 +83,16 @@ private:
QMap<int, ProcessDescriptor> m_processes;
PingHelper m_pingHelper;
struct XrayWorker {
QSharedPointer<QProcess> process;
QByteArray stdoutBuf;
QPointer<QEventLoop> startLoop;
bool startResult = false;
};
QHash<QString, XrayWorker> m_xrayWorkers;
void onXrayWorkerLine(const QString& ifname, const QByteArray& line);
};
#endif // IPCSERVER_H
+72 -7
View File
@@ -14,6 +14,28 @@
#ifdef Q_OS_WIN
#include "../client/platforms/windows/daemon/windowsfirewall.h"
#include "../client/platforms/windows/daemon/windowsdaemon.h"
namespace {
int resolveVpnAdapterIndex(const QJsonObject& configStr) {
int index = configStr.value("vpnAdapterIndex").toInt();
if (index != 0) {
return index;
}
const QString ifname = configStr.value("ifname").toString();
if (ifname.isEmpty()) {
return 0;
}
NET_LUID luid;
if (ConvertInterfaceAliasToLuid((wchar_t*)ifname.utf16(), &luid) != 0) {
return 0;
}
NET_IFINDEX ifindex = 0;
if (ConvertInterfaceLuidToIndex(&luid, &ifindex) != 0) {
return 0;
}
return static_cast<int>(ifindex);
}
}
#endif
#ifdef Q_OS_LINUX
@@ -137,6 +159,16 @@ bool KillSwitch::disableKillSwitch() {
return true;
}
bool KillSwitch::disableKillSwitchForTunnel(const QString& ifname, const QStringList& remainingRanges) {
#ifdef Q_OS_WIN
Q_UNUSED(remainingRanges)
return WindowsFirewall::create(this)->disableKillSwitchForTunnel(ifname);
#else
Q_UNUSED(ifname)
return resetAllowedRange(remainingRanges);
#endif
}
bool KillSwitch::disableAllTraffic() {
#ifdef Q_OS_WIN
WindowsFirewall::create(this)->enableInterface(-1);
@@ -164,31 +196,50 @@ bool KillSwitch::disableAllTraffic() {
return true;
}
QStringList KillSwitch::combinedAllowNets() const {
QStringList result = m_allowedRanges;
for (const QString &site : m_splitTunnelAllows) {
if (!site.isEmpty() && !result.contains(site)) {
result.append(site);
}
}
return result;
}
bool KillSwitch::resetAllowedRange(const QStringList &ranges) {
m_allowedRanges = ranges;
const QStringList combined = combinedAllowNets();
#ifdef Q_OS_LINUX
LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("110.allowNets"), true);
LinuxFirewall::updateAllowNets(m_allowedRanges);
LinuxFirewall::updateAllowNets(combined);
#endif
#ifdef Q_OS_MACOS
MacOSFirewall::setAnchorEnabled(QStringLiteral("110.allowNets"), true);
MacOSFirewall::setAnchorTable(QStringLiteral("110.allowNets"), true, QStringLiteral("allownets"), m_allowedRanges);
MacOSFirewall::setAnchorTable(QStringLiteral("110.allowNets"), true, QStringLiteral("allownets"), combined);
#endif
#ifdef Q_OS_WIN
if (isStrictKillSwitchEnabled()) {
WindowsFirewall::create(this)->enableInterface(-1);
}
WindowsFirewall::create(this)->allowTrafficRange(m_allowedRanges);
WindowsFirewall::create(this)->allowTrafficRange(combined);
#endif
return true;
}
bool KillSwitch::addAllowedRange(const QStringList &ranges) {
bool KillSwitch::addAllowedRange(const QString &ifname, const QStringList &ranges) {
#ifdef Q_OS_WIN
if (!ifname.isEmpty()) {
return WindowsFirewall::create(this)->allowTrafficRange(ranges, ifname);
}
#else
Q_UNUSED(ifname)
#endif
for (const QString &range : ranges) {
if (!range.isEmpty() && !m_allowedRanges.contains(range)) {
m_allowedRanges.append(range);
@@ -209,10 +260,14 @@ bool KillSwitch::enablePeerTraffic(const QJsonObject &configStr) {
config.m_secondaryDnsServer = configStr.value(amnezia::configKey::dns2).toString();
}
config.m_serverPublicKey = "openvpn";
config.m_ifname = configStr.value("ifname").toString();
const QString protocolName = configStr.value(amnezia::configKey::vpnProto).toString();
const QString pubkey = configStr.value(protocolName + "_config_data").toObject()
.value(amnezia::configKey::serverPubKey).toString();
config.m_serverPublicKey = pubkey.isEmpty() ? QStringLiteral("openvpn") : pubkey;
config.m_serverIpv4Gateway = configStr.value("vpnGateway").toString();
config.m_serverIpv4AddrIn = configStr.value("vpnServer").toString();
int vpnAdapterIndex = configStr.value("vpnAdapterIndex").toInt();
int vpnAdapterIndex = resolveVpnAdapterIndex(configStr);
int inetAdapterIndex = configStr.value("inetAdapterIndex").toInt();
int splitTunnelType = configStr.value("splitTunnelType").toInt();
@@ -271,10 +326,13 @@ bool KillSwitch::enablePeerTraffic(const QJsonObject &configStr) {
bool KillSwitch::enableKillSwitch(const QJsonObject &configStr, int vpnAdapterIndex) {
#ifdef Q_OS_WIN
Q_UNUSED(vpnAdapterIndex)
const int resolvedIndex = resolveVpnAdapterIndex(configStr);
const QString ifname = configStr.value("ifname").toString();
if (configStr.value("splitTunnelType").toInt() != 0) {
WindowsFirewall::create(this)->allowAllTraffic();
}
return WindowsFirewall::create(this)->enableInterface(vpnAdapterIndex);
return WindowsFirewall::create(this)->enableInterface(resolvedIndex, ifname);
#endif
#if defined(Q_OS_LINUX) || defined(Q_OS_MACOS)
@@ -306,6 +364,13 @@ bool KillSwitch::enableKillSwitch(const QJsonObject &configStr, int vpnAdapterIn
allownets.append(v.toString());
}
}
m_splitTunnelAllows = allownets;
for (const QString &endpoint : m_allowedRanges) {
if (!endpoint.isEmpty() && !allownets.contains(endpoint)) {
allownets.append(endpoint);
}
}
#endif
#ifdef Q_OS_LINUX
+4 -1
View File
@@ -14,16 +14,19 @@ public:
bool init();
bool refresh(bool enabled);
bool disableKillSwitch();
bool disableKillSwitchForTunnel(const QString& ifname, const QStringList& remainingRanges);
bool disableAllTraffic();
bool enablePeerTraffic(const QJsonObject &configStr);
bool enableKillSwitch(const QJsonObject &configStr, int vpnAdapterIndex);
bool resetAllowedRange(const QStringList &ranges);
bool addAllowedRange(const QStringList &ranges);
bool addAllowedRange(const QString &ifname, const QStringList &ranges);
bool isStrictKillSwitchEnabled();
private:
KillSwitch(QObject* parent) {};
QStringList combinedAllowNets() const;
QStringList m_allowedRanges;
QStringList m_splitTunnelAllows;
QSharedPointer<SecureQSettings> m_appSettigns;
};
+10 -4
View File
@@ -56,14 +56,20 @@ LocalServer::LocalServer(QObject *parent) : QObject(parent),
#ifdef Q_OS_LINUX
// Signal handling for a proper shutdown.
QObject::connect(qApp, &QCoreApplication::aboutToQuit,
[]() { LinuxDaemon::instance()->deactivate(); });
QObject::connect(qApp, &QCoreApplication::aboutToQuit, [this]() {
LinuxDaemon::instance()->deactivate();
m_ipcServer.restoreTunnelResolvers();
m_ipcServer.disableKillSwitch();
});
#endif
#ifdef Q_OS_MAC
// Signal handling for a proper shutdown.
QObject::connect(qApp, &QCoreApplication::aboutToQuit,
[]() { MacOSDaemon::instance()->deactivate(); });
QObject::connect(qApp, &QCoreApplication::aboutToQuit, [this]() {
MacOSDaemon::instance()->deactivate();
m_ipcServer.restoreTunnelResolvers();
m_ipcServer.disableKillSwitch();
});
#endif
#ifdef Q_OS_WIN
+122
View File
@@ -1,21 +1,139 @@
#include <QCoreApplication>
#include <QDir>
#include <QJsonDocument>
#include <QJsonObject>
#include <cstdlib>
#include <cstring>
#include <thread>
#include "version.h"
#include "localserver.h"
#include "logger.h"
#include "systemservice.h"
#include "xray.h"
#include "core/utils/utilities.h"
#ifdef Q_OS_WIN
#include "platforms/windows/daemon/windowsdaemontunnel.h"
#include <windows.h>
namespace {
int s_argc = 0;
char** s_argv = nullptr;
} // namespace
#else
#include <unistd.h>
#endif
namespace {
constexpr const char* kXrayWorkerArg = "--xray-worker";
void writeWorkerEvent(const QJsonObject& obj)
{
const QByteArray bytes = QJsonDocument(obj).toJson(QJsonDocument::Compact) + '\n';
std::fwrite(bytes.constData(), 1, bytes.size(), stdout);
std::fflush(stdout);
}
void workerMessageHandler(QtMsgType type, const QMessageLogContext&, const QString& msg)
{
const char* level = "debug";
switch (type) {
case QtDebugMsg: level = "debug"; break;
case QtInfoMsg: level = "info"; break;
case QtWarningMsg: level = "warn"; break;
case QtCriticalMsg: level = "error"; break;
case QtFatalMsg: level = "fatal"; break;
}
writeWorkerEvent({{"ev", "log"}, {"level", QString::fromLatin1(level)}, {"msg", msg}});
}
int readStdinChunk(char* buf, int cap)
{
#ifdef Q_OS_WIN
DWORD n = 0;
BOOL ok = ReadFile(GetStdHandle(STD_INPUT_HANDLE), buf, cap, &n, nullptr);
return ok ? static_cast<int>(n) : -1;
#else
return static_cast<int>(::read(STDIN_FILENO, buf, cap));
#endif
}
int runXrayWorker(int argc, char** argv)
{
qInstallMessageHandler(workerMessageHandler);
QCoreApplication app(argc, argv);
auto* xray = new Xray;
auto* buf = new QByteArray;
auto exitWorker = [xray](int code) {
xray->stopXray();
std::fflush(stdout);
std::_Exit(code);
};
auto handleLine = [xray, exitWorker](const QByteArray& line) {
const QJsonObject cmd = QJsonDocument::fromJson(line).object();
const QString op = cmd.value("op").toString();
if (op == "start") {
const QString cfg = cmd.value("config").toString();
const bool ok = xray->startXray(cfg);
writeWorkerEvent({{"ev", ok ? "ready" : "failed"}});
} else if (op == "stop") {
writeWorkerEvent({{"ev", "stopped"}});
exitWorker(0);
}
};
auto onChunk = [buf, handleLine](const QByteArray& data) {
buf->append(data);
int nl;
while ((nl = buf->indexOf('\n')) >= 0) {
const QByteArray line = buf->left(nl);
buf->remove(0, nl + 1);
handleLine(line);
}
};
// Detached reader thread: stdin is an anonymous pipe (the parent's write end).
// QSocketNotifier doesn't work on anonymous pipes on Windows, so use a blocking
// read in a thread and dispatch events back to the main thread. On EOF the
// worker exits immediately via _Exit to avoid races with QCoreApplication
// teardown while this thread is still alive.
std::thread([onChunk, exitWorker]() {
char chunk[4096];
while (true) {
const int n = readStdinChunk(chunk, sizeof(chunk));
if (n <= 0) {
// Parent gone or pipe error: shut xray down and exit.
exitWorker(0);
return;
}
QMetaObject::invokeMethod(qApp, [onChunk, data = QByteArray(chunk, n)]() {
onChunk(data);
}, Qt::QueuedConnection);
}
}).detach();
return app.exec();
}
bool hasXrayWorkerArg(int argc, char** argv)
{
for (int i = 1; i < argc; ++i) {
if (std::strcmp(argv[i], kXrayWorkerArg) == 0) {
return true;
}
}
return false;
}
} // namespace
int runApplication(int argc, char** argv)
{
QCoreApplication app(argc,argv);
@@ -44,6 +162,10 @@ int runApplication(int argc, char** argv)
int main(int argc, char **argv)
{
if (hasXrayWorkerArg(argc, argv)) {
return runXrayWorker(argc, argv);
}
Utils::initializePath(Logger::systemLogDir());
if (argc >= 2) {
+10
View File
@@ -20,6 +20,16 @@ int Router::routeAddList(const QString &gw, const QStringList &ips)
#endif
}
int Router::routeAddListVia(const QString &ifname, const QString &gw, const QStringList &ips)
{
#ifdef Q_OS_LINUX
return RouterLinux::Instance().routeAddListVia(ifname, gw, ips);
#else
Q_UNUSED(ifname)
return routeAddList(gw, ips);
#endif
}
bool Router::clearSavedRoutes()
{
#ifdef Q_OS_WIN
+1
View File
@@ -17,6 +17,7 @@ class Router : public QObject
Q_OBJECT
public:
static int routeAddList(const QString &gw, const QStringList &ips);
static int routeAddListVia(const QString &ifname, const QString &gw, const QStringList &ips);
static bool clearSavedRoutes();
static int routeDeleteList(const QString &gw, const QStringList &ips);
static bool flushDns();
+46 -6
View File
@@ -3,6 +3,7 @@
#include <QProcess>
#include <QThread>
#include <core/utils/utilities.h>
#include <cerrno>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
@@ -57,15 +58,20 @@ bool RouterLinux::routeAdd(const QString &ipWithSubnet, const QString &gw, const
route.rt_flags = RTF_UP | RTF_GATEWAY;
route.rt_metric = 0;
if (int err = ioctl(sock, SIOCADDRT, &route) < 0)
if (ioctl(sock, SIOCADDRT, &route) < 0 && errno != EEXIST)
{
qDebug().noquote() << "route add error: gw "
<< ((struct sockaddr_in *)&route.rt_gateway)->sin_addr.s_addr
<< " ip " << ((struct sockaddr_in *)&route.rt_dst)->sin_addr.s_addr
<< " mask " << ((struct sockaddr_in *)&route.rt_genmask)->sin_addr.s_addr << " " << err;
qDebug().noquote() << "route add error: gw " << gw << " ip " << ip
<< " mask " << mask << " errno " << errno;
return false;
}
// EEXIST means the route is already in the kernel table (e.g. left over from a
// prior session). We still want to track it so it gets cleaned up on teardown.
for (const Route &r : m_addedRoutes) {
if (r.dst == ipWithSubnet && r.gw == gw) {
return true;
}
}
m_addedRoutes.append({ipWithSubnet, gw});
return true;
}
@@ -81,6 +87,34 @@ int RouterLinux::routeAddList(const QString &gw, const QStringList &ips)
return cnt;
}
int RouterLinux::routeAddListVia(const QString &ifname, const QString &gw, const QStringList &ips)
{
if (ifname.isEmpty()) {
qWarning() << "routeAddListVia: empty ifname";
return 0;
}
int cnt = 0;
for (const QString &ip : ips) {
QStringList args;
args << "route" << "replace" << ip;
if (!gw.isEmpty()) {
args << "via" << gw;
}
args << "dev" << ifname << "scope" << "link";
QProcess p;
p.setProcessChannelMode(QProcess::MergedChannels);
p.start("ip", args);
if (p.waitForFinished(2000) && p.exitCode() == 0) {
cnt++;
} else {
qWarning().noquote() << "routeAddListVia failed:" << ip << "via" << gw << "dev" << ifname
<< "rc=" << p.exitCode() << "out=" << p.readAll();
}
}
return cnt;
}
bool RouterLinux::clearSavedRoutes()
{
int temp_sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
@@ -135,7 +169,13 @@ bool RouterLinux::routeDelete(const QString &ipWithSubnet, const QString &gw, co
if (ioctl(sock, SIOCDELRT, &route) < 0)
{
qDebug().noquote() << "route delete error: gw " << gw << " ip " << ip << " mask " << mask;
// ESRCH means the route is already gone (e.g. kernel auto-removed it when
// the owning interface went away). The delete is a no-op, not a failure.
if (errno == ESRCH) {
return true;
}
qDebug().noquote() << "route delete error: gw " << gw << " ip " << ip
<< " mask " << mask << " errno " << errno;
return false;
}
return true;
+1
View File
@@ -26,6 +26,7 @@ public:
bool routeAdd(const QString &ip, const QString &gw, const int &sock);
int routeAddList(const QString &gw, const QStringList &ips);
int routeAddListVia(const QString &ifname, const QString &gw, const QStringList &ips);
bool clearSavedRoutes();
bool routeDelete(const QString &ip, const QString &gw, const int &sock);
bool routeDeleteList(const QString &gw, const QStringList &ips);
+2
View File
@@ -26,6 +26,8 @@ bool RouterMac::routeAdd(const QString &ipWithSubnet, const QString &gw)
return false;
}
routeDelete(ipWithSubnet, gw);
QString cmd;
if (mask == "255.255.255.255") {
cmd = QString("route add -host %1 %2").arg(ip).arg(gw);
+1 -25
View File
@@ -1,8 +1,5 @@
#include "xray.h"
#include "core/utils/networkUtilities.h"
#ifdef Q_OS_MAC
#include "router_mac.h"
#endif
#include <QDebug>
#include <QNetworkInterface>
@@ -34,9 +31,7 @@
bool Xray::startXray(const QString &cfg)
{
qDebug() << "Xray::startXray()";
const auto gatewayAndIface = NetworkUtilities::getGatewayAndIface();
const QString defaultGateway = gatewayAndIface.first;
const QNetworkInterface defaultIface = gatewayAndIface.second;
const QNetworkInterface defaultIface = NetworkUtilities::getGatewayAndIface().second;
#ifdef Q_OS_LINUX
m_defaultIfaceName = defaultIface.name().toUtf8();
#else
@@ -46,17 +41,6 @@ bool Xray::startXray(const QString &cfg)
qDebug() << "[xray] using uplink interface:" << defaultIface.name() << "(" << defaultIface.index() << ")";
}
#ifdef Q_OS_MAC
m_uplinkIfaceName = defaultIface.name();
m_uplinkGateway = defaultGateway;
if (!m_uplinkIfaceName.isEmpty()) {
const bool installed = RouterMac::Instance().routeAddXray(m_uplinkIfaceName, m_uplinkGateway);
if (!installed) {
qWarning() << "[xray] failed to install xray routes on" << m_uplinkIfaceName;
}
}
#endif
if (auto err = amnezia_xray_setsockcallback(ctxSockCallback, this); err != nullptr) {
qDebug() << "[xray] sockopt failed: " << err;
amnezia_xray_free(err);
@@ -91,14 +75,6 @@ bool Xray::stopXray()
success = false;
}
#ifdef Q_OS_MAC
if (!m_uplinkIfaceName.isEmpty()) {
RouterMac::Instance().routeDeleteXray(m_uplinkIfaceName, m_uplinkGateway);
}
m_uplinkIfaceName.clear();
m_uplinkGateway.clear();
#endif
return success;
}
-5
View File
@@ -31,11 +31,6 @@ private:
#else
int m_defaultIfaceIdx;
#endif
#ifdef Q_OS_MAC
QString m_uplinkIfaceName;
QString m_uplinkGateway;
#endif
};
#endif // XRAY_H