From b5c0a0df45f404015b3e3535cb9ed521f3d996e6 Mon Sep 17 00:00:00 2001 From: cd-amn Date: Fri, 15 May 2026 15:07:42 +0000 Subject: [PATCH] feat: drive WG via Tunnel coordinator path --- client/cmake/sources.cmake | 2 - client/core/protocols/vpnProtocol.h | 1 + client/core/protocols/wireGuardProtocol.cpp | 5 + client/core/protocols/wireGuardProtocol.h | 1 + client/core/tunnel.cpp | 4 + client/core/tunnelSession.cpp | 21 -- client/core/tunnelSession.h | 38 --- client/daemon/daemonlocalserverconnection.cpp | 3 +- client/vpnConnection.cpp | 260 +++++++++++------- client/vpnConnection.h | 28 +- 10 files changed, 192 insertions(+), 171 deletions(-) delete mode 100644 client/core/tunnelSession.cpp delete mode 100644 client/core/tunnelSession.h diff --git a/client/cmake/sources.cmake b/client/cmake/sources.cmake index c856bbec3..05427df95 100644 --- a/client/cmake/sources.cmake +++ b/client/cmake/sources.cmake @@ -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 ) diff --git a/client/core/protocols/vpnProtocol.h b/client/core/protocols/vpnProtocol.h index 763e1cb77..fb4a9c450 100644 --- a/client/core/protocols/vpnProtocol.h +++ b/client/core/protocols/vpnProtocol.h @@ -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; diff --git a/client/core/protocols/wireGuardProtocol.cpp b/client/core/protocols/wireGuardProtocol.cpp index ec48b2b74..4e69de2b1 100644 --- a/client/core/protocols/wireGuardProtocol.cpp +++ b/client/core/protocols/wireGuardProtocol.cpp @@ -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); diff --git a/client/core/protocols/wireGuardProtocol.h b/client/core/protocols/wireGuardProtocol.h index 5abd30cb0..cf219bd77 100644 --- a/client/core/protocols/wireGuardProtocol.h +++ b/client/core/protocols/wireGuardProtocol.h @@ -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; diff --git a/client/core/tunnel.cpp b/client/core/tunnel.cpp index ab26f7879..955d9e79e 100644 --- a/client/core/tunnel.cpp +++ b/client/core/tunnel.cpp @@ -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(); } diff --git a/client/core/tunnelSession.cpp b/client/core/tunnelSession.cpp deleted file mode 100644 index 000bda5d5..000000000 --- a/client/core/tunnelSession.cpp +++ /dev/null @@ -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(); -} diff --git a/client/core/tunnelSession.h b/client/core/tunnelSession.h deleted file mode 100644 index 8c0aaad18..000000000 --- a/client/core/tunnelSession.h +++ /dev/null @@ -1,38 +0,0 @@ -#ifndef TUNNELSESSION_H -#define TUNNELSESSION_H - -#include -#include -#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 diff --git a/client/daemon/daemonlocalserverconnection.cpp b/client/daemon/daemonlocalserverconnection.cpp index c0575db48..bdae04d46 100644 --- a/client/daemon/daemonlocalserverconnection.cpp +++ b/client/daemon/daemonlocalserverconnection.cpp @@ -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); diff --git a/client/vpnConnection.cpp b/client/vpnConnection.cpp index f45102d5c..db48244de 100644 --- a/client/vpnConnection.cpp +++ b/client/vpnConnection.cpp @@ -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().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::QueuedConnection | Qt::UniqueConnection)); - connect(m_staging, &TunnelSession::failed, - this, &VpnConnection::onStagingFailed, - static_cast(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(sender()); + if (!tunnel) return; + tunnel->commit(); +} - m_vpnProtocol->promoteStagingToActive(m_staging->config(), m_staging->tunName()); +void VpnConnection::onTunnelActivated() +{ + Tunnel* tunnel = qobject_cast(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(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); + } + } } diff --git a/client/vpnConnection.h b/client/vpnConnection.h index 9ae365d62..759e79059 100644 --- a/client/vpnConnection.h +++ b/client/vpnConnection.h @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -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() 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 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 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