mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-06-22 02:01:08 +07:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7b96cfec8c | |||
| 657b0fd60c | |||
| 1be22a3051 |
@@ -9,10 +9,13 @@
|
||||
#include "ipc.h"
|
||||
|
||||
#include <QCryptographicHash>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QTimer>
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkInterface>
|
||||
#include <QNetworkProxy>
|
||||
#include <QTcpSocket>
|
||||
#include <QtCore/qlogging.h>
|
||||
#include <QtCore/qobjectdefs.h>
|
||||
#include <QtCore/qprocess.h>
|
||||
@@ -56,6 +59,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 +93,13 @@ ErrorCode XrayProtocol::start()
|
||||
{
|
||||
qDebug() << "XrayProtocol::start()";
|
||||
|
||||
m_connectivityProbeStarted = false;
|
||||
|
||||
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 +136,50 @@ ErrorCode XrayProtocol::start()
|
||||
qDebug() << "XrayProtocol: patched legacy inbound listen address to 127.0.0.1";
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
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())
|
||||
@@ -142,31 +202,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;
|
||||
@@ -191,15 +233,31 @@ ErrorCode XrayProtocol::startTun2Socks()
|
||||
if (!line.contains("[TCP]") && !line.contains("[UDP]"))
|
||||
qDebug() << "[tun2socks]:" << line;
|
||||
|
||||
if (line.contains("[STACK] tun://") && line.contains("<-> socks5://")) {
|
||||
disconnect(m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::readyReadStandardOutput, this, nullptr);
|
||||
if (line.contains("resource busy"))
|
||||
m_tunResourceBusy = true;
|
||||
|
||||
if (ErrorCode res = setupRouting(); res != ErrorCode::NoError) {
|
||||
stop();
|
||||
setLastError(res);
|
||||
} else {
|
||||
setConnectionState(Vpn::ConnectionState::Connected);
|
||||
}
|
||||
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);
|
||||
|
||||
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);
|
||||
@@ -207,15 +265,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++;
|
||||
@@ -331,3 +381,98 @@ ErrorCode XrayProtocol::setupRouting()
|
||||
},
|
||||
[]() { return ErrorCode::AmneziaServiceConnectionFailed; });
|
||||
}
|
||||
|
||||
bool XrayProtocol::probeServerReachable()
|
||||
{
|
||||
if (m_remoteAddress.isEmpty() || m_serverPort <= 0) {
|
||||
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(m_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)
|
||||
{
|
||||
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);
|
||||
proxy.setCapabilities(QNetworkProxy::TunnelingCapability | QNetworkProxy::HostNameLookupCapability);
|
||||
sock->setProxy(proxy);
|
||||
|
||||
auto *timeout = new QTimer(this);
|
||||
timeout->setSingleShot(true);
|
||||
|
||||
auto done = QSharedPointer<bool>::create(false);
|
||||
|
||||
auto finish = [=](bool ok) {
|
||||
if (*done)
|
||||
return;
|
||||
*done = true;
|
||||
timeout->stop();
|
||||
timeout->deleteLater();
|
||||
sock->abort();
|
||||
sock->deleteLater();
|
||||
onResult(ok);
|
||||
};
|
||||
|
||||
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(m_connectivityProbeTimeoutMs);
|
||||
sock->connectToHost(m_remoteAddress, static_cast<quint16>(m_serverPort));
|
||||
}
|
||||
|
||||
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 >= m_maxLivenessFailures) {
|
||||
qCritical() << "XrayProtocol: liveness check failed" << m_livenessFailures
|
||||
<< "times in a row, the tunnel is dead";
|
||||
stop();
|
||||
setLastError(ErrorCode::XrayConnectionLost);
|
||||
} else {
|
||||
qWarning() << "XrayProtocol: liveness check failed (" << m_livenessFailures << "/"
|
||||
<< m_maxLivenessFailures << ")";
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
m_livenessFailures = 0;
|
||||
m_livenessTimer->start(m_livenessIntervalMs);
|
||||
}
|
||||
|
||||
void XrayProtocol::stopLivenessMonitor()
|
||||
{
|
||||
if (m_livenessTimer)
|
||||
m_livenessTimer->stop();
|
||||
m_livenessFailures = 0;
|
||||
}
|
||||
|
||||
@@ -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,17 @@ private:
|
||||
ErrorCode setupRouting();
|
||||
ErrorCode startTun2Socks();
|
||||
|
||||
bool probeServerReachable();
|
||||
void runConnectivityProbe(std::function<void(bool)> onResult);
|
||||
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 +49,22 @@ private:
|
||||
int m_tun2socksRetryCount = 0;
|
||||
static constexpr int maxTun2SocksRetries = 5;
|
||||
static constexpr int tun2socksRetryDelayMs = 400;
|
||||
|
||||
bool m_connectivityProbeStarted = false;
|
||||
bool m_tunResourceBusy = false;
|
||||
|
||||
QTimer *m_livenessTimer = nullptr;
|
||||
int m_livenessFailures = 0;
|
||||
|
||||
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
|
||||
|
||||
@@ -71,6 +71,9 @@ namespace amnezia
|
||||
OpenSslFailed = 800,
|
||||
XrayExecutableCrashed = 803,
|
||||
Tun2SockExecutableCrashed = 804,
|
||||
XrayServerUnreachable = 805,
|
||||
XrayConnectivityCheckFailed = 806,
|
||||
XrayConnectionLost = 807,
|
||||
|
||||
// import and install errors
|
||||
ImportInvalidConfigError = 900,
|
||||
|
||||
@@ -59,6 +59,9 @@ 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("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;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <QFileInfo>
|
||||
#include <QLocalSocket>
|
||||
|
||||
#include "daemon.h"
|
||||
#include "daemonlocalserverconnection.h"
|
||||
#include "leakdetector.h"
|
||||
#include "logger.h"
|
||||
@@ -58,8 +59,11 @@ bool DaemonLocalServer::initialize() {
|
||||
|
||||
DaemonLocalServerConnection* connection =
|
||||
new DaemonLocalServerConnection(&m_server, socket);
|
||||
connect(socket, &QLocalSocket::disconnected, connection,
|
||||
&DaemonLocalServerConnection::deleteLater);
|
||||
connect(socket, &QLocalSocket::disconnected, connection, [connection]() {
|
||||
logger.debug() << "Client connection dropped, deactivating daemon";
|
||||
Daemon::instance()->deactivate(true);
|
||||
connection->deleteLater();
|
||||
});
|
||||
});
|
||||
|
||||
return true;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -318,7 +318,7 @@ bool KillSwitch::enableKillSwitch(const QJsonObject &configStr, int vpnAdapterIn
|
||||
LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("100.blockAll"), blockAll);
|
||||
LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("110.allowNets"), allowNets);
|
||||
LinuxFirewall::updateAllowNets(allownets);
|
||||
LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("120.blockNets"), blockNets);
|
||||
LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("120.blockNets"), blockAll);
|
||||
LinuxFirewall::updateBlockNets(blocknets);
|
||||
LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("130.allowMarkedXray"), true);
|
||||
LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("200.allowVPN"), true);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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};
|
||||
|
||||
Reference in New Issue
Block a user