add close proc & fix check inet

This commit is contained in:
dranik
2026-06-10 10:42:36 +03:00
parent 1be22a3051
commit 657b0fd60c
9 changed files with 124 additions and 75 deletions
+45 -67
View File
@@ -13,13 +13,9 @@
#include <QJsonDocument>
#include <QTimer>
#include <QJsonObject>
#include <QNetworkAccessManager>
#include <QNetworkInterface>
#include <QNetworkProxy>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QTcpSocket>
#include <QUrl>
#include <QtCore/qlogging.h>
#include <QtCore/qobjectdefs.h>
#include <QtCore/qprocess.h>
@@ -124,8 +120,6 @@ ErrorCode XrayProtocol::start()
return ErrorCode::XrayExecutableCrashed;
}
// Fix fingerprint: old configs may contain "Mozilla/5.0" which xray-core rejects.
// Replace with the correct default at runtime so stale stored configs still work.
if (xrayConfigStr.contains("Mozilla/5.0", Qt::CaseInsensitive)) {
xrayConfigStr.replace("Mozilla/5.0", amnezia::protocols::xray::defaultFingerprint,
Qt::CaseInsensitive);
@@ -133,15 +127,12 @@ ErrorCode XrayProtocol::start()
<< amnezia::protocols::xray::defaultFingerprint;
}
// Fix inbound listen address: old configs may use "10.33.0.2" which doesn't exist
// until TUN is created. xray must listen on 127.0.0.1 so tun2socks can connect.
if (xrayConfigStr.contains(amnezia::protocols::xray::defaultLocalAddr)) {
xrayConfigStr.replace(amnezia::protocols::xray::defaultLocalAddr,
amnezia::protocols::xray::defaultLocalListenAddr);
qDebug() << "XrayProtocol: patched legacy inbound listen address to 127.0.0.1";
}
// Safety net: if tun2socks never reports the bridge (e.g. xray hangs), bail out.
startTimeoutTimer();
return IpcClient::withInterface(
@@ -167,6 +158,25 @@ void XrayProtocol::stop()
stopTimeoutTimer();
stopLivenessMonitor();
if (m_tun2socksProcess) {
m_tun2socksProcess->blockSignals(true);
#ifndef Q_OS_WIN
m_tun2socksProcess->terminate();
auto waitForFinished = m_tun2socksProcess->waitForFinished(1000);
if (!waitForFinished.waitForFinished() || !waitForFinished.returnValue()) {
qWarning() << "Failed to terminate tun2socks. Killing the process...";
m_tun2socksProcess->kill();
m_tun2socksProcess->waitForFinished(1000);
}
#else
m_tun2socksProcess->kill();
#endif
m_tun2socksProcess->close();
m_tun2socksProcess.reset();
}
IpcClient::withInterface([](QSharedPointer<IpcInterfaceReplica> iface) {
auto disableKillSwitch = iface->disableKillSwitch();
if (!disableKillSwitch.waitForFinished() || !disableKillSwitch.returnValue())
@@ -189,31 +199,13 @@ void XrayProtocol::stop()
qWarning() << "Failed to stop xray";
});
if (m_tun2socksProcess) {
m_tun2socksProcess->blockSignals(true);
#ifndef Q_OS_WIN
m_tun2socksProcess->terminate();
auto waitForFinished = m_tun2socksProcess->waitForFinished(1000);
if (!waitForFinished.waitForFinished() || !waitForFinished.returnValue()) {
qWarning() << "Failed to terminate tun2socks. Killing the process...";
m_tun2socksProcess->kill();
}
#else
// terminate does not do anything useful on Windows
// so just kill the process
m_tun2socksProcess->kill();
#endif
m_tun2socksProcess->close();
m_tun2socksProcess.reset();
}
setConnectionState(Vpn::ConnectionState::Disconnected);
}
ErrorCode XrayProtocol::startTun2Socks()
{
m_tunResourceBusy = false;
m_tun2socksProcess = IpcClient::CreatePrivilegedProcess();
if (!m_tun2socksProcess->waitForSource()) {
return ErrorCode::AmneziaServiceConnectionFailed;
@@ -238,14 +230,14 @@ ErrorCode XrayProtocol::startTun2Socks()
if (!line.contains("[TCP]") && !line.contains("[UDP]"))
qDebug() << "[tun2socks]:" << line;
if (line.contains("resource busy"))
m_tunResourceBusy = true;
if (line.contains("[STACK] tun://") && line.contains("<-> socks5://") && !m_connectivityProbeStarted) {
m_connectivityProbeStarted = true;
disconnect(m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::readyReadStandardOutput, this, nullptr);
disconnect(m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::readyReadStandardError, this, nullptr);
// The local tun<->socks bridge is up, but that only means xray is listening
// locally. Verify real end-to-end traffic (xray -> VPS -> internet) through the
// SOCKS5 proxy before declaring the connection established.
runConnectivityProbe([this](bool ok) {
if (!ok) {
qCritical() << "Xray connectivity probe failed: no traffic flows through the tunnel";
@@ -270,15 +262,7 @@ ErrorCode XrayProtocol::startTun2Socks()
connect(
m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::finished, this,
[this](int exitCode, QProcess::ExitStatus exitStatus) {
// Check stdout for "resource busy" — the TUN device was not yet released
// by the previous tun2socks instance. Retry after a short delay.
bool resourceBusy = false;
if (m_tun2socksProcess) {
auto readOut = m_tun2socksProcess->readAllStandardOutput();
if (readOut.waitForFinished()) {
resourceBusy = readOut.returnValue().contains("resource busy");
}
}
const bool resourceBusy = m_tunResourceBusy;
if (resourceBusy && m_tun2socksRetryCount < maxTun2SocksRetries) {
m_tun2socksRetryCount++;
@@ -398,15 +382,13 @@ ErrorCode XrayProtocol::setupRouting()
bool XrayProtocol::probeServerReachable()
{
if (m_remoteAddress.isEmpty() || m_serverPort <= 0) {
// Not enough info to probe (e.g. imported config with unusual structure).
// Don't block the connection here — the SOCKS5 probe will still catch a dead server.
qWarning() << "XrayProtocol: skipping server reachability probe (address/port unknown)";
return true;
}
QTcpSocket sock;
sock.connectToHost(m_remoteAddress, static_cast<quint16>(m_serverPort));
const bool ok = sock.waitForConnected(serverProbeTimeoutMs);
const bool ok = sock.waitForConnected(m_serverProbeTimeoutMs);
if (!ok) {
qWarning() << "XrayProtocol: server" << m_remoteAddress << ":" << m_serverPort
<< "unreachable:" << sock.errorString();
@@ -417,23 +399,22 @@ bool XrayProtocol::probeServerReachable()
void XrayProtocol::runConnectivityProbe(std::function<void(bool)> onResult)
{
auto *nam = new QNetworkAccessManager(this);
if (m_remoteAddress.isEmpty() || m_serverPort <= 0) {
qWarning() << "XrayProtocol: connectivity probe skipped (server address/port unknown)";
onResult(true);
return;
}
auto *sock = new QTcpSocket(this);
QNetworkProxy proxy(QNetworkProxy::Socks5Proxy, QStringLiteral("127.0.0.1"),
static_cast<quint16>(m_socksPort), m_socksUser, m_socksPassword);
// Let the proxy (xray) resolve DNS remotely so the probe also validates the tunnel's DNS path.
proxy.setCapabilities(QNetworkProxy::TunnelingCapability | QNetworkProxy::HostNameLookupCapability);
nam->setProxy(proxy);
QNetworkRequest req(QUrl(QStringLiteral("http://connectivitycheck.gstatic.com/generate_204")));
req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
QNetworkReply *reply = nam->get(req);
sock->setProxy(proxy);
auto *timeout = new QTimer(this);
timeout->setSingleShot(true);
// Guard so exactly one of {finished, timeout} fires the callback.
auto done = QSharedPointer<bool>::create(false);
auto finish = [=](bool ok) {
@@ -442,20 +423,17 @@ void XrayProtocol::runConnectivityProbe(std::function<void(bool)> onResult)
*done = true;
timeout->stop();
timeout->deleteLater();
reply->deleteLater();
nam->deleteLater();
sock->abort();
sock->deleteLater();
onResult(ok);
};
connect(reply, &QNetworkReply::finished, this, [=]() {
finish(reply->error() == QNetworkReply::NoError);
});
connect(timeout, &QTimer::timeout, this, [=]() {
reply->abort();
finish(false);
});
connect(sock, &QTcpSocket::connected, this, [=]() { finish(true); });
connect(sock, &QAbstractSocket::errorOccurred, this, [=](QAbstractSocket::SocketError) { finish(false); });
connect(timeout, &QTimer::timeout, this, [=]() { finish(false); });
timeout->start(connectivityProbeTimeoutMs);
timeout->start(m_connectivityProbeTimeoutMs);
sock->connectToHost(m_remoteAddress, static_cast<quint16>(m_serverPort));
}
void XrayProtocol::startLivenessMonitor()
@@ -472,21 +450,21 @@ void XrayProtocol::startLivenessMonitor()
if (ok) {
m_livenessFailures = 0;
} else if (++m_livenessFailures >= maxLivenessFailures) {
} else if (++m_livenessFailures >= m_maxLivenessFailures) {
qCritical() << "XrayProtocol: liveness check failed" << m_livenessFailures
<< "times in a row, the tunnel is dead";
stop();
setLastError(ErrorCode::XrayConnectivityCheckFailed);
setLastError(ErrorCode::XrayConnectionLost);
} else {
qWarning() << "XrayProtocol: liveness check failed (" << m_livenessFailures << "/"
<< maxLivenessFailures << ")";
<< m_maxLivenessFailures << ")";
}
});
});
}
m_livenessFailures = 0;
m_livenessTimer->start(livenessIntervalMs);
m_livenessTimer->start(m_livenessIntervalMs);
}
void XrayProtocol::stopLivenessMonitor()
+11 -4
View File
@@ -54,13 +54,20 @@ private:
static constexpr int tun2socksRetryDelayMs = 400;
bool m_connectivityProbeStarted = false;
bool m_tunResourceBusy = false;
QTimer *m_livenessTimer = nullptr;
int m_livenessFailures = 0;
static constexpr int serverProbeTimeoutMs = 5000;
static constexpr int connectivityProbeTimeoutMs = 7000;
static constexpr int livenessIntervalMs = 15000;
static constexpr int maxLivenessFailures = 3;
static constexpr int defaultServerProbeTimeoutMs = 5000;
static constexpr int defaultConnectivityProbeTimeoutMs = 7000;
static constexpr int defaultLivenessIntervalMs = 15000;
static constexpr int defaultMaxLivenessFailures = 3;
int m_serverProbeTimeoutMs = defaultServerProbeTimeoutMs;
int m_connectivityProbeTimeoutMs = defaultConnectivityProbeTimeoutMs;
int m_livenessIntervalMs = defaultLivenessIntervalMs;
int m_maxLivenessFailures = defaultMaxLivenessFailures;
};
#endif // XRAYPROTOCOL_H
+1
View File
@@ -73,6 +73,7 @@ namespace amnezia
Tun2SockExecutableCrashed = 804,
XrayServerUnreachable = 805,
XrayConnectivityCheckFailed = 806,
XrayConnectionLost = 807,
// import and install errors
ImportInvalidConfigError = 900,
+2 -1
View File
@@ -60,7 +60,8 @@ QString errorString(ErrorCode code) {
case (ErrorCode::AmneziaServiceConnectionFailed): errorMessage = QObject::tr("Amnezia helper service error"); break;
case (ErrorCode::OpenSslFailed): errorMessage = QObject::tr("OpenSSL failed"); break;
case (ErrorCode::XrayServerUnreachable): errorMessage = QObject::tr("Can't connect: the VPN server is unreachable"); break;
case (ErrorCode::XrayConnectivityCheckFailed): errorMessage = QObject::tr("Connected, but no internet traffic flows through the tunnel"); break;
case (ErrorCode::XrayConnectivityCheckFailed): errorMessage = QObject::tr("Can't connect: no internet traffic flows through the tunnel"); break;
case (ErrorCode::XrayConnectionLost): errorMessage = QObject::tr("Connection lost: traffic stopped flowing through the tunnel"); break;
// VPN errors
case (ErrorCode::OpenVpnAdaptersInUseError): errorMessage = QObject::tr("Can't connect: another VPN connection is active"); break;
+9 -2
View File
@@ -8,6 +8,7 @@
#include <QFileInfo>
#include <QLocalSocket>
#include "daemon.h"
#include "daemonlocalserverconnection.h"
#include "leakdetector.h"
#include "logger.h"
@@ -58,8 +59,14 @@ bool DaemonLocalServer::initialize() {
DaemonLocalServerConnection* connection =
new DaemonLocalServerConnection(&m_server, socket);
connect(socket, &QLocalSocket::disconnected, connection,
&DaemonLocalServerConnection::deleteLater);
connect(socket, &QLocalSocket::disconnected, connection, [connection]() {
// The client holds a single persistent connection for the whole session, so a dropped
// socket means the app exited or crashed. Deactivate the tunnel so WireGuard, routing and
// DNS don't leak. deactivate() is a safe no-op if nothing is active.
logger.debug() << "Client connection dropped, deactivating daemon";
Daemon::instance()->deactivate(true);
connection->deleteLater();
});
});
return true;
+36
View File
@@ -28,6 +28,42 @@ IpcServer::IpcServer(QObject *parent) : IpcInterfaceSource(parent)
connect(&m_pingHelper, &PingHelper::connectionLose, this, &IpcServer::connectionLose);
}
IpcServer::~IpcServer()
{
}
void IpcServer::resetServiceState()
{
qDebug() << "IpcServer::resetServiceState — tearing down active VPN state";
Xray::getInstance().stopXray();
for (auto it = m_processes.cbegin(); it != m_processes.cend(); ++it) {
const ProcessDescriptor &pd = it.value();
if (!pd.ipcProcess)
continue;
pd.ipcProcess->terminate();
if (!pd.ipcProcess->waitForFinished(1000)) {
pd.ipcProcess->kill();
pd.ipcProcess->waitForFinished(1000);
}
pd.ipcProcess->close();
}
m_processes.clear();
Utils::killProcessByName(Utils::tun2socksPath());
Utils::killProcessByName(Utils::openVpnExecPath());
KillSwitch::instance()->disableKillSwitch();
Router::restoreResolvers();
Router::clearSavedRoutes();
Router::StartRoutingIpv6();
Router::flushDns();
m_pingHelper.stop();
}
int IpcServer::createPrivilegedProcess()
{
#ifdef MZ_DEBUG
+4
View File
@@ -17,6 +17,10 @@ class IpcServer : public IpcInterfaceSource
{
public:
explicit IpcServer(QObject *parent = nullptr);
virtual ~IpcServer();
void resetServiceState();
virtual int createPrivilegedProcess() override;
virtual int routeAddList(const QString &gw, const QStringList &ips) override;
+14 -1
View File
@@ -35,7 +35,20 @@ LocalServer::LocalServer(QObject *parent) : QObject(parent),
QObject::connect(m_server.data(), &QLocalServer::newConnection, this, [this]() {
qDebug() << "LocalServer new connection";
m_serverNode.addHostSideConnection(m_server->nextPendingConnection());
QLocalSocket *socket = m_server->nextPendingConnection();
if (!socket)
return;
m_activeClientSocket = socket;
QObject::connect(socket, &QLocalSocket::disconnected, this, [this, socket]() {
qDebug() << "LocalServer: client disconnected";
if (m_activeClientSocket == socket)
m_ipcServer.resetServiceState();
});
m_serverNode.addHostSideConnection(socket);
if (!m_isRemotingEnabled) {
m_isRemotingEnabled = true;
+2
View File
@@ -41,6 +41,8 @@ public:
QRemoteObjectHost m_serverNode;
bool m_isRemotingEnabled = false;
QPointer<QLocalSocket> m_activeClientSocket;
NetworkWatcher m_networkWatcher;
#ifdef Q_OS_LINUX
DaemonLocalServer server{qApp};