Fix: Add check inet and liveness monitor Xray

This commit is contained in:
dranik
2026-06-09 16:06:56 +03:00
parent 594635e5cf
commit 1be22a3051
4 changed files with 199 additions and 8 deletions
+172 -8
View File
@@ -9,10 +9,17 @@
#include "ipc.h"
#include <QCryptographicHash>
#include <QJsonArray>
#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>
@@ -56,6 +63,28 @@ XrayProtocol::XrayProtocol(const QJsonObject &configuration, QObject *parent) :
qWarning() << "Xray config string is not a valid JSON object";
m_xrayConfig = {};
}
m_serverPort = extractServerPort();
}
int XrayProtocol::extractServerPort() const
{
const QJsonArray outbounds = m_xrayConfig.value(amnezia::protocols::xray::outbounds).toArray();
if (outbounds.isEmpty())
return 0;
const QJsonObject settings = outbounds.first().toObject().value(amnezia::protocols::xray::settings).toObject();
QJsonArray servers;
if (settings.contains(amnezia::protocols::xray::vnext))
servers = settings.value(amnezia::protocols::xray::vnext).toArray();
else if (settings.contains(amnezia::protocols::xray::servers))
servers = settings.value(amnezia::protocols::xray::servers).toArray();
if (servers.isEmpty())
return 0;
return servers.first().toObject().value(amnezia::protocols::xray::port).toInt();
}
XrayProtocol::~XrayProtocol()
@@ -68,6 +97,14 @@ ErrorCode XrayProtocol::start()
{
qDebug() << "XrayProtocol::start()";
m_connectivityProbeStarted = false;
// Step 3: fail fast if the VPS itself is unreachable, before spawning xray/tun2socks.
if (!probeServerReachable()) {
qCritical() << "XrayProtocol: VPN server" << m_remoteAddress << "is unreachable";
return ErrorCode::XrayServerUnreachable;
}
// Inject SOCKS5 auth into the inbound before starting xray.
// Re-uses existing credentials if the config already has them (e.g. imported config).
amnezia::serialization::inbounds::InboundCredentials creds;
@@ -104,22 +141,32 @@ ErrorCode XrayProtocol::start()
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(
[&](QSharedPointer<IpcInterfaceReplica> iface) {
auto xrayStart = iface->xrayStart(xrayConfigStr);
if (!xrayStart.waitForFinished() || !xrayStart.returnValue()) {
qCritical() << "Failed to start xray";
stopTimeoutTimer();
return ErrorCode::XrayExecutableCrashed;
}
return startTun2Socks();
},
[]() { return ErrorCode::AmneziaServiceConnectionFailed; });
[this]() {
stopTimeoutTimer();
return ErrorCode::AmneziaServiceConnectionFailed;
});
}
void XrayProtocol::stop()
{
qDebug() << "XrayProtocol::stop()";
stopTimeoutTimer();
stopLivenessMonitor();
IpcClient::withInterface([](QSharedPointer<IpcInterfaceReplica> iface) {
auto disableKillSwitch = iface->disableKillSwitch();
if (!disableKillSwitch.waitForFinished() || !disableKillSwitch.returnValue())
@@ -191,15 +238,31 @@ ErrorCode XrayProtocol::startTun2Socks()
if (!line.contains("[TCP]") && !line.contains("[UDP]"))
qDebug() << "[tun2socks]:" << line;
if (line.contains("[STACK] tun://") && line.contains("<-> socks5://")) {
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);
if (ErrorCode res = setupRouting(); res != ErrorCode::NoError) {
stop();
setLastError(res);
} else {
setConnectionState(Vpn::ConnectionState::Connected);
}
// 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";
stop();
setLastError(ErrorCode::XrayConnectivityCheckFailed);
return;
}
if (ErrorCode res = setupRouting(); res != ErrorCode::NoError) {
stop();
setLastError(res);
} else {
stopTimeoutTimer();
setConnectionState(Vpn::ConnectionState::Connected);
startLivenessMonitor();
}
});
}
},
Qt::QueuedConnection);
@@ -331,3 +394,104 @@ ErrorCode XrayProtocol::setupRouting()
},
[]() { return ErrorCode::AmneziaServiceConnectionFailed; });
}
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);
if (!ok) {
qWarning() << "XrayProtocol: server" << m_remoteAddress << ":" << m_serverPort
<< "unreachable:" << sock.errorString();
}
sock.abort();
return ok;
}
void XrayProtocol::runConnectivityProbe(std::function<void(bool)> onResult)
{
auto *nam = new QNetworkAccessManager(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);
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) {
if (*done)
return;
*done = true;
timeout->stop();
timeout->deleteLater();
reply->deleteLater();
nam->deleteLater();
onResult(ok);
};
connect(reply, &QNetworkReply::finished, this, [=]() {
finish(reply->error() == QNetworkReply::NoError);
});
connect(timeout, &QTimer::timeout, this, [=]() {
reply->abort();
finish(false);
});
timeout->start(connectivityProbeTimeoutMs);
}
void XrayProtocol::startLivenessMonitor()
{
if (!m_livenessTimer) {
m_livenessTimer = new QTimer(this);
connect(m_livenessTimer, &QTimer::timeout, this, [this]() {
if (connectionState() != Vpn::ConnectionState::Connected)
return;
runConnectivityProbe([this](bool ok) {
if (connectionState() != Vpn::ConnectionState::Connected)
return;
if (ok) {
m_livenessFailures = 0;
} else if (++m_livenessFailures >= maxLivenessFailures) {
qCritical() << "XrayProtocol: liveness check failed" << m_livenessFailures
<< "times in a row, the tunnel is dead";
stop();
setLastError(ErrorCode::XrayConnectivityCheckFailed);
} else {
qWarning() << "XrayProtocol: liveness check failed (" << m_livenessFailures << "/"
<< maxLivenessFailures << ")";
}
});
});
}
m_livenessFailures = 0;
m_livenessTimer->start(livenessIntervalMs);
}
void XrayProtocol::stopLivenessMonitor()
{
if (m_livenessTimer)
m_livenessTimer->stop();
m_livenessFailures = 0;
}
+23
View File
@@ -6,12 +6,16 @@
#include <QHostAddress>
#include <QList>
#include <functional>
#include "core/utils/errorCodes.h"
#include "core/utils/routeModes.h"
#include "core/utils/commonStructs.h"
#include "core/utils/ipcClient.h"
#include "vpnProtocol.h"
class QTimer;
class XrayProtocol : public VpnProtocol
{
public:
@@ -25,10 +29,20 @@ private:
ErrorCode setupRouting();
ErrorCode startTun2Socks();
// Step 3: fast pre-connect TCP probe to the real VPN server endpoint.
bool probeServerReachable();
// Step 2/4: end-to-end probe through the local SOCKS5 proxy (xray -> VPS -> internet).
void runConnectivityProbe(std::function<void(bool)> onResult);
// Step 4: periodic liveness monitoring after the tunnel is established.
void startLivenessMonitor();
void stopLivenessMonitor();
int extractServerPort() const;
QJsonObject m_xrayConfig;
amnezia::RouteMode m_routeMode;
QList<QHostAddress> m_dnsServers;
QString m_remoteAddress;
int m_serverPort = 0;
QString m_socksUser;
QString m_socksPassword;
@@ -38,6 +52,15 @@ private:
int m_tun2socksRetryCount = 0;
static constexpr int maxTun2SocksRetries = 5;
static constexpr int tun2socksRetryDelayMs = 400;
bool m_connectivityProbeStarted = 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;
};
#endif // XRAYPROTOCOL_H
+2
View File
@@ -71,6 +71,8 @@ namespace amnezia
OpenSslFailed = 800,
XrayExecutableCrashed = 803,
Tun2SockExecutableCrashed = 804,
XrayServerUnreachable = 805,
XrayConnectivityCheckFailed = 806,
// import and install errors
ImportInvalidConfigError = 900,
+2
View File
@@ -59,6 +59,8 @@ QString errorString(ErrorCode code) {
case (ErrorCode::OpenVpnExecutableMissing): errorMessage = QObject::tr("OpenVPN executable missing"); break;
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;
// VPN errors
case (ErrorCode::OpenVpnAdaptersInUseError): errorMessage = QObject::tr("Can't connect: another VPN connection is active"); break;