#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 #include #include #include #include #include #include #include #include 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(configuration.value(amnezia::configKey::splitTunnelType).toInt()); m_remoteAddress = NetworkUtilities::getIPAddress(m_rawConfig.value(amnezia::configKey::hostName).toString()); #ifdef Q_OS_MACOS m_tunName = configuration.value("tunName").toString("utun22"); #else m_tunName = configuration.value("tunName").toString("tun2"); #endif const QString primaryDns = configuration.value(amnezia::configKey::dns1).toString(); m_dnsServers.push_back(QHostAddress(primaryDns)); if (primaryDns != amnezia::protocols::dns::amneziaDnsIp) { const QString secondaryDns = configuration.value(amnezia::configKey::dns2).toString(); m_dnsServers.push_back(QHostAddress(secondaryDns)); } 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()"; // 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 iface) { auto xrayStart = iface->xrayStart(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()"; IpcClient::withInterface([this](QSharedPointer iface) { auto restoreResolvers = iface->restoreResolvers(); if (!restoreResolvers.waitForFinished() || !restoreResolvers.returnValue()) qWarning() << "Failed to restore resolvers"; auto deleteTun = iface->deleteTun(m_tunName); if (!deleteTun.waitForFinished() || !deleteTun.returnValue()) qWarning() << "Failed to delete tun"; auto xrayStop = iface->xrayStop(); 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(); } setConnectionState(Vpn::ConnectionState::Disconnected); } 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::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 (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 (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 iface) -> ErrorCode { #ifdef Q_OS_WIN const int inetAdapterIndex = NetworkUtilities::AdapterIndexTo(QHostAddress(m_remoteAddress)); #endif 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; } auto updateResolvers = iface->updateResolvers(m_tunName, m_dnsServers); if (!updateResolvers.waitForFinished() || !updateResolvers.returnValue()) { qCritical() << "Failed to set DNS resolvers for TUN"; return ErrorCode::InternalError; } emit tunnelAddressesUpdated(m_vpnGateway, m_vpnLocalAddress); return ErrorCode::NoError; }, []() { return ErrorCode::AmneziaServiceConnectionFailed; }); }