Files
amnezia-client/client/core/protocols/xrayProtocol.cpp
T

290 lines
11 KiB
C++

#include "xrayProtocol.h"
#include "core/protocols/protocolUtils.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/constants/protocolConstants.h"
#include "core/utils/ipcClient.h"
#include "core/utils/networkUtilities.h"
#include "core/utils/serialization/serialization.h"
#include "ipc.h"
#include <QCryptographicHash>
#include <QJsonDocument>
#include <QTimer>
#include <QJsonObject>
#include <QNetworkInterface>
#include <QtCore/qlogging.h>
#include <QtCore/qobjectdefs.h>
#include <QtCore/qprocess.h>
#include <exception>
XrayProtocol::XrayProtocol(const QJsonObject &configuration, QObject *parent) : VpnProtocol(configuration, parent)
{
m_vpnGateway = amnezia::protocols::xray::defaultLocalAddr;
m_vpnLocalAddress = amnezia::protocols::xray::defaultLocalAddr;
m_routeGateway = NetworkUtilities::getGatewayAndIface().first;
m_routeMode = static_cast<amnezia::RouteMode>(configuration.value(amnezia::configKey::splitTunnelType).toInt());
m_remoteAddress = NetworkUtilities::getIPAddress(m_rawConfig.value(amnezia::configKey::hostName).toString());
m_tunName = configuration.value("tunName").toString();
if (m_tunName.isEmpty()) {
m_tunName = configuration.value("ifname").toString();
}
if (m_tunName.isEmpty()) {
#ifdef Q_OS_MACOS
m_tunName = QStringLiteral("utun22");
#else
m_tunName = QStringLiteral("tun2");
#endif
}
QJsonObject xrayConfiguration = configuration.value(ProtocolUtils::key_proto_config_data(Proto::Xray)).toObject();
if (xrayConfiguration.isEmpty()) {
xrayConfiguration = configuration.value(ProtocolUtils::key_proto_config_data(Proto::SSXray)).toObject();
}
if (xrayConfiguration.isEmpty()) {
qWarning() << "Xray config wrapper is empty";
m_xrayConfig = {};
}
m_xrayConfig = QJsonDocument::fromJson(xrayConfiguration.value(amnezia::configKey::config).toString().toUtf8()).object();
if (m_xrayConfig.isEmpty()) {
qWarning() << "Xray config string is not a valid JSON object";
m_xrayConfig = {};
}
}
XrayProtocol::~XrayProtocol()
{
qDebug() << "XrayProtocol::~XrayProtocol()";
XrayProtocol::stop();
}
ErrorCode XrayProtocol::start()
{
qDebug() << "XrayProtocol::start()";
m_phase = Phase::Active;
// 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;
try {
creds = amnezia::serialization::inbounds::EnsureInboundAuth(m_xrayConfig);
} catch (const std::exception &e) {
qCritical() << "EnsureInboundAuth failed:" << e.what();
return ErrorCode::InternalError;
}
m_socksUser = creds.username;
m_socksPassword = creds.password;
m_socksPort = creds.port;
QString xrayConfigStr = QJsonDocument(m_xrayConfig).toJson(QJsonDocument::Compact);
if (xrayConfigStr.isEmpty()) {
qCritical() << "Xray config is empty";
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);
qDebug() << "XrayProtocol: patched legacy fingerprint to"
<< 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";
}
return IpcClient::withInterface(
[&](QSharedPointer<IpcInterfaceReplica> iface) {
auto xrayStart = iface->xrayStart(m_tunName, xrayConfigStr);
if (!xrayStart.waitForFinished() || !xrayStart.returnValue()) {
qCritical() << "Failed to start xray";
return ErrorCode::XrayExecutableCrashed;
}
return startTun2Socks();
},
[]() { return ErrorCode::AmneziaServiceConnectionFailed; });
}
void XrayProtocol::stop()
{
qDebug() << "XrayProtocol::stop()";
if (m_phase != Phase::Active) {
return;
}
m_phase = Phase::Stopping;
IpcClient::withInterface([this](QSharedPointer<IpcInterfaceReplica> iface) {
auto deleteTun = iface->deleteTun(m_tunName);
if (!deleteTun.waitForFinished() || !deleteTun.returnValue())
qWarning() << "Failed to delete tun";
auto xrayStop = iface->xrayStop(m_tunName);
if (!xrayStop.waitForFinished() || !xrayStop.returnValue())
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();
}
m_phase = Phase::Inactive;
setConnectionState(Vpn::ConnectionState::Disconnected);
}
void XrayProtocol::setPrimary(const QJsonObject &config)
{
Q_UNUSED(config)
QMetaObject::invokeMethod(this, [this]() {
emit primaryReady();
}, Qt::QueuedConnection);
}
ErrorCode XrayProtocol::startTun2Socks()
{
m_tun2socksProcess = IpcClient::CreatePrivilegedProcess();
if (!m_tun2socksProcess->waitForSource()) {
return ErrorCode::AmneziaServiceConnectionFailed;
}
const QString proxyUrl = QString("socks5://%1:%2@127.0.0.1:%3").arg(m_socksUser, m_socksPassword, QString::number(m_socksPort));
m_tun2socksProcess->setProgram(PermittedProcess::Tun2Socks);
m_tun2socksProcess->setArguments({ "-device", QString("tun://%1").arg(m_tunName), "-proxy", proxyUrl });
connect(
m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::errorOccurred, this,
[this](QProcess::ProcessError error) {
if (error != QProcess::FailedToStart) {
// Other errors are reported via the finished signal or are transient.
return;
}
qCritical() << "Tun2socks failed to start";
stop();
setLastError(ErrorCode::Tun2SockExecutableMissing);
},
Qt::QueuedConnection);
connect(
m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::readyReadStandardError, this,
[this]() {
auto readAllStandardError = m_tun2socksProcess->readAllStandardError();
if (!readAllStandardError.waitForFinished()) {
qWarning() << "Failed to read output from tun2socks";
return;
}
const QString line = readAllStandardError.returnValue();
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 (ErrorCode res = setupRouting(); res != ErrorCode::NoError) {
stop();
setLastError(res);
} else {
setConnectionState(Vpn::ConnectionState::Connected);
}
}
},
Qt::QueuedConnection);
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");
}
}
if (m_phase == Phase::Active && resourceBusy
&& m_tun2socksRetryCount < maxTun2SocksRetries) {
m_tun2socksRetryCount++;
qWarning() << QString("Tun2socks: TUN resource busy, retrying (%1/%2) in %3ms...")
.arg(m_tun2socksRetryCount)
.arg(maxTun2SocksRetries)
.arg(tun2socksRetryDelayMs);
QTimer::singleShot(tun2socksRetryDelayMs, this, [this]() {
if (m_phase != Phase::Active) {
return;
}
if (ErrorCode err = startTun2Socks(); err != ErrorCode::NoError) {
stop();
setLastError(err);
}
});
return;
}
m_tun2socksRetryCount = 0;
if (exitStatus == QProcess::ExitStatus::CrashExit) {
qCritical() << "Tun2socks process crashed!";
} else {
qCritical() << QString("Tun2socks process was closed with %1 exit code").arg(exitCode);
}
stop();
setLastError(ErrorCode::Tun2SockExecutableCrashed);
},
Qt::QueuedConnection);
m_tun2socksProcess->start();
return ErrorCode::NoError;
}
ErrorCode XrayProtocol::setupRouting()
{
return IpcClient::withInterface(
[this](QSharedPointer<IpcInterfaceReplica> iface) -> ErrorCode {
#ifndef Q_OS_WIN
auto createTun = iface->createTun(m_tunName, amnezia::protocols::xray::defaultLocalAddr);
if (!createTun.waitForFinished() || !createTun.returnValue()) {
qCritical() << "Failed to assign IP address for TUN";
return ErrorCode::InternalError;
}
#else
Q_UNUSED(iface)
#endif
emit tunnelAddressesUpdated(m_vpnGateway, m_vpnLocalAddress);
return ErrorCode::NoError;
},
[]() { return ErrorCode::AmneziaServiceConnectionFailed; });
}