From 9d69ab89d53e218bd2dd81f5047399b4b44c6ed8 Mon Sep 17 00:00:00 2001 From: cd-amn Date: Mon, 18 May 2026 16:47:54 +0000 Subject: [PATCH] feat: introduce Tunnel wrapping VpnProtocol with two-phase lifecycle --- client/cmake/sources.cmake | 2 + client/core/protocols/vpnProtocol.cpp | 8 ++ client/core/protocols/vpnProtocol.h | 5 + client/core/protocols/wireGuardProtocol.cpp | 26 ++-- client/core/protocols/wireGuardProtocol.h | 5 +- client/core/tunnel.cpp | 150 ++++++++++++++++++++ client/core/tunnel.h | 77 ++++++++++ 7 files changed, 257 insertions(+), 16 deletions(-) create mode 100644 client/core/tunnel.cpp create mode 100644 client/core/tunnel.h diff --git a/client/cmake/sources.cmake b/client/cmake/sources.cmake index 45051c998..c0c3dcd87 100644 --- a/client/cmake/sources.cmake +++ b/client/cmake/sources.cmake @@ -66,6 +66,7 @@ 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/tunnel.h ) # Mozilla headres @@ -147,6 +148,7 @@ 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/tunnel.cpp ) # Mozilla sources diff --git a/client/core/protocols/vpnProtocol.cpp b/client/core/protocols/vpnProtocol.cpp index 82d8b6bef..10cf9ba25 100644 --- a/client/core/protocols/vpnProtocol.cpp +++ b/client/core/protocols/vpnProtocol.cpp @@ -106,6 +106,13 @@ 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; +} + VpnProtocol *VpnProtocol::factory(DockerContainer container, const QJsonObject &configuration) { switch (container) { @@ -137,6 +144,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"); diff --git a/client/core/protocols/vpnProtocol.h b/client/core/protocols/vpnProtocol.h index 68a79ec78..83868d563 100644 --- a/client/core/protocols/vpnProtocol.h +++ b/client/core/protocols/vpnProtocol.h @@ -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,7 @@ public: QString vpnLocalAddress() const; static VpnProtocol* factory(amnezia::DockerContainer container, const QJsonObject &configuration); + static bool isWireGuardBased(amnezia::DockerContainer container); signals: void bytesChanged(quint64 receivedBytes, quint64 sentBytes); @@ -78,6 +81,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? diff --git a/client/core/protocols/wireGuardProtocol.cpp b/client/core/protocols/wireGuardProtocol.cpp index 643f8045f..c67b07261 100644 --- a/client/core/protocols/wireGuardProtocol.cpp +++ b/client/core/protocols/wireGuardProtocol.cpp @@ -15,7 +15,8 @@ WireguardProtocol::WireguardProtocol(const QJsonObject &configuration, QObject * 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, @@ -42,6 +43,10 @@ WireguardProtocol::WireguardProtocol(const QJsonObject &configuration, QObject * 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); } @@ -51,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(); @@ -65,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); } diff --git a/client/core/protocols/wireGuardProtocol.h b/client/core/protocols/wireGuardProtocol.h index a6c502c56..a50a12a14 100644 --- a/client/core/protocols/wireGuardProtocol.h +++ b/client/core/protocols/wireGuardProtocol.h @@ -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 m_impl; }; diff --git a/client/core/tunnel.cpp b/client/core/tunnel.cpp new file mode 100644 index 000000000..3c994803c --- /dev/null +++ b/client/core/tunnel.cpp @@ -0,0 +1,150 @@ +#include "tunnel.h" + +#include + +#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); + } +} diff --git a/client/core/tunnel.h b/client/core/tunnel.h new file mode 100644 index 000000000..2eb0222be --- /dev/null +++ b/client/core/tunnel.h @@ -0,0 +1,77 @@ +#ifndef TUNNEL_H +#define TUNNEL_H + +#include +#include +#include +#include + +#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 protocol() const { return m_protocol; } + + 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; + amnezia::DockerContainer m_container; + QJsonObject m_config; + QSharedPointer m_protocol; + +private: + void onProtocolStateChanged(Vpn::ConnectionState state); + void onPrimaryReady(); + void onPrimaryFailed(); + + State m_state = State::Idle; + QTimer* m_deadline = nullptr; +}; + +#endif // TUNNEL_H