Compare commits

...

14 Commits

Author SHA1 Message Date
Pokamest Nikak
aab275cacd Fix for linux sleep reconnect 2026-08-21 14:17:55 +01:00
NickVs2015
b66e3def09 fix: resolve deadlock filewrite and remove qml ApplicationActive event 2026-08-18 09:49:33 +03:00
NickVs2015
39a652df5b fix: QRingBuffer corruption under concurrent logging 2026-08-18 09:49:27 +03:00
NickVs2015
542ae60d27 fix: add log systemWoke received 2026-08-18 09:44:09 +03:00
NickVs2015
87d90ee92f fix: recover from stuck busy-indicator/disabled-controls on real OS wakeup (Windows/Linux) 2026-08-18 09:44:04 +03:00
NickVs2015
d906df6927 fix: prevent double WireGuard stop and fix NM sleep/wake 2026-08-18 09:43:53 +03:00
NickVs2015
9a472c6793 fix: bad rule xargs, cgroup v2 mkdir, amnvpnrt rt_tables, restart flush 2026-08-06 19:33:37 +03:00
NickVs2015
9bf974a2f4 fix: amnvpnrt flush error 2026-08-06 16:42:19 +03:00
NickVs2015
46100fd311 fix: make Linux DNS retry setup 2026-08-06 15:16:29 +03:00
NickVs2015
34b62f8fd6 fix: replace m_linkDefaultRoutes map with single m_gatewayIfindex
fix: solve infinite reconnect NM down
2026-08-06 14:46:01 +03:00
NickVs2015
b8e9ec8570 fix: killswitch compatible 2026-08-06 14:01:24 +03:00
NickVs2015
c35988d79f fix: NM down/up reconnection problem 2026-08-06 14:01:24 +03:00
NickVs2015
410538c6b2 fix: dns rewrites and flush 2026-08-06 14:01:24 +03:00
NickVs2015
cc71330c27 fix: make dbus async 2026-08-06 14:01:24 +03:00
26 changed files with 568 additions and 67 deletions

View File

@@ -29,6 +29,7 @@ ConnectionController::ConnectionController(SecureServersRepository* serversRepos
m_vpnConnection(vpnConnection)
{
connect(m_vpnConnection, &VpnConnection::connectionStateChanged, this, &ConnectionController::connectionStateChanged);
connect(m_vpnConnection, &VpnConnection::systemWoke, this, &ConnectionController::systemWoke);
connect(this, &ConnectionController::openConnectionRequested, m_vpnConnection, &VpnConnection::connectToVpn, Qt::QueuedConnection);
connect(this, &ConnectionController::closeConnectionRequested, m_vpnConnection, &VpnConnection::disconnectFromVpn, Qt::QueuedConnection);
connect(this, &ConnectionController::killSwitchModeChangedRequested, m_vpnConnection, &VpnConnection::onKillSwitchModeChanged, Qt::QueuedConnection);

View File

@@ -65,6 +65,7 @@ public:
signals:
void connectionStateChanged(Vpn::ConnectionState state);
void systemWoke();
void openConnectionRequested(const QString &serverId, DockerContainer container, const QJsonObject &vpnConfiguration);
void closeConnectionRequested();
void killSwitchModeChangedRequested(bool enabled);

View File

@@ -35,6 +35,11 @@ void VpnProtocol::setLastError(ErrorCode lastError)
qCritical().noquote() << "VpnProtocol error, code" << m_lastError << errorString(m_lastError);
}
void VpnProtocol::recordLastError(ErrorCode lastError)
{
m_lastError = lastError;
}
ErrorCode VpnProtocol::lastError() const
{
return m_lastError;

View File

@@ -89,6 +89,10 @@ protected:
void startTimeoutTimer();
void stopTimeoutTimer();
// Remembers the error for lastError() without forcing the Error connection
// state, unlike setLastError().
void recordLastError(ErrorCode lastError);
Vpn::ConnectionState m_connectionState;
QString m_routeGateway;
@@ -99,7 +103,7 @@ protected:
private:
QTimer* m_timeoutTimer;
ErrorCode m_lastError;
ErrorCode m_lastError = ErrorCode::NoError;
quint64 m_receivedBytes;
quint64 m_sentBytes;
};

View File

@@ -1,4 +1,5 @@
#include <QCoreApplication>
#include <QDebug>
#include <QFileInfo>
#include <QProcess>
#include <QTcpSocket>
@@ -50,19 +51,39 @@ WireguardProtocol::~WireguardProtocol()
void WireguardProtocol::stop()
{
if (m_stopped) {
return;
}
m_stopped = true;
stopMzImpl();
return;
}
ErrorCode WireguardProtocol::startMzImpl()
{
QString protocolName = m_rawConfig.value("protocol").toString();
QJsonObject vpnConfigData = m_rawConfig.value(protocolName + "_config_data").toObject();
vpnConfigData[configKey::hostName] = NetworkUtilities::getIPAddress(vpnConfigData.value(configKey::hostName).toString());
m_rawConfig.insert(protocolName + "_config_data", vpnConfigData);
m_rawConfig[configKey::hostName] = NetworkUtilities::getIPAddress(m_rawConfig[configKey::hostName].toString());
const QString protocolName = m_rawConfig.value("protocol").toString();
const QString configDataKey = protocolName + "_config_data";
QJsonObject vpnConfigData = m_rawConfig.value(configDataKey).toObject();
m_impl->activate(m_rawConfig);
const QString endpointHost = vpnConfigData.value(configKey::hostName).toString();
const QString endpointIp = NetworkUtilities::getIPAddress(endpointHost);
if (endpointIp.isEmpty()) {
qWarning() << "WireguardProtocol: unable to resolve the endpoint host, aborting this attempt";
recordLastError(ErrorCode::EndpointResolutionError);
return ErrorCode::EndpointResolutionError;
}
// Activate a resolved copy: m_rawConfig must keep the original hostname so
// a later attempt (e.g. reconnect after wakeup) re-resolves it from scratch
// instead of reusing a stale — or empty — address.
QJsonObject rawConfig = m_rawConfig;
vpnConfigData[configKey::hostName] = endpointIp;
rawConfig.insert(configDataKey, vpnConfigData);
if (rawConfig.value(configKey::hostName).toString() == endpointHost) {
rawConfig[configKey::hostName] = endpointIp;
}
m_impl->activate(rawConfig);
return ErrorCode::NoError;
}
@@ -75,5 +96,6 @@ ErrorCode WireguardProtocol::stopMzImpl()
ErrorCode WireguardProtocol::start()
{
m_stopped = false;
return startMzImpl();
}

View File

@@ -28,6 +28,7 @@ public:
private:
QScopedPointer<ControllerImpl> m_impl;
bool m_stopped = false;
};
#endif // WIREGUARDPROTOCOL_H

View File

@@ -68,6 +68,7 @@ namespace amnezia
OpenVpnUnknownError = 701,
OpenVpnTapAdapterError = 702,
AddressPoolError = 703,
EndpointResolutionError = 704,
// 3rd party utils errors
OpenSslFailed = 800,

View File

@@ -66,6 +66,7 @@ QString errorString(ErrorCode code) {
case (ErrorCode::OpenVpnAdaptersInUseError): errorMessage = QObject::tr("Can't connect: another VPN connection is active"); break;
case (ErrorCode::OpenVpnTapAdapterError): errorMessage = QObject::tr("Can't setup OpenVPN TAP network adapter"); break;
case (ErrorCode::AddressPoolError): errorMessage = QObject::tr("VPN pool error: no available addresses"); break;
case (ErrorCode::EndpointResolutionError): errorMessage = QObject::tr("Failed to resolve the VPN server address"); break;
case (ErrorCode::ImportInvalidConfigError): errorMessage = QObject::tr("The config does not contain any containers and credentials for connecting to the server"); break;
case (ErrorCode::ImportBackupFileUseRestoreInstead): errorMessage = QObject::tr("Backup files cannot be imported here. Use 'Restore from backup' instead."); break;

View File

@@ -637,7 +637,7 @@ void Daemon::checkHandshake() {
pendingHandshakes++;
}
}
// Check again if there were connections that haven't completed a handshake.
if (pendingHandshakes > 0) {
m_handshakeTimer.start(HANDSHAKE_POLL_MSEC);

View File

@@ -76,7 +76,16 @@ void LocalSocketController::errorOccurred(
}
qCritical() << "ControllerError";
disconnectInternal();
// The socket to the daemon is gone (daemon restart/crash or a failed
// connection attempt). Unlike a plain tunnel-down message we cannot talk to
// the daemon anymore, so remember that and let the next activate()/
// deactivate() re-establish the connection instead of writing into a dead
// socket forever.
m_daemonState = eDisconnected;
m_initializingRetry = 0;
m_initializingTimer.stop();
emit disconnected();
}
void LocalSocketController::disconnectInternal() {
@@ -122,7 +131,38 @@ void LocalSocketController::daemonConnected() {
checkStatus();
}
bool LocalSocketController::isSocketAlive() const {
// ConnectingState counts as alive: on a fresh protocol activate() runs
// right after initialize(), before the connection (and the status
// handshake) completes — writes are buffered by the socket, and the daemon
// processes them once the connection is established.
const QLocalSocket::LocalSocketState state = m_socket->state();
return state == QLocalSocket::ConnectedState ||
state == QLocalSocket::ConnectingState;
}
void LocalSocketController::reconnectToDaemon() {
if (m_daemonState == eInitializing) {
// A connection attempt is already in progress.
return;
}
if (isSocketAlive()) {
return;
}
logger.warning() << "Daemon socket is not connected; reconnecting";
m_initializingRetry = 0;
initializeInternal();
}
void LocalSocketController::activate(const QJsonObject &rawConfig) {
if (!isSocketAlive()) {
logger.error() << "Cannot activate, daemon connection is not ready";
reconnectToDaemon();
emit disconnected();
return;
}
QString protocolName = rawConfig.value("protocol").toString();
int splitTunnelType = rawConfig.value("splitTunnelType").toInt();
@@ -267,8 +307,9 @@ void LocalSocketController::activate(const QJsonObject &rawConfig) {
void LocalSocketController::deactivate() {
logger.debug() << "Deactivating";
if (m_daemonState != eReady) {
if (m_daemonState != eReady || !isSocketAlive()) {
logger.debug() << "No disconnect, controller is not ready";
reconnectToDaemon();
emit disconnected();
return;
}
@@ -334,7 +375,12 @@ void LocalSocketController::readData() {
logger.debug() << "Reading";
Q_ASSERT(m_socket);
Q_ASSERT(m_daemonState == eInitializing || m_daemonState == eReady);
if (m_daemonState != eInitializing && m_daemonState != eReady) {
// Stray data delivered around a socket teardown — nothing to do with it.
m_socket->readAll();
m_buffer.clear();
return;
}
QByteArray input = m_socket->readAll();
m_buffer.append(input);

View File

@@ -39,6 +39,8 @@ class LocalSocketController final : public ControllerImpl {
private:
void initializeInternal();
void disconnectInternal();
void reconnectToDaemon();
bool isSocketAlive() const;
void daemonConnected();
void errorOccurred(QLocalSocket::LocalSocketError socketError);

View File

@@ -7,10 +7,14 @@
#include <net/if.h>
#include <QDBusVariant>
#include <QNetworkInterface>
#include <QTimer>
#include <QtDBus/QtDBus>
#include "core/utils/networkUtilities.h"
#include "leakdetector.h"
#include "logger.h"
#include "router_linux.h"
constexpr const char* DBUS_RESOLVE_SERVICE = "org.freedesktop.resolve1";
constexpr const char* DBUS_RESOLVE_PATH = "/org/freedesktop/resolve1";
@@ -27,24 +31,56 @@ DnsUtilsLinux::DnsUtilsLinux(QObject* parent) : DnsUtils(parent) {
logger.debug() << "DnsUtilsLinux created.";
QDBusConnection conn = QDBusConnection::systemBus();
m_resolver = new QDBusInterface(DBUS_RESOLVE_SERVICE, DBUS_RESOLVE_PATH,
DBUS_RESOLVE_MANAGER, conn, this);
auto* watcher = new QDBusServiceWatcher(
DBUS_RESOLVE_SERVICE, conn,
QDBusServiceWatcher::WatchForRegistration |
QDBusServiceWatcher::WatchForUnregistration, this);
connect(watcher, &QDBusServiceWatcher::serviceRegistered,
this, &DnsUtilsLinux::onResolverRegistered);
connect(watcher, &QDBusServiceWatcher::serviceUnregistered,
this, &DnsUtilsLinux::onResolverUnregistered);
if (conn.interface()->isServiceRegistered(DBUS_RESOLVE_SERVICE)) {
onResolverRegistered();
}
}
void DnsUtilsLinux::onResolverRegistered() {
m_resolver.reset(new QDBusInterface(DBUS_RESOLVE_SERVICE, DBUS_RESOLVE_PATH,
DBUS_RESOLVE_MANAGER,
QDBusConnection::systemBus()));
logger.debug() << "systemd-resolved available, DNS resolver initialized";
if (!m_pendingIfname.isEmpty()) {
logger.debug() << "Re-applying DNS configuration for" << m_pendingIfname;
updateResolvers(m_pendingIfname, m_pendingResolvers);
}
}
void DnsUtilsLinux::onResolverUnregistered() {
logger.debug() << "systemd-resolved disappeared, dropping DNS resolver";
m_resolver.reset();
}
DnsUtilsLinux::~DnsUtilsLinux() {
MZ_COUNT_DTOR(DnsUtilsLinux);
for (auto iterator = m_linkDomains.constBegin();
iterator != m_linkDomains.constEnd(); ++iterator) {
QList<QVariant> argumentList;
argumentList << QVariant::fromValue(iterator.key());
argumentList << QVariant::fromValue(iterator.value());
m_resolver->asyncCallWithArgumentList(QStringLiteral("SetLinkDomains"),
argumentList);
}
if (m_resolver) {
if (m_gatewayIfindex > 0)
setLinkDefaultRoute(m_gatewayIfindex, true);
if (m_ifindex > 0) {
m_resolver->asyncCall(QStringLiteral("RevertLink"), m_ifindex);
for (auto iterator = m_linkDomains.constBegin();
iterator != m_linkDomains.constEnd(); ++iterator) {
QList<QVariant> argumentList;
argumentList << QVariant::fromValue(iterator.key());
argumentList << QVariant::fromValue(iterator.value());
m_resolver->asyncCallWithArgumentList(QStringLiteral("SetLinkDomains"),
argumentList);
}
if (m_ifindex > 0) {
m_resolver->asyncCall(QStringLiteral("RevertLink"), m_ifindex);
}
}
logger.debug() << "DnsUtilsLinux destroyed.";
@@ -52,19 +88,52 @@ DnsUtilsLinux::~DnsUtilsLinux() {
bool DnsUtilsLinux::updateResolvers(const QString& ifname,
const QList<QHostAddress>& resolvers) {
if (m_gatewayIfindex > 0) {
setLinkDefaultRoute(m_gatewayIfindex, true);
m_gatewayIfindex = 0;
}
const int previousIfindex = m_ifindex;
m_ifindex = if_nametoindex(qPrintable(ifname));
if (m_ifindex <= 0) {
logger.error() << "Unable to resolve ifindex for" << ifname;
return false;
}
m_pendingIfname = ifname;
m_pendingResolvers = resolvers;
if (!m_resolver) {
logger.debug() << "systemd-resolved not ready, queuing DNS configuration";
return true;
}
const int gwIdx = NetworkUtilities::getGatewayAndIface().second.index();
if (gwIdx > 0 && gwIdx != m_ifindex && gwIdx != m_gatewayIfindex) {
m_gatewayIfindex = gwIdx;
setLinkDefaultRoute(gwIdx, false);
}
setLinkDNS(m_ifindex, resolvers);
setLinkDefaultRoute(m_ifindex, true);
updateLinkDomains();
if (previousIfindex > 0 && previousIfindex != m_ifindex) {
m_resolver->callWithArgumentList(QDBus::Block, QStringLiteral("RevertLink"),
{QVariant::fromValue(previousIfindex)});
}
return true;
}
bool DnsUtilsLinux::restoreResolvers() {
m_pendingIfname.clear();
m_pendingResolvers.clear();
if (m_gatewayIfindex > 0) {
setLinkDefaultRoute(m_gatewayIfindex, true);
m_gatewayIfindex = 0;
}
for (auto iterator = m_linkDomains.constBegin();
iterator != m_linkDomains.constEnd(); ++iterator) {
setLinkDomains(iterator.key(), iterator.value());
@@ -72,7 +141,7 @@ bool DnsUtilsLinux::restoreResolvers() {
m_linkDomains.clear();
/* Revert the VPN interface's DNS configuration */
if (m_ifindex > 0) {
if (m_ifindex > 0 && m_resolver) {
QList<QVariant> argumentList = {QVariant::fromValue(m_ifindex)};
QDBusPendingReply<> reply = m_resolver->asyncCallWithArgumentList(
QStringLiteral("RevertLink"), argumentList);
@@ -90,13 +159,17 @@ bool DnsUtilsLinux::restoreResolvers() {
void DnsUtilsLinux::dnsCallCompleted(QDBusPendingCallWatcher* call) {
QDBusPendingReply<> reply = *call;
if (reply.isError()) {
logger.error() << "Error received from the DBus service";
logger.debug() << "DBus call failed (may be transient after systemd-resolved restart)";
logger.debug() << "Restarting resolved to clear its query backlog";
RouterLinux::Instance().flushDns();
scheduleRetry();
}
delete call;
}
void DnsUtilsLinux::setLinkDNS(int ifindex,
const QList<QHostAddress>& resolvers) {
if (!m_resolver) return;
QList<DnsResolver> resolverList;
char ifnamebuf[IF_NAMESIZE];
const char* ifname = if_indextoname(ifindex, ifnamebuf);
@@ -121,6 +194,7 @@ void DnsUtilsLinux::setLinkDNS(int ifindex,
void DnsUtilsLinux::setLinkDomains(int ifindex,
const QList<DnsLinkDomain>& domains) {
if (!m_resolver) return;
char ifnamebuf[IF_NAMESIZE];
const char* ifname = if_indextoname(ifindex, ifnamebuf);
if (ifname) {
@@ -144,6 +218,7 @@ void DnsUtilsLinux::setLinkDomains(int ifindex,
}
void DnsUtilsLinux::setLinkDefaultRoute(int ifindex, bool enable) {
if (!m_resolver) return;
QList<QVariant> argumentList;
argumentList << QVariant::fromValue(ifindex);
argumentList << QVariant::fromValue(enable);
@@ -156,6 +231,7 @@ void DnsUtilsLinux::setLinkDefaultRoute(int ifindex, bool enable) {
}
void DnsUtilsLinux::updateLinkDomains() {
if (!m_resolver) return;
/* Get the list of search domains, and remove any others that might conspire
* to satisfy DNS resolution. Unfortunately, this is a pain because Qt doesn't
* seem to be able to demarshall complex property types.
@@ -174,11 +250,20 @@ void DnsUtilsLinux::updateLinkDomains() {
void DnsUtilsLinux::dnsDomainsReceived(QDBusPendingCallWatcher* call) {
QDBusPendingReply<QVariant> reply = *call;
call->deleteLater();
if (reply.isError()) {
logger.error() << "Error retrieving the DNS domains from the DBus service";
delete call;
// systemd-resolved may still be starting up after a restart — retry a few times
if (m_ifindex > 0 && m_domainRetries++ < 5) {
logger.debug() << "systemd-resolved not ready yet, retrying DNS setup ("
<< m_domainRetries << "/5)";
QTimer::singleShot(500, this, &DnsUtilsLinux::updateLinkDomains);
} else {
logger.warning() << "Failed to configure DNS after 5 retries";
m_domainRetries = 0;
}
return;
}
m_domainRetries = 0;
/* Update the state of the DNS domains */
m_linkDomains.clear();
@@ -204,9 +289,30 @@ void DnsUtilsLinux::dnsDomainsReceived(QDBusPendingCallWatcher* call) {
}
/* Add a root search domain for the new interface. */
QList<DnsLinkDomain> newlist = {root};
setLinkDomains(m_ifindex, newlist);
delete call;
if (m_ifindex > 0) {
setLinkDomains(m_ifindex, {root});
/* Disable DefaultRoute on the physical gateway so systemd-resolved
* routes all DNS through the VPN interface. */
const int gwIdx = NetworkUtilities::getGatewayAndIface().second.index();
if (gwIdx > 0 && gwIdx != m_ifindex && gwIdx != m_gatewayIfindex) {
m_gatewayIfindex = gwIdx;
setLinkDefaultRoute(gwIdx, false);
}
}
}
void DnsUtilsLinux::scheduleRetry() {
if (m_pendingIfname.isEmpty() || m_retryPending || m_domainRetries >= 5)
return;
m_retryPending = true;
++m_domainRetries;
logger.debug() << "Retrying full DNS setup (" << m_domainRetries << "/5)";
QTimer::singleShot(1000, this, [this]() {
m_retryPending = false;
if (!m_pendingIfname.isEmpty())
updateResolvers(m_pendingIfname, m_pendingResolvers);
});
}
static DnsMetatypeRegistrationProxy s_dnsMetatypeProxy;

View File

@@ -6,7 +6,12 @@
#define DNSUTILSLINUX_H
#include <QDBusInterface>
#include <QScopedPointer>
#include <QDBusPendingCallWatcher>
#include <QDBusServiceWatcher>
#include <QHostAddress>
#include <QList>
#include <QString>
#include "daemon/dnsutils.h"
#include "dbustypeslinux.h"
@@ -29,13 +34,22 @@ class DnsUtilsLinux final : public DnsUtils {
void updateLinkDomains();
private slots:
void onResolverRegistered();
void onResolverUnregistered();
void dnsCallCompleted(QDBusPendingCallWatcher*);
void dnsDomainsReceived(QDBusPendingCallWatcher*);
private:
void scheduleRetry();
int m_ifindex = 0;
int m_gatewayIfindex = 0;
int m_domainRetries = 0;
bool m_retryPending = false;
QMap<int, DnsLinkDomainList> m_linkDomains;
QDBusInterface* m_resolver = nullptr;
QScopedPointer<QDBusInterface> m_resolver;
QString m_pendingIfname;
QList<QHostAddress> m_pendingResolvers;
};
#endif // DNSUTILSLINUX_H

View File

@@ -33,6 +33,7 @@
#include "linuxfirewall.h"
#include "logger.h"
#include "xray_defs.h"
#include <QFileInfo>
#include <QProcess>
#define BRAND_CODE "amn"
@@ -109,7 +110,7 @@ int LinuxFirewall::linkChain(LinuxFirewall::IPVersion ip, const QString& chain,
// (we can't safely delete all rules at once since rule numbers change)
// TODO: occasionally this script results in warnings in logs "Bad rule (does a matching rule exist in the chain?)" - this happens when
// the e.g OUTPUT chain is empty but this script attempts to delete things from it anyway. It doesn't cause any problems, but we should still fix at some point..
return execute(QStringLiteral("if ! %1 -L %2 -n --line-numbers -t %4 2> /dev/null | awk 'int($1) == 1 && $2 == \"%3\" { found=1 } END { if(found==1) { exit 0 } else { exit 1 } }' ; then %1 -I %2 -j %3 -t %4 && %1 -L %2 -n --line-numbers -t %4 2> /dev/null | awk 'int($1) > 1 && $2 == \"%3\" { print $1; exit }' | xargs %1 -t %4 -D %2 ; fi").arg(cmd, parent, chain, tableName));
return execute(QStringLiteral("if ! %1 -L %2 -n --line-numbers -t %4 2> /dev/null | awk 'int($1) == 1 && $2 == \"%3\" { found=1 } END { if(found==1) { exit 0 } else { exit 1 } }' ; then %1 -I %2 -j %3 -t %4 && %1 -L %2 -n --line-numbers -t %4 2> /dev/null | awk 'int($1) > 1 && $2 == \"%3\" { print $1; exit }' | xargs -r %1 -t %4 -D %2 ; fi").arg(cmd, parent, chain, tableName));
}
else
return execute(QStringLiteral("if ! %1 -C %2 -j %3 -t %4 2> /dev/null ; then %1 -A %2 -j %3 -t %4; fi").arg(cmd, parent, chain, tableName));
@@ -501,13 +502,22 @@ int LinuxFirewall::execute(const QString &command, bool ignoreErrors)
logger.debug() << "(" << exitCode << ") $ " << command;
if (!out.isEmpty())
logger.info() << out;
if (!err.isEmpty())
if (!err.isEmpty() && !ignoreErrors)
logger.warning() << err;
return exitCode;
}
void LinuxFirewall::setupTrafficSplitting()
{
const QString cgroupBase = QStringLiteral("/sys/fs/cgroup/net_cls");
if (!QFileInfo::exists(cgroupBase)) {
logger.warning() << "net_cls cgroup v1 not available, traffic splitting disabled";
return;
}
execute(QStringLiteral(
"if ! grep -qE '^[0-9]+[[:space:]]+%1$' /etc/iproute2/rt_tables 2>/dev/null ; then "
"echo '200 %1' >> /etc/iproute2/rt_tables ; fi"
).arg(kRtableName));
auto cGroupDir = "/sys/fs/cgroup/net_cls/" BRAND_CODE "vpnexclusions/";
logger.info() << "Should be setting up cgroup in" << cGroupDir << "for traffic splitting";
execute(QStringLiteral("if [ ! -d %1 ] ; then mkdir %1 ; sleep 0.1 ; echo %2 > %1/net_cls.classid ; fi").arg(cGroupDir).arg(kCGroupId));
@@ -519,6 +529,7 @@ void LinuxFirewall::teardownTrafficSplitting()
{
logger.info() << "Tearing down cgroup and routing rules";
execute(QStringLiteral("if ip rule list | grep -q %1; then ip rule del from all fwmark %1 lookup %2 2> /dev/null ; fi").arg(kPacketTag, kRtableName));
execute(QStringLiteral("ip route flush table %1").arg(kRtableName));
execute(QStringLiteral("ip route flush table %1").arg(kRtableName), true);
execute(QStringLiteral("ip route flush cache"));
execute(QStringLiteral("sed -i '/%1/d' /etc/iproute2/rt_tables").arg(kRtableName));
}

View File

@@ -263,7 +263,11 @@ bool WireguardUtilsLinux::updatePeer(const InterfaceConfig& config) {
// 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));
if (!config.m_serverIpv4AddrIn.isEmpty() &&
!m_rtmonitor->addExclusionRoute(IPAddress(config.m_serverIpv4AddrIn))) {
logger.error() << "No gateway — cannot add server exclusion route";
return false;
}
m_rtmonitor->addExclusionRoute(IPAddress(config.m_serverIpv6AddrIn));
}

View File

@@ -37,7 +37,6 @@
enum NMState {
NM_STATE_UNKNOWN = 0,
NM_STATE_ASLEEP = 10,
NM_STATE_DISABLED = 10,
NM_STATE_DISCONNECTED = 20,
NM_STATE_DISCONNECTING = 30,
NM_STATE_CONNECTING = 40,
@@ -202,9 +201,18 @@ void LinuxNetworkWatcherWorker::NMStateChanged(quint32 state)
{
logger.debug() << "NMStateChanged " << state;
if (state == NM_STATE_ASLEEP || state == NM_STATE_DISABLED) {
// NM_STATE_ASLEEP means networking just went *down* (e.g. NM tearing
// interfaces down for suspend), not that the system woke up. Fire
// wakeup() on the transition back out of it, once NM starts doing
// anything again, rather than on entering it.
if (state == NM_STATE_ASLEEP) {
m_wasAsleep = true;
} else if (m_wasAsleep) {
m_wasAsleep = false;
emit wakeup();
} else if (state == NM_STATE_CONNECTED_GLOBAL) {
}
if (state == NM_STATE_CONNECTED_GLOBAL) {
emit networkChanged();
}
}

View File

@@ -39,6 +39,10 @@ class LinuxNetworkWatcherWorker final : public QObject {
// initialization. When a property of them changes, we check if the access
// point is active and unsecure.
QStringList m_devicePaths;
// Set on NM_STATE_ASLEEP, cleared when NetworkManager comes back. Guards
// wakeup() so it fires on the resume transition, not on going to sleep.
bool m_wasAsleep = false;
};
#endif // LINUXNETWORKWATCHERWORKER_H

View File

@@ -19,6 +19,7 @@ ConnectionUiController::ConnectionUiController(ConnectionController* connectionC
m_serversController(serversController)
{
connect(m_connectionController, &ConnectionController::connectionStateChanged, this, &ConnectionUiController::onConnectionStateChanged);
connect(m_connectionController, &ConnectionController::systemWoke, this, &ConnectionUiController::systemWoke);
connect(this, &ConnectionUiController::connectButtonClicked, this, &ConnectionUiController::toggleConnection, Qt::QueuedConnection);

View File

@@ -44,6 +44,7 @@ public slots:
signals:
void connectionStateChanged();
void systemWoke();
void connectionErrorOccurred(ErrorCode errorCode);

View File

@@ -16,6 +16,31 @@ Window {
id: root
objectName: "mainWindow"
property bool controlsDisabled: false
Connections {
target: PageController
function onDisableControls(disabled) {
root.controlsDisabled = disabled
}
}
function clearStuckUiBlockIfNeeded(reason) {
if (busyIndicator.visible || root.controlsDisabled) {
console.warn("UI was still blocked (busyIndicator/disableControls) on", reason, "; clearing it")
busyIndicator.visible = false
PageController.disableControls(false)
}
}
Connections {
target: ConnectionController
function onSystemWoke() {
console.log("ConnectionController.systemWoke received")
root.clearStuckUiBlockIfNeeded("system wakeup")
}
}
Connections {
target: Qt.application
function onStateChanged() {

View File

@@ -36,6 +36,20 @@
using namespace ProtocolUtils;
#ifdef AMNEZIA_DESKTOP
namespace {
// A reconnect attempt that has not reached Connected within this time is
// considered failed and is retried (some failures — e.g. a daemon-side
// activation error — produce no client-visible event at all).
constexpr int RECONNECT_ATTEMPT_TIMEOUT_MSEC = 30 * 1000;
constexpr int RECONNECT_RETRY_BASE_MSEC = 1000;
constexpr int RECONNECT_RETRY_MAX_MSEC = 60 * 1000;
// A fresh trigger does not restart an attempt younger than this: such an
// attempt was started under (almost) the same network conditions anyway.
constexpr int RECONNECT_ATTEMPT_MIN_AGE_MSEC = 1000;
}
#endif
VpnConnection::VpnConnection(SecureServersRepository* serversRepository, SecureAppSettingsRepository* appSettingsRepository, QObject *parent)
: QObject(parent), m_serversRepository(serversRepository), m_appSettingsRepository(appSettingsRepository), m_checkTimer(this)
{
@@ -44,6 +58,13 @@ VpnConnection::VpnConnection(SecureServersRepository* serversRepository, SecureA
connect(IosController::Instance(), &IosController::connectionStateChanged, this, &VpnConnection::setConnectionState);
connect(IosController::Instance(), &IosController::bytesChanged, this, &VpnConnection::onBytesChanged);
#endif
#ifdef AMNEZIA_DESKTOP
m_reconnectRetryTimer.setSingleShot(true);
connect(&m_reconnectRetryTimer, &QTimer::timeout, this, &VpnConnection::startReconnectAttempt);
m_reconnectWatchdogTimer.setSingleShot(true);
connect(&m_reconnectWatchdogTimer, &QTimer::timeout, this, &VpnConnection::onReconnectWatchdogTimeout);
#endif
}
VpnConnection::~VpnConnection()
@@ -336,8 +357,11 @@ void VpnConnection::connectToVpn(const QString &serverId, DockerContainer contai
m_vpnConfiguration = vpnConfiguration;
#ifdef AMNEZIA_DESKTOP
cancelReconnect();
if (m_vpnProtocol) {
disconnect(m_vpnProtocol.data(), &VpnProtocol::protocolError, this, &VpnConnection::vpnProtocolError);
// Detach every slot of ours (state, bytes, errors) so tearing the old
// protocol down doesn't inject events into the new connection flow.
m_vpnProtocol->disconnect(this);
m_vpnProtocol->stop();
m_vpnProtocol.reset();
}
@@ -376,13 +400,19 @@ void VpnConnection::connectToVpn(const QString &serverId, DockerContainer contai
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(), &VpnProtocol::connectionStateChanged, this, &VpnConnection::onProtocolConnectionStateChanged);
connect(m_vpnProtocol.data(), SIGNAL(bytesChanged(quint64, quint64)), this, SLOT(onBytesChanged(quint64, quint64)));
#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);
// The replica is thread-local and long-lived while this method runs on
// every connect — UniqueConnection keeps these from piling up.
const auto queuedUnique = static_cast<Qt::ConnectionType>(Qt::QueuedConnection | Qt::UniqueConnection);
connect(rep.data(), &IpcInterfaceReplica::networkChanged, this, &VpnConnection::onIpcNetworkChanged, queuedUnique);
connect(rep.data(), &IpcInterfaceReplica::wakeup, this, &VpnConnection::onIpcWakeup, queuedUnique);
#if defined(Q_OS_WIN) || defined(Q_OS_LINUX)
connect(rep.data(), &IpcInterfaceReplica::wakeup, this, &VpnConnection::systemWoke, queuedUnique);
#endif
});
#endif
}
@@ -549,29 +579,168 @@ QString VpnConnection::bytesPerSecToText(quint64 bytes)
return QString("%1 %2").arg(QString::number(mbps, 'f', 2)).arg(tr("Mbps")); // Mbit/s
}
void VpnConnection::reconnectToVpn() {
#ifdef AMNEZIA_DESKTOP
void VpnConnection::onIpcWakeup()
{
requestReconnect(QStringLiteral("wakeup"));
}
void VpnConnection::onIpcNetworkChanged()
{
requestReconnect(QStringLiteral("network change"));
}
void VpnConnection::requestReconnect(const QString &trigger)
{
if (m_vpnProtocol.isNull())
return;
if (m_reconnectActive) {
// Conditions changed (e.g. the network actually came back after
// wakeup) — restart the backoff sequence and try again right away.
// An in-flight attempt that was started before this trigger is likely
// doomed (it raced the network coming up), so restart it too instead
// of waiting out its watchdog; a just-started attempt is left alone.
if (m_reconnectAttemptInFlight && m_reconnectAttemptAge.isValid()
&& m_reconnectAttemptAge.elapsed() < RECONNECT_ATTEMPT_MIN_AGE_MSEC) {
qDebug() << "Reconnect: new trigger" << trigger << "ignored, current attempt has just started";
m_reconnectAttempt = 0;
return;
}
qDebug() << "Reconnect: new trigger" << trigger << "while already reconnecting, retrying immediately";
m_reconnectAttempt = 0;
m_reconnectAttemptInFlight = false;
m_reconnectRetryTimer.stop();
m_reconnectWatchdogTimer.stop();
startReconnectAttempt();
return;
}
if (m_connectionState != Vpn::ConnectionState::Connected) {
qWarning() << QString("Reconnect triggered on %1 during inappropriate state: %2; ignoring slot")
qWarning() << QString("Reconnect triggered by %1 during inappropriate state: %2; ignoring")
.arg(trigger)
.arg(QMetaEnum::fromType<Vpn::ConnectionState>().valueToKey(m_connectionState));
return;
}
qDebug() << "Reconnect triggered. Reconnecting to the server";
qDebug() << "Reconnect triggered by" << trigger << ". Reconnecting to the server";
m_reconnectActive = true;
m_reconnectAttempt = 0;
setConnectionState(Vpn::ConnectionState::Reconnecting);
startReconnectAttempt();
}
m_vpnProtocol->stop();
if (ErrorCode err = m_vpnProtocol->start(); err != ErrorCode::NoError) {
setConnectionState(Vpn::ConnectionState::Error);
emit vpnProtocolError(err);
void VpnConnection::startReconnectAttempt()
{
if (!m_reconnectActive)
return;
if (m_vpnProtocol.isNull()) {
cancelReconnect();
return;
}
++m_reconnectAttempt;
qDebug() << "Reconnect: attempt" << m_reconnectAttempt;
m_reconnectAttemptAge.start();
// stop() may synchronously emit Disconnected; while the machine is active
// (and no attempt is in flight yet) onProtocolConnectionStateChanged
// suppresses it so the UI stays in Reconnecting.
m_vpnProtocol->stop();
m_reconnectAttemptInFlight = true;
if (ErrorCode err = m_vpnProtocol->start(); err != ErrorCode::NoError) {
qWarning() << "Reconnect: attempt" << m_reconnectAttempt << "failed to start, error" << err;
scheduleReconnectRetry();
return;
}
// start() may have failed synchronously through a protocol event, in which
// case the retry is already scheduled and the watchdog must stay off.
if (m_reconnectAttemptInFlight) {
m_reconnectWatchdogTimer.start(RECONNECT_ATTEMPT_TIMEOUT_MSEC);
}
}
void VpnConnection::onReconnectWatchdogTimeout()
{
if (!m_reconnectActive || !m_reconnectAttemptInFlight)
return;
qWarning() << "Reconnect: attempt" << m_reconnectAttempt << "did not reach the Connected state in time";
scheduleReconnectRetry();
}
void VpnConnection::scheduleReconnectRetry()
{
m_reconnectWatchdogTimer.stop();
m_reconnectAttemptInFlight = false;
if (!m_reconnectActive)
return;
const int delay = reconnectRetryDelayMsec();
qDebug() << "Reconnect: next attempt in" << delay << "ms";
m_reconnectRetryTimer.start(delay);
}
void VpnConnection::cancelReconnect()
{
m_reconnectActive = false;
m_reconnectAttemptInFlight = false;
m_reconnectRetryTimer.stop();
m_reconnectWatchdogTimer.stop();
}
int VpnConnection::reconnectRetryDelayMsec() const
{
// 1s, 2s, 4s, ... capped at RECONNECT_RETRY_MAX_MSEC; a fresh trigger
// resets m_reconnectAttempt and thus the sequence.
const int exponent = qMin(m_reconnectAttempt > 0 ? m_reconnectAttempt - 1 : 0, 6);
return qMin(RECONNECT_RETRY_BASE_MSEC << exponent, RECONNECT_RETRY_MAX_MSEC);
}
#endif
void VpnConnection::onProtocolConnectionStateChanged(Vpn::ConnectionState state)
{
#ifdef AMNEZIA_DESKTOP
if (m_reconnectActive) {
switch (state) {
case Vpn::ConnectionState::Connected:
qDebug() << "Reconnect: succeeded on attempt" << m_reconnectAttempt;
cancelReconnect();
break; // propagate below
case Vpn::ConnectionState::Disconnected:
case Vpn::ConnectionState::Error:
// Keep the between-attempts cleanup the old code used to run for
// a swallowed Disconnected (DNS flush, saved-routes cleanup), but
// hold the UI in Reconnecting and keep retrying.
onConnectionStateChanged(state);
if (m_reconnectAttemptInFlight) {
qWarning() << "Reconnect: attempt" << m_reconnectAttempt << "failed, protocol reported"
<< QMetaEnum::fromType<Vpn::ConnectionState>().valueToKey(state);
scheduleReconnectRetry();
}
return;
default:
// Transient states while retrying — keep showing Reconnecting.
return;
}
}
#endif
setConnectionState(state);
}
void VpnConnection::disconnectFromVpn()
{
#ifdef AMNEZIA_DESKTOP
cancelReconnect();
#endif
#if defined(Q_OS_IOS) || defined(MACOS_NE)
// iOS/macOS NE use IosController directly; m_vpnProtocol is not set there.
IosController::Instance()->disconnectVpn();
@@ -597,6 +766,13 @@ void VpnConnection::disconnectFromVpn()
});
#endif
#ifdef AMNEZIA_DESKTOP
// Drive the final state ourselves: a protocol that is already internally
// Disconnected (e.g. after failed reconnect attempts) will not emit
// another Disconnected, which used to leave the UI stuck in Disconnecting.
m_vpnProtocol->disconnect(this);
#endif
m_vpnProtocol->stop();
#if !defined(Q_OS_ANDROID) && !defined(AMNEZIA_DESKTOP)
@@ -604,13 +780,22 @@ void VpnConnection::disconnectFromVpn()
#endif
m_vpnProtocol = nullptr;
#ifdef AMNEZIA_DESKTOP
setConnectionState(Vpn::ConnectionState::Disconnected);
#endif
}
void VpnConnection::setConnectionState(Vpn::ConnectionState state) {
onConnectionStateChanged(state);
#ifndef AMNEZIA_DESKTOP
// On desktop the reconnect machine decides which protocol events are
// propagated (see onProtocolConnectionStateChanged); on mobile keep the
// historical behavior of hiding the stop() blip during a reconnect.
if (state == Vpn::Disconnected && m_connectionState == Vpn::Reconnecting)
return;
#endif
m_connectionState = state;
emit connectionStateChanged(state);

View File

@@ -6,6 +6,7 @@
#include <QString>
#include <QScopedPointer>
#include <QRemoteObjectNode>
#include <QElapsedTimer>
#include <QTimer>
#include "core/protocols/vpnProtocol.h"
@@ -50,7 +51,6 @@ public:
public slots:
void setRepositories(SecureServersRepository* serversRepository, SecureAppSettingsRepository* appSettingsRepository);
void connectToVpn(const QString &serverId, DockerContainer container, const QJsonObject &vpnConfiguration);
void reconnectToVpn();
void disconnectFromVpn();
void onKillSwitchModeChanged(bool enabled);
@@ -65,10 +65,21 @@ signals:
void serviceIsNotReady();
void systemWoke();
protected slots:
void onBytesChanged(quint64 receivedBytes, quint64 sentBytes);
void onConnectionStateChanged(Vpn::ConnectionState state);
private slots:
void onProtocolConnectionStateChanged(Vpn::ConnectionState state);
#ifdef AMNEZIA_DESKTOP
void onIpcWakeup();
void onIpcNetworkChanged();
void startReconnectAttempt();
void onReconnectWatchdogTimeout();
#endif
protected:
QSharedPointer<VpnProtocol> m_vpnProtocol;
@@ -96,6 +107,24 @@ private:
void appendSplitTunnelingConfig();
void appendKillSwitchConfig();
#ifdef AMNEZIA_DESKTOP
// Auto-reconnect state machine (wakeup / network change). While it is
// active the UI is held in the Reconnecting state and stop()/start()
// attempts are retried with backoff until the protocol reports Connected
// or the user cancels via connect/disconnect.
void requestReconnect(const QString &trigger);
void scheduleReconnectRetry();
void cancelReconnect();
int reconnectRetryDelayMsec() const;
bool m_reconnectActive = false; // machine engaged, UI shows Reconnecting
bool m_reconnectAttemptInFlight = false; // start() issued, waiting for the outcome
int m_reconnectAttempt = 0; // attempts since the last trigger, drives backoff
QTimer m_reconnectRetryTimer{this}; // single-shot, schedules the next attempt
QTimer m_reconnectWatchdogTimer{this}; // single-shot, bounds a single attempt
QElapsedTimer m_reconnectAttemptAge; // how long the in-flight attempt has been running
#endif
};
#endif // VPNCONNECTION_H

View File

@@ -22,6 +22,7 @@
QFile Logger::m_file;
QTextStream Logger::m_textStream;
QMutex Logger::m_fileMutex;
QString Logger::m_logFileName = QString("%1.log").arg(APPLICATION_NAME);
QString Logger::m_serviceLogFileName = QString("%1.log").arg(SERVICE_NAME);
@@ -68,15 +69,22 @@ bool Logger::init(bool isServiceLogger)
return false;
}
m_file.setFileName(appDir.filePath(logFileName));
if (!m_file.open(QIODevice::Append)) {
bool opened = false;
{
QMutexLocker locker(&m_fileMutex);
m_file.setFileName(appDir.filePath(logFileName));
opened = m_file.open(QIODevice::Append);
if (opened) {
m_file.setTextModeEnabled(true);
m_textStream.setDevice(&m_file);
}
}
if (!opened) {
qWarning() << "Cannot open log file:" << logFileName;
return false;
}
m_file.setTextModeEnabled(true);
m_textStream.setDevice(&m_file);
qInstallMessageHandler(messageHandler);
return true;
@@ -84,6 +92,7 @@ bool Logger::init(bool isServiceLogger)
void Logger::deInit()
{
QMutexLocker locker(&m_fileMutex);
m_textStream.setDevice(nullptr);
m_file.close();
}
@@ -137,8 +146,11 @@ QString Logger::serviceLogsFilePath()
QString Logger::getLogFile()
{
if (m_file.isOpen()) {
m_file.flush();
{
QMutexLocker locker(&m_fileMutex);
if (m_file.isOpen()) {
m_file.flush();
}
}
QFile file(userLogsFilePath());
@@ -154,8 +166,11 @@ QString Logger::getLogFile()
QString Logger::getServiceLogFile()
{
if (m_file.isOpen()) {
m_file.flush();
{
QMutexLocker locker(&m_fileMutex);
if (m_file.isOpen()) {
m_file.flush();
}
}
QFile file(serviceLogsFilePath());
@@ -184,8 +199,12 @@ bool Logger::openLogsFolder(bool isServiceLogger)
void Logger::clearLogs(bool isServiceLogger)
{
bool isLogActive = m_file.isOpen();
m_file.close();
bool isLogActive;
{
QMutexLocker locker(&m_fileMutex);
isLogActive = m_file.isOpen();
m_file.close();
}
QFile file(isServiceLogger ? serviceLogsFilePath() : userLogsFilePath());
@@ -243,9 +262,12 @@ Logger::LogStreamer::~LogStreamer()
.arg(QDateTime::currentDateTimeUtc().toString("[yyyy-MM-dd hh:mm:ss.zzzZ]"),
logLevelString, m_logger->className(), m_data->m_buffer.trimmed());
if (m_file.isOpen()) {
QTextStream logToFile(&m_file);
logToFile << message << Qt::endl << Qt::flush;
{
QMutexLocker locker(&m_fileMutex);
if (m_file.isOpen()) {
QTextStream logToFile(&m_file);
logToFile << message << Qt::endl << Qt::flush;
}
}
QTextStream logToOutput((m_logLevel == LogLevel::Error) ? stderr : stdout);

View File

@@ -4,6 +4,7 @@
#include <QDebug>
#include <QDir>
#include <QFile>
#include <QMutex>
#include <QString>
#include <QTextStream>
@@ -104,6 +105,7 @@ private:
static QFile m_file;
static QTextStream m_textStream;
static QMutex m_fileMutex;
static QString m_logFileName;
static QString m_serviceLogFileName;

View File

@@ -178,11 +178,16 @@ bool RouterLinux::flushDns()
}
p.waitForFinished();
QByteArray output(p.readAll());
QByteArray output = p.readAll();
if ((p.exitStatus() != QProcess::NormalExit) || (p.exitCode() != 0)) {
qDebug().noquote() << "Failed to flush DNS: " + output;
return false;
}
if (output.isEmpty())
qDebug().noquote() << "Flush dns completed";
else
qDebug().noquote() << "OUTPUT systemctl restart nscd/systemd-resolved: " + output;
qDebug().noquote() << "OUTPUT systemctl restart: " + output;
return true;
}