feat: drive WG via Tunnel coordinator path

This commit is contained in:
cd-amn
2026-05-15 15:07:42 +00:00
parent cc83170dd6
commit b5c0a0df45
10 changed files with 192 additions and 171 deletions

View File

@@ -65,7 +65,6 @@ set(HEADERS ${HEADERS}
${CLIENT_ROOT_DIR}/core/utils/managementServer.h
${CLIENT_ROOT_DIR}/core/utils/constants.h
${CLIENT_ROOT_DIR}/core/vpnTrafficGuard.h
${CLIENT_ROOT_DIR}/core/tunnelSession.h
${CLIENT_ROOT_DIR}/core/tunnel.h
)
@@ -145,7 +144,6 @@ set(SOURCES ${SOURCES}
${CLIENT_ROOT_DIR}/core/utils/utilities.cpp
${CLIENT_ROOT_DIR}/core/utils/managementServer.cpp
${CLIENT_ROOT_DIR}/core/vpnTrafficGuard.cpp
${CLIENT_ROOT_DIR}/core/tunnelSession.cpp
${CLIENT_ROOT_DIR}/core/tunnel.cpp
)

View File

@@ -61,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;

View File

@@ -92,6 +92,11 @@ ErrorCode WireguardProtocol::stopMzImpl()
return ErrorCode::NoError;
}
void WireguardProtocol::setPrimary(const QJsonObject& config)
{
m_impl->setPrimary(config);
}
void WireguardProtocol::activateStaging(const QJsonObject& config, const QString& stagingIfname)
{
m_impl->activateStaging(config, stagingIfname);

View File

@@ -21,6 +21,7 @@ public:
ErrorCode start() override;
void stop() override;
void setPrimary(const QJsonObject& config) override;
void activateStaging(const QJsonObject& config, const QString& stagingIfname) override;
void discardStaging() override;

View File

@@ -24,6 +24,7 @@ void Tunnel::prepare() {
setState(State::Preparing);
m_config.insert("ifname", m_ifname);
m_protocol.reset(VpnProtocol::factory(m_container, m_config));
if (!m_protocol) {
setState(State::Failed);
@@ -53,6 +54,9 @@ void Tunnel::commit() {
return;
}
setState(State::Committing);
if (m_protocol) {
m_protocol->setPrimary(m_config);
}
setState(State::Active);
emit activated();
}

View File

@@ -1,21 +0,0 @@
#include "tunnelSession.h"
TunnelSession::TunnelSession(const QJsonObject& config,
amnezia::DockerContainer container,
const QString& tunName,
const QString& remoteAddress,
QObject* parent)
: QObject(parent)
, m_config(config)
, m_container(container)
, m_tunName(tunName)
, m_remoteAddress(remoteAddress)
{}
void TunnelSession::confirmHandshake(const QString& pubkey) {
emit handshakeConfirmed(pubkey);
}
void TunnelSession::markFailed() {
emit failed();
}

View File

@@ -1,38 +0,0 @@
#ifndef TUNNELSESSION_H
#define TUNNELSESSION_H
#include <QObject>
#include <QJsonObject>
#include "core/utils/containerEnum.h"
class TunnelSession : public QObject {
Q_OBJECT
public:
explicit TunnelSession(const QJsonObject& config,
amnezia::DockerContainer container,
const QString& tunName,
const QString& remoteAddress,
QObject* parent = nullptr);
const QString& tunName() const { return m_tunName; }
const QString& remoteAddress() const { return m_remoteAddress; }
amnezia::DockerContainer container() const { return m_container; }
const QJsonObject& config() const { return m_config; }
public slots:
void confirmHandshake(const QString& pubkey);
void markFailed();
signals:
void handshakeConfirmed(const QString& pubkey);
void failed();
private:
QString m_tunName;
QString m_remoteAddress;
amnezia::DockerContainer m_container;
QJsonObject m_config;
};
#endif // TUNNELSESSION_H

View File

@@ -113,8 +113,7 @@ void DaemonLocalServerConnection::parseCommand(const QByteArray& data) {
return;
}
m_responseStyle[config.m_ifname] = ResponseStyle::LegacyActive;
if (!Daemon::instance()->activate(config.m_ifname, config) ||
!Daemon::instance()->setPrimary(config.m_ifname, config)) {
if (!Daemon::instance()->activate(config.m_ifname, config)) {
logger.error() << "Failed to activate the interface";
Daemon::instance()->deactivateTunnel(config.m_ifname);
m_responseStyle.remove(config.m_ifname);

View File

@@ -30,7 +30,6 @@
#endif
#include "core/utils/networkUtilities.h"
#include "daemon/wireguardutils.h"
using namespace ProtocolUtils;
@@ -71,7 +70,7 @@ void VpnConnection::onConnectionStateChanged(Vpn::ConnectionState state)
#ifdef AMNEZIA_DESKTOP
switch (state) {
case Vpn::ConnectionState::Connected: {
m_trafficGuard->setupRoutes(m_active->config(), m_vpnProtocol, m_active->remoteAddress());
m_trafficGuard->setupRoutes(m_vpnConfiguration, m_vpnProtocol, m_remoteAddress);
} break;
default:
break;
@@ -126,6 +125,35 @@ Vpn::ConnectionState VpnConnection::connectionState() const
return m_connectionState;
}
QString VpnConnection::allocateIfname()
{
for (int i = 0; ; ++i) {
const QString name = QStringLiteral("amn") + QString::number(i);
if (!m_ifnamesInUse.contains(name)) {
m_ifnamesInUse.insert(name);
return name;
}
}
}
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);
connect(tunnel, &Tunnel::addressesUpdated,
m_trafficGuard.data(), &VpnTrafficGuard::applyFirewall);
}
}
void VpnConnection::connectToVpn(int serverIndex, DockerContainer container, const QJsonObject &vpnConfiguration)
{
if (!m_appSettingsRepository || !m_serversRepository) {
@@ -143,16 +171,16 @@ void VpnConnection::connectToVpn(int serverIndex, DockerContainer container, con
NetworkUtilities::getIPAddress(vpnConfiguration.value(configKey::hostName).toString());
#ifdef AMNEZIA_DESKTOP
if (m_vpnProtocol != nullptr
&& m_connectionState == Vpn::ConnectionState::Connected
&& VpnProtocol::isWireGuardBased(container)
&& m_active && VpnProtocol::isWireGuardBased(m_active->container())) {
// Seamless WG -> WG switch path: already connected via Tunnel, new container is also WG.
if (m_active
&& m_connectionState == Vpn::ConnectionState::Connected
&& VpnProtocol::isWireGuardBased(container)) {
if (!m_trafficGuard->allowEndpoint(resolvedRemote)) {
setConnectionState(Vpn::ConnectionState::Error);
emit vpnProtocolError(ErrorCode::AmneziaServiceConnectionFailed);
return;
}
startStagingSwitch(container, vpnConfiguration);
startTunnelSwitch(container, vpnConfiguration, resolvedRemote);
return;
}
@@ -167,6 +195,13 @@ void VpnConnection::connectToVpn(int serverIndex, DockerContainer container, con
QJsonObject config = vpnConfiguration;
#ifdef AMNEZIA_DESKTOP
if (m_active) {
const QString oldIfname = m_active->ifname();
m_active->deactivate();
delete m_active;
m_active = nullptr;
releaseIfname(oldIfname);
}
if (m_vpnProtocol) {
disconnect(m_vpnProtocol.data(), &VpnProtocol::protocolError, this, &VpnConnection::vpnProtocolError);
m_trafficGuard->teardown();
@@ -178,17 +213,26 @@ void VpnConnection::connectToVpn(int serverIndex, DockerContainer container, con
appendSplitTunnelingConfig(config);
delete m_active;
m_active = new TunnelSession(config, container, WG_INTERFACE, resolvedRemote, this);
m_vpnConfiguration = config;
m_remoteAddress = resolvedRemote;
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
m_vpnProtocol.reset(VpnProtocol::factory(container, m_active->config()));
#ifdef AMNEZIA_DESKTOP
if (VpnProtocol::isWireGuardBased(container)) {
const QString ifname = allocateIfname();
m_active = new Tunnel(ifname, container, config, resolvedRemote, this);
wireTunnelSignals(m_active, /*isActive=*/true);
m_trafficGuard->setConfig(config);
m_active->prepare();
return;
}
m_vpnProtocol.reset(VpnProtocol::factory(container, m_vpnConfiguration));
if (!m_vpnProtocol) {
setConnectionState(Vpn::ConnectionState::Error);
return;
}
m_vpnProtocol->prepare();
m_trafficGuard->setConfig(m_active->config());
m_trafficGuard->setConfig(m_vpnConfiguration);
#elif defined Q_OS_ANDROID
androidVpnProtocol = createDefaultAndroidVpnProtocol();
createAndroidConnections();
@@ -196,7 +240,7 @@ void VpnConnection::connectToVpn(int serverIndex, DockerContainer container, con
m_vpnProtocol.reset(androidVpnProtocol);
#elif defined Q_OS_IOS || defined(MACOS_NE)
Proto proto = ContainerUtils::defaultProtocol(container);
IosController::Instance()->connectVpn(proto, m_active->config());
IosController::Instance()->connectVpn(proto, m_vpnConfiguration);
connect(&m_checkTimer, &QTimer::timeout, IosController::Instance(), &IosController::checkStatus);
return;
#endif
@@ -372,7 +416,7 @@ void VpnConnection::createAndroidConnections()
AndroidVpnProtocol *VpnConnection::createDefaultAndroidVpnProtocol()
{
return new AndroidVpnProtocol(m_active->config());
return new AndroidVpnProtocol(m_vpnConfiguration);
}
#endif
@@ -383,9 +427,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));
@@ -396,6 +437,25 @@ void VpnConnection::reconnectToVpn() {
setConnectionState(Vpn::ConnectionState::Reconnecting);
#ifdef AMNEZIA_DESKTOP
if (m_active) {
const QString ifname = m_active->ifname();
const DockerContainer container = m_active->container();
const QJsonObject config = m_active->config();
const QString remoteAddress = m_active->remoteAddress();
m_active->deactivate();
delete m_active;
m_active = new Tunnel(ifname, container, config, remoteAddress, this);
wireTunnelSignals(m_active, /*isActive=*/true);
m_active->prepare();
return;
}
#endif
if (m_vpnProtocol.isNull())
return;
m_vpnProtocol->stop();
if (ErrorCode err = m_vpnProtocol->start(); err != ErrorCode::NoError) {
setConnectionState(Vpn::ConnectionState::Error);
@@ -411,6 +471,29 @@ void VpnConnection::disconnectFromVpn()
disconnect(&m_checkTimer, &QTimer::timeout, IosController::Instance(), &IosController::checkStatus);
#endif
#ifdef AMNEZIA_DESKTOP
if (m_staging) {
m_trafficGuard->revokeEndpoint(m_staging->remoteAddress());
m_staging->deactivate();
releaseIfname(m_staging->ifname());
delete m_staging;
m_staging = nullptr;
}
if (m_active) {
setConnectionState(Vpn::ConnectionState::Disconnecting);
m_trafficGuard->teardown();
m_trafficGuard->revokeEndpoint(m_remoteAddress);
m_active->deactivate();
releaseIfname(m_active->ifname());
delete m_active;
m_active = nullptr;
m_vpnProtocol.reset();
setConnectionState(Vpn::ConnectionState::Disconnected);
return;
}
#endif
if (m_vpnProtocol.isNull()) {
setConnectionState(Vpn::ConnectionState::Disconnected);
return;
@@ -434,11 +517,6 @@ void VpnConnection::disconnectFromVpn()
#endif
m_vpnProtocol->stop();
delete m_active;
m_active = nullptr;
delete m_staging;
m_staging = nullptr;
#if !defined(Q_OS_ANDROID) && !defined(AMNEZIA_DESKTOP)
m_vpnProtocol->deleteLater();
#endif
@@ -456,103 +534,89 @@ void VpnConnection::setConnectionState(Vpn::ConnectionState state) {
emit connectionStateChanged(state);
}
void VpnConnection::startStagingSwitch(DockerContainer container,
const QJsonObject &vpnConfiguration)
void VpnConnection::startTunnelSwitch(DockerContainer container,
const QJsonObject &vpnConfiguration,
const QString &resolvedRemote)
{
disconnect(m_vpnProtocol.data(), &VpnProtocol::tunnelAddressesUpdated,
m_trafficGuard.data(), &VpnTrafficGuard::applyFirewall);
disconnect(m_vpnProtocol.data(), &VpnProtocol::connectionStateChanged,
this, &VpnConnection::setConnectionState);
const QString newRemoteAddress =
NetworkUtilities::getIPAddress(vpnConfiguration.value(configKey::hostName).toString());
const QString stagingIfname = (m_active->tunName() == QString(WG_INTERFACE))
? QString(WG_STAGING_INTERFACE)
: QString(WG_INTERFACE);
QJsonObject config = vpnConfiguration;
#ifdef AMNEZIA_DESKTOP
appendKillSwitchConfig(config);
#endif
appendSplitTunnelingConfig(config);
m_staging = new TunnelSession(config, container,
stagingIfname, newRemoteAddress, this);
connect(m_staging, &TunnelSession::handshakeConfirmed,
this, &VpnConnection::onStagingHandshakeConfirmed,
static_cast<Qt::ConnectionType>(Qt::QueuedConnection | Qt::UniqueConnection));
connect(m_staging, &TunnelSession::failed,
this, &VpnConnection::onStagingFailed,
static_cast<Qt::ConnectionType>(Qt::QueuedConnection | Qt::UniqueConnection));
connect(m_vpnProtocol.data(), &VpnProtocol::stagingConnected,
m_staging, &TunnelSession::confirmHandshake, Qt::UniqueConnection);
connect(m_vpnProtocol.data(), &VpnProtocol::stagingFailed,
m_staging, &TunnelSession::markFailed, Qt::UniqueConnection);
const QString stagingIfname = allocateIfname();
m_staging = new Tunnel(stagingIfname, container, config, resolvedRemote, this);
wireTunnelSignals(m_staging, /*isActive=*/false);
setConnectionState(Vpn::ConnectionState::Switching);
m_vpnProtocol->activateStaging(m_staging->config(), stagingIfname);
m_staging->prepare();
}
void VpnConnection::onStagingHandshakeConfirmed(const QString &pubkey)
void VpnConnection::onTunnelPrepared()
{
Q_UNUSED(pubkey);
Q_ASSERT(m_staging);
Tunnel* tunnel = qobject_cast<Tunnel*>(sender());
if (!tunnel) return;
tunnel->commit();
}
m_vpnProtocol->promoteStagingToActive(m_staging->config(), m_staging->tunName());
void VpnConnection::onTunnelActivated()
{
Tunnel* tunnel = qobject_cast<Tunnel*>(sender());
if (!tunnel) return;
m_trafficGuard->revokeEndpoint(m_active->remoteAddress());
if (tunnel == m_staging) {
// Make-before-break gate passed: new tunnel is primary, old still allowed by KS.
if (m_active) {
const QString oldRemote = m_active->remoteAddress();
const QString oldIfname = m_active->ifname();
m_active->deactivate();
delete m_active;
releaseIfname(oldIfname);
m_trafficGuard->revokeEndpoint(oldRemote);
}
disconnect(m_vpnProtocol.data(), &VpnProtocol::protocolError,
this, &VpnConnection::vpnProtocolError);
m_vpnProtocol->abandon();
m_vpnProtocol.reset();
delete m_active;
m_active = m_staging;
m_staging = nullptr;
m_vpnProtocol.reset(VpnProtocol::factory(m_active->container(), m_active->config()));
if (!m_vpnProtocol) {
setConnectionState(Vpn::ConnectionState::Error);
m_active = m_staging;
m_staging = nullptr;
m_vpnConfiguration = m_active->config();
m_remoteAddress = m_active->remoteAddress();
m_vpnProtocol = m_active->protocol();
m_trafficGuard->setConfig(m_vpnConfiguration);
setConnectionState(Vpn::ConnectionState::Connected);
return;
}
m_vpnProtocol->prepare();
m_vpnProtocol->assumeConnected();
m_trafficGuard->setConfig(m_active->config());
setConnectionState(Vpn::ConnectionState::Connected);
createProtocolConnections();
const QString proto = m_active->config().value("protocol").toString();
const QJsonObject vpnCfg = m_active->config().value(proto + "_config_data").toObject();
m_trafficGuard->applyFirewall(m_active->remoteAddress(), vpnCfg.value(configKey::clientIp).toString());
if (tunnel == m_active) {
m_vpnProtocol = m_active->protocol();
setConnectionState(Vpn::ConnectionState::Connected);
}
}
void VpnConnection::onStagingFailed()
void VpnConnection::onTunnelFailed(amnezia::ErrorCode error)
{
Q_ASSERT(m_staging);
Tunnel* tunnel = qobject_cast<Tunnel*>(sender());
if (!tunnel) return;
m_vpnProtocol->discardStaging();
if (tunnel == m_staging) {
m_trafficGuard->revokeEndpoint(m_staging->remoteAddress());
m_staging->deactivate();
releaseIfname(m_staging->ifname());
delete m_staging;
m_staging = nullptr;
setConnectionState(Vpn::ConnectionState::Connected);
emit serverSwitchFailed();
return;
}
m_trafficGuard->revokeEndpoint(m_staging->remoteAddress());
disconnect(m_vpnProtocol.data(), &VpnProtocol::stagingConnected,
m_staging, &TunnelSession::confirmHandshake);
disconnect(m_vpnProtocol.data(), &VpnProtocol::stagingFailed,
m_staging, &TunnelSession::markFailed);
delete m_staging;
m_staging = nullptr;
connect(m_vpnProtocol.data(), &VpnProtocol::connectionStateChanged,
this, &VpnConnection::setConnectionState, Qt::UniqueConnection);
connect(m_vpnProtocol.data(), &VpnProtocol::tunnelAddressesUpdated,
m_trafficGuard.data(), &VpnTrafficGuard::applyFirewall, Qt::UniqueConnection);
setConnectionState(Vpn::ConnectionState::Connected);
emit serverSwitchFailed();
if (tunnel == m_active) {
m_trafficGuard->teardown();
m_trafficGuard->revokeEndpoint(m_remoteAddress);
releaseIfname(m_active->ifname());
delete m_active;
m_active = nullptr;
m_vpnProtocol.reset();
setConnectionState(Vpn::ConnectionState::Error);
if (error != ErrorCode::NoError) {
emit vpnProtocolError(error);
}
}
}

View File

@@ -3,6 +3,7 @@
#include <QObject>
#include <QMetaObject>
#include <QSet>
#include <QString>
#include <QScopedPointer>
#include <QRemoteObjectNode>
@@ -16,7 +17,7 @@
#include "core/repositories/secureAppSettingsRepository.h"
#include "core/vpnTrafficGuard.h"
#include "core/tunnelSession.h"
#include "core/tunnel.h"
#ifdef Q_OS_ANDROID
#include "core/protocols/androidVpnProtocol.h"
@@ -39,10 +40,7 @@ public:
QSharedPointer<VpnProtocol> vpnProtocol() const;
const QString &remoteAddress() const {
static const QString empty;
return m_active ? m_active->remoteAddress() : empty;
}
const QString &remoteAddress() const { return m_remoteAddress; }
#ifdef Q_OS_ANDROID
void restoreConnection();
@@ -79,10 +77,13 @@ private:
SecureAppSettingsRepository* m_appSettingsRepository;
QScopedPointer<VpnTrafficGuard> m_trafficGuard;
QJsonObject m_vpnConfiguration;
QString m_remoteAddress;
QJsonObject m_routeMode;
TunnelSession* m_active = nullptr;
TunnelSession* m_staging = nullptr;
Tunnel* m_active = nullptr;
Tunnel* m_staging = nullptr;
QSet<QString> m_ifnamesInUse;
// Only for iOS for now, check counters
QTimer m_checkTimer;
@@ -97,15 +98,22 @@ private:
Vpn::ConnectionState m_connectionState;
void createProtocolConnections();
void wireTunnelSignals(Tunnel* tunnel, bool isActive);
QString allocateIfname();
void releaseIfname(const QString& ifname);
void appendSplitTunnelingConfig(QJsonObject &config);
void appendKillSwitchConfig(QJsonObject &config);
void startStagingSwitch(DockerContainer container, const QJsonObject &vpnConfiguration);
void startTunnelSwitch(DockerContainer container,
const QJsonObject &vpnConfiguration,
const QString &resolvedRemote);
private slots:
void onStagingHandshakeConfirmed(const QString &pubkey);
void onStagingFailed();
void onTunnelPrepared();
void onTunnelActivated();
void onTunnelFailed(amnezia::ErrorCode error);
};
#endif // VPNCONNECTION_H