mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-10-23 03:26:59 +09:00
fix: rework xray settings - split client/server data, unify saving, fix tls (#3006)
* Feat: split xray client and server settings * remove test logs * in-place save and TLS pin * fixed xhttp/mkcp + tls * remove check version xray in start connect * address review findings for the settings split * reset namespace * reset namespace * remove template xray * fix: stop xray defaults from overwriting entered settings * revert dropdown selection refactor from the defaults fix * move save 'updateServerConfig' * remove comment
This commit is contained in:
35 files changed
+2691
-1282
No files matched your search
@@ -4,6 +4,7 @@
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QDateTime>
|
||||
#include <QThread>
|
||||
#include <QUuid>
|
||||
#include "logger.h"
|
||||
@@ -23,124 +24,49 @@
|
||||
namespace {
|
||||
Logger logger("XrayConfigurator");
|
||||
|
||||
QString normalizeXhttpMode(const QString &m) {
|
||||
const QString t = m.trimmed();
|
||||
if (t.isEmpty() || t.compare(QLatin1String("Auto"), Qt::CaseInsensitive) == 0) {
|
||||
return QStringLiteral("auto");
|
||||
}
|
||||
if (t.compare(QLatin1String("Packet-up"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("packet-up");
|
||||
if (t.compare(QLatin1String("Stream-up"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("stream-up");
|
||||
if (t.compare(QLatin1String("Stream-one"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("stream-one");
|
||||
return t.toLower();
|
||||
}
|
||||
|
||||
// Xray-core: empty → path; "None" in UI → omit (core default path)
|
||||
QString normalizeSessionSeqPlacement(const QString &p)
|
||||
{
|
||||
if (p.isEmpty() || p.compare(QLatin1String("None"), Qt::CaseInsensitive) == 0)
|
||||
return {};
|
||||
return p.toLower();
|
||||
}
|
||||
|
||||
QString normalizeUplinkDataPlacement(const QString &p)
|
||||
{
|
||||
if (p.isEmpty() || p.compare(QLatin1String("Body"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("body");
|
||||
if (p.compare(QLatin1String("Auto"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("auto");
|
||||
if (p.compare(QLatin1String("Query"), Qt::CaseInsensitive) == 0)
|
||||
// "Query" is not valid for uplink payload in splithttp; closest documented mode
|
||||
return QStringLiteral("header");
|
||||
return p.toLower();
|
||||
}
|
||||
|
||||
// splithttp: cookie | header | query | queryInHeader (not "body")
|
||||
QString normalizeXPaddingPlacement(const QString &p)
|
||||
{
|
||||
QString t = p.trimmed();
|
||||
if (t.isEmpty())
|
||||
return QString::fromLatin1(amnezia::protocols::xray::defaultXPaddingPlacement).toLower();
|
||||
if (t.compare(QLatin1String("Body"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("queryInHeader");
|
||||
if (t.contains(QLatin1String("queryInHeader"), Qt::CaseInsensitive)
|
||||
|| t.compare(QLatin1String("Query in header"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("queryInHeader");
|
||||
return t.toLower();
|
||||
}
|
||||
|
||||
// splithttp: repeat-x | tokenish
|
||||
QString normalizeXPaddingMethod(const QString &m)
|
||||
{
|
||||
QString t = m.trimmed();
|
||||
if (t.isEmpty() || t.compare(QLatin1String("Repeat-x"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("repeat-x");
|
||||
if (t.compare(QLatin1String("Tokenish"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("tokenish");
|
||||
if (t.compare(QLatin1String("Random"), Qt::CaseInsensitive) == 0
|
||||
|| t.compare(QLatin1String("Zero"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("repeat-x");
|
||||
return t.toLower();
|
||||
}
|
||||
|
||||
// xray wants int ranges as "from-to" string, not a {from,to} object.
|
||||
QString makeRangeString(const QString &minV, const QString &maxV)
|
||||
{
|
||||
return minV + QLatin1Char('-') + maxV;
|
||||
}
|
||||
|
||||
void putIntRangeIfAny(QJsonObject &obj, const char *key, QString minV, QString maxV, const char *fallbackMin,
|
||||
const char *fallbackMax)
|
||||
{
|
||||
if (minV.isEmpty() && maxV.isEmpty())
|
||||
return;
|
||||
if (minV.isEmpty())
|
||||
minV = QString::fromLatin1(fallbackMin);
|
||||
if (maxV.isEmpty())
|
||||
maxV = QString::fromLatin1(fallbackMax);
|
||||
obj[QString::fromUtf8(key)] = makeRangeString(minV, maxV);
|
||||
}
|
||||
|
||||
QString effectiveClientFlow(const amnezia::XrayServerConfig &srv)
|
||||
{
|
||||
const bool rawTransport = srv.transport.isEmpty() || srv.transport == QLatin1String("raw");
|
||||
const bool secureFlow =
|
||||
srv.security == QLatin1String("tls") || srv.security == QLatin1String("reality");
|
||||
return (rawTransport && secureFlow) ? srv.flow : QString();
|
||||
return amnezia::xrayEffective::clientFlow(srv);
|
||||
}
|
||||
|
||||
QString effectiveSecurity(const amnezia::XrayServerConfig &srv)
|
||||
{
|
||||
if (srv.transport == QLatin1String("mkcp") && srv.security == QLatin1String("reality")) {
|
||||
return QStringLiteral("none");
|
||||
}
|
||||
return srv.security;
|
||||
return amnezia::xrayEffective::security(srv);
|
||||
}
|
||||
|
||||
// Desktop applies this in XrayProtocol::start(); iOS/Android pass JSON straight to libxray — same fixes here.
|
||||
void sanitizeXrayNativeConfig(amnezia::ProtocolConfig &pc)
|
||||
{
|
||||
QString c = pc.nativeConfig();
|
||||
if (c.isEmpty()) {
|
||||
return;
|
||||
const QString before = pc.nativeConfig();
|
||||
const QString after = amnezia::sanitizeNativeConfig(before);
|
||||
if (after != before) {
|
||||
pc.setNativeConfig(after);
|
||||
}
|
||||
bool changed = false;
|
||||
if (c.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)) {
|
||||
c.replace(QLatin1String("Mozilla/5.0"), QString::fromLatin1(amnezia::protocols::xray::defaultFingerprint),
|
||||
Qt::CaseInsensitive);
|
||||
changed = true;
|
||||
}
|
||||
const QString legacyListen = QString::fromLatin1(amnezia::protocols::xray::defaultLocalAddr);
|
||||
const QString listenOk = QString::fromLatin1(amnezia::protocols::xray::defaultLocalListenAddr);
|
||||
if (c.contains(legacyListen)) {
|
||||
c.replace(legacyListen, listenOk);
|
||||
changed = true;
|
||||
|
||||
QString tlsFingerprintFromEnsureOutput(const QString &stdOut)
|
||||
{
|
||||
const QString prefix = QStringLiteral("amnezia_tls_fp=");
|
||||
const auto lines = stdOut.split(QLatin1Char('\n'));
|
||||
for (QString line : lines) {
|
||||
line = line.trimmed();
|
||||
if (!line.startsWith(prefix))
|
||||
continue;
|
||||
QString fp = line.mid(prefix.size());
|
||||
fp.remove(QLatin1Char(':'));
|
||||
fp.remove(QLatin1Char(' '));
|
||||
fp = fp.toLower();
|
||||
if (fp.size() != 64)
|
||||
return {};
|
||||
for (const QChar ch : fp) {
|
||||
const ushort u = ch.unicode();
|
||||
const bool hex = (u >= '0' && u <= '9') || (u >= 'a' && u <= 'f');
|
||||
if (!hex)
|
||||
return {};
|
||||
}
|
||||
if (changed) {
|
||||
pc.setNativeConfig(c);
|
||||
return fp;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
} // namespace
|
||||
|
||||
@@ -157,30 +83,6 @@ amnezia::ProtocolConfig XrayConfigurator::processConfigWithLocalSettings(const a
|
||||
return protocolConfig;
|
||||
}
|
||||
|
||||
ErrorCode XrayConfigurator::uploadServerConfigJson(const ServerCredentials &credentials, DockerContainer container,
|
||||
const DnsSettings &dnsSettings, const QJsonObject &serverConfig) const
|
||||
{
|
||||
const QString updatedConfig = QJsonDocument(serverConfig).toJson();
|
||||
ErrorCode errorCode = m_sshSession->uploadTextFileToContainer(
|
||||
container, credentials, updatedConfig, amnezia::protocols::xray::serverConfigPath,
|
||||
libssh::ScpOverwriteMode::ScpOverwriteExisting);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Failed to upload updated config";
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
const QString restartScript = QStringLiteral("sudo docker restart $CONTAINER_NAME");
|
||||
errorCode = m_sshSession->runScript(
|
||||
credentials,
|
||||
m_sshSession->replaceVars(restartScript,
|
||||
amnezia::genBaseVars(credentials, container, dnsSettings.primaryDns,
|
||||
dnsSettings.secondaryDns)));
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Failed to restart container";
|
||||
}
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
ErrorCode XrayConfigurator::readRealityKeyFiles(const DockerContainer container, const ServerCredentials &credentials,
|
||||
QString &outPublicKey, QString &outShortId) const
|
||||
{
|
||||
@@ -196,12 +98,17 @@ ErrorCode XrayConfigurator::readRealityKeyFiles(const DockerContainer container,
|
||||
if (fileError == ErrorCode::NoError && !out.isEmpty()) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
if (fileError != ErrorCode::NoError) {
|
||||
logger.error() << "Xray readRealityKeyFiles: ssh failed path=" << path
|
||||
<< "error=" << static_cast<int>(fileError);
|
||||
return fileError;
|
||||
}
|
||||
if (attempt < 2) {
|
||||
QThread::msleep(500);
|
||||
}
|
||||
}
|
||||
logger.error() << "Xray readRealityKeyFiles: failed path=" << path;
|
||||
return ErrorCode::XrayRealityKeysReadFailed;
|
||||
logger.error() << "Xray readRealityKeyFiles: key file empty path=" << path;
|
||||
return ErrorCode::XrayKeysReadFailed;
|
||||
};
|
||||
|
||||
ErrorCode errorCode = readKeyFile(QString::fromLatin1(amnezia::protocols::xray::PublicKeyPath), outPublicKey);
|
||||
@@ -232,10 +139,6 @@ ErrorCode XrayConfigurator::applyServerSettingsToRemote(const ServerCredentials
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
logger.info() << "Xray applyServerSettings: start"
|
||||
<< "container=" << static_cast<int>(container) << "host=" << credentials.hostName
|
||||
<< "transport=" << srv.transport << "security=" << srv.security << "port=" << srv.port
|
||||
<< "appendClient=" << appendNewClient;
|
||||
const QString flowValue = effectiveClientFlow(srv);
|
||||
QString realityPublicKey;
|
||||
QString realityShortId;
|
||||
@@ -264,72 +167,60 @@ ErrorCode XrayConfigurator::applyServerSettingsToRemote(const ServerCredentials
|
||||
}
|
||||
|
||||
QJsonObject serverConfig = doc.object();
|
||||
if (!serverConfig.contains(amnezia::protocols::xray::inbounds)) {
|
||||
logger.error() << "Server config missing 'inbounds' field";
|
||||
return ErrorCode::XrayServerConfigInvalid;
|
||||
}
|
||||
const QJsonObject originalConfig = serverConfig;
|
||||
|
||||
QJsonArray inbounds = serverConfig[amnezia::protocols::xray::inbounds].toArray();
|
||||
if (inbounds.isEmpty()) {
|
||||
logger.error() << "Server config has empty 'inbounds' array";
|
||||
return ErrorCode::XrayServerConfigInvalid;
|
||||
}
|
||||
|
||||
QJsonObject inbound = inbounds[0].toObject();
|
||||
if (!inbound.contains(amnezia::protocols::xray::settings)) {
|
||||
logger.error() << "Inbound missing 'settings' field";
|
||||
return ErrorCode::XrayServerConfigInvalid;
|
||||
}
|
||||
|
||||
QJsonObject settings = inbound[amnezia::protocols::xray::settings].toObject();
|
||||
if (!settings.contains(amnezia::protocols::xray::clients)) {
|
||||
settings[amnezia::protocols::xray::clients] = QJsonArray {};
|
||||
}
|
||||
|
||||
QJsonArray clients = settings[amnezia::protocols::xray::clients].toArray();
|
||||
QJsonArray clients = XrayServerConfig::clientsFromServerInboundJson(serverConfig);
|
||||
QString clientId;
|
||||
|
||||
if (appendNewClient) {
|
||||
clientId = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
QJsonObject clientEntry;
|
||||
clientEntry[amnezia::protocols::xray::id] = clientId;
|
||||
if (!flowValue.isEmpty()) {
|
||||
clientEntry[amnezia::protocols::xray::flow] = flowValue;
|
||||
const bool adoptingOwnAccount = !(xrayCfg->hasClientConfig() && !xrayCfg->clientConfig->id.isEmpty());
|
||||
|
||||
QString ownClientId;
|
||||
if (readContainerKeyFile(container, credentials, QString::fromLatin1(amnezia::protocols::xray::uuidPath),
|
||||
ownClientId)
|
||||
!= ErrorCode::NoError) {
|
||||
ownClientId.clear();
|
||||
}
|
||||
|
||||
const int existingIndex =
|
||||
adoptingOwnAccount ? XrayServerConfig::indexOfClient(clients, ownClientId) : -1;
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
clientId = ownClientId;
|
||||
clients[existingIndex] =
|
||||
XrayServerConfig::applyFlowToClient(clients[existingIndex].toObject(), flowValue);
|
||||
} else {
|
||||
const bool takeOverOwnAccount = adoptingOwnAccount && !ownClientId.isEmpty();
|
||||
clientId = takeOverOwnAccount ? ownClientId : QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
clients.append(XrayServerConfig::makeClientEntry(clientId, flowValue));
|
||||
}
|
||||
clients.append(clientEntry);
|
||||
} else {
|
||||
if (clients.isEmpty()) {
|
||||
logger.error() << "Server config has no VLESS clients";
|
||||
return ErrorCode::XrayServerNoVlessClients;
|
||||
}
|
||||
clientId = clients[0].toObject()[amnezia::protocols::xray::id].toString();
|
||||
clientId = XrayServerConfig::firstClientId(clients);
|
||||
if (clientId.isEmpty()) {
|
||||
logger.error() << "Server config VLESS client has empty id";
|
||||
return ErrorCode::XrayServerNoVlessClients;
|
||||
}
|
||||
QJsonArray updatedClients;
|
||||
for (const QJsonValue &v : clients) {
|
||||
QJsonObject c = v.toObject();
|
||||
if (flowValue.isEmpty()) {
|
||||
c.remove(amnezia::protocols::xray::flow);
|
||||
} else {
|
||||
c[amnezia::protocols::xray::flow] = flowValue;
|
||||
}
|
||||
updatedClients.append(c);
|
||||
}
|
||||
clients = updatedClients;
|
||||
clients = XrayServerConfig::applyFlowToClients(clients, flowValue);
|
||||
}
|
||||
|
||||
settings[amnezia::protocols::xray::clients] = clients;
|
||||
inbound[amnezia::protocols::xray::settings] = settings;
|
||||
inbounds[0] = inbound;
|
||||
serverConfig[amnezia::protocols::xray::inbounds] = inbounds;
|
||||
const XrayServerJsonStatus writeStatus =
|
||||
XrayServerConfig::setClientsInServerInboundJson(serverConfig, clients);
|
||||
if (writeStatus != XrayServerJsonStatus::Ok) {
|
||||
return ErrorCode::XrayServerConfigInvalid;
|
||||
}
|
||||
|
||||
errorCode = uploadServerConfigJson(credentials, container, dnsSettings, serverConfig);
|
||||
const QString listenPort = srv.port.isEmpty() ? QString::fromLatin1(amnezia::protocols::xray::defaultPort)
|
||||
: srv.port;
|
||||
if (serverConfig != originalConfig) {
|
||||
errorCode = uploadServerConfigAtomically(credentials, container, listenPort, serverConfig);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Xray applyServerSettings: upload/restart failed, error=" << static_cast<int>(errorCode);
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
logger.info() << "Xray applyServerSettings: server config uploaded and container restarted";
|
||||
|
||||
if (outClientId) {
|
||||
@@ -337,7 +228,8 @@ ErrorCode XrayConfigurator::applyServerSettingsToRemote(const ServerCredentials
|
||||
}
|
||||
|
||||
XrayProtocolConfig updated =
|
||||
buildClientProtocolConfig(credentials, container, srv, clientId, errorCode, realityPublicKey, realityShortId);
|
||||
buildClientProtocolConfig(credentials, container, srv, clientId, errorCode,
|
||||
realityPublicKey, realityShortId);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Xray applyServerSettings: buildClientProtocolConfig failed, error="
|
||||
<< static_cast<int>(errorCode);
|
||||
@@ -360,19 +252,24 @@ ErrorCode XrayConfigurator::readContainerKeyFile(DockerContainer container, cons
|
||||
if (fileError == ErrorCode::NoError && !out.isEmpty()) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
if (fileError != ErrorCode::NoError) {
|
||||
logger.error() << "Xray readContainerKeyFile: ssh failed path=" << path
|
||||
<< "error=" << static_cast<int>(fileError);
|
||||
return fileError;
|
||||
}
|
||||
if (attempt < 2) {
|
||||
QThread::msleep(500);
|
||||
}
|
||||
}
|
||||
logger.error() << "Xray readContainerKeyFile: failed path=" << path;
|
||||
return ErrorCode::XrayRealityKeysReadFailed;
|
||||
logger.error() << "Xray readContainerKeyFile: key file empty path=" << path;
|
||||
return ErrorCode::XrayKeysReadFailed;
|
||||
}
|
||||
|
||||
ErrorCode XrayConfigurator::writeServerConfigForSetup(const ServerCredentials &credentials, DockerContainer container,
|
||||
ContainerConfig &containerConfig, const DnsSettings &dnsSettings)
|
||||
ContainerConfig &containerConfig, const DnsSettings &dnsSettings,
|
||||
bool useAtomicApply)
|
||||
{
|
||||
Q_UNUSED(dnsSettings);
|
||||
namespace px = amnezia::protocols::xray;
|
||||
|
||||
const auto *xrayCfg = containerConfig.protocolConfig.as<XrayProtocolConfig>();
|
||||
if (!xrayCfg) {
|
||||
@@ -385,82 +282,72 @@ ErrorCode XrayConfigurator::writeServerConfigForSetup(const ServerCredentials &c
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
logger.info() << "Xray writeServerConfigForSetup: start container=" << static_cast<int>(container)
|
||||
<< "transport=" << srv.transport << "security=" << srv.security << "port=" << srv.port;
|
||||
|
||||
ErrorCode errorCode = ErrorCode::NoError;
|
||||
|
||||
QString clientId;
|
||||
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(px::uuidPath), clientId);
|
||||
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(amnezia::protocols::xray::uuidPath), clientId);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
const QString securityEff = effectiveSecurity(srv);
|
||||
QString tlsPin;
|
||||
if (securityEff == QLatin1String("tls")) {
|
||||
errorCode = ensureTlsCertificate(credentials, container, tlsPin);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
|
||||
QString realityPrivateKey;
|
||||
QString realityPublicKey;
|
||||
QString realityShortId;
|
||||
if (securityEff == QLatin1String("reality")) {
|
||||
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(px::PrivateKeyPath), realityPrivateKey);
|
||||
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(amnezia::protocols::xray::PrivateKeyPath), realityPrivateKey);
|
||||
if (errorCode != ErrorCode::NoError)
|
||||
return errorCode;
|
||||
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(px::PublicKeyPath), realityPublicKey);
|
||||
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(amnezia::protocols::xray::PublicKeyPath), realityPublicKey);
|
||||
if (errorCode != ErrorCode::NoError)
|
||||
return errorCode;
|
||||
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(px::shortidPath), realityShortId);
|
||||
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(amnezia::protocols::xray::shortidPath), realityShortId);
|
||||
if (errorCode != ErrorCode::NoError)
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
QJsonObject streamSettings = buildStreamSettings(srv, clientId);
|
||||
if (securityEff == QLatin1String("reality")) {
|
||||
const QString siteEff = srv.site.isEmpty() ? QString::fromLatin1(px::defaultSite) : srv.site;
|
||||
const QString sniEff = srv.sni.isEmpty() ? siteEff : srv.sni;
|
||||
const QString fpEff = srv.fingerprint.isEmpty() ? QString::fromLatin1(px::defaultFingerprint) : srv.fingerprint;
|
||||
QJsonObject rs;
|
||||
rs[QStringLiteral("dest")] = siteEff + QStringLiteral(":443");
|
||||
rs[px::fingerprint] = fpEff;
|
||||
rs[QStringLiteral("privateKey")] = realityPrivateKey;
|
||||
rs[px::serverNames] = QJsonArray { sniEff };
|
||||
rs[QStringLiteral("shortIds")] = QJsonArray { realityShortId };
|
||||
streamSettings[px::realitySettings] = rs;
|
||||
}
|
||||
|
||||
QJsonObject clientEntry;
|
||||
clientEntry[px::id] = clientId;
|
||||
const QString flowValue = effectiveClientFlow(srv);
|
||||
if (!flowValue.isEmpty()) {
|
||||
clientEntry[px::flow] = flowValue;
|
||||
}
|
||||
|
||||
QJsonObject settings;
|
||||
settings[px::clients] = QJsonArray { clientEntry };
|
||||
settings[QStringLiteral("decryption")] = QStringLiteral("none");
|
||||
|
||||
QJsonObject inbound;
|
||||
inbound[px::port] = srv.port.isEmpty() ? QString(px::defaultPort).toInt() : srv.port.toInt();
|
||||
inbound[QStringLiteral("protocol")] = QStringLiteral("vless");
|
||||
inbound[px::settings] = settings;
|
||||
inbound[px::streamSettings] = streamSettings;
|
||||
|
||||
QJsonObject serverConfig;
|
||||
serverConfig[QStringLiteral("log")] = QJsonObject { { QStringLiteral("loglevel"), QStringLiteral("error") } };
|
||||
serverConfig[px::inbounds] = QJsonArray { inbound };
|
||||
serverConfig[px::outbounds] =
|
||||
QJsonArray { QJsonObject { { QStringLiteral("protocol"), QStringLiteral("freedom") } } };
|
||||
|
||||
const QString json = QString::fromUtf8(QJsonDocument(serverConfig).toJson());
|
||||
errorCode = m_sshSession->uploadTextFileToContainer(container, credentials, json,
|
||||
QString::fromLatin1(px::serverConfigPath),
|
||||
libssh::ScpOverwriteMode::ScpOverwriteExisting);
|
||||
const QJsonArray clients = collectServerClients(credentials, container, flowValue, clientId, errorCode);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Xray writeServerConfigForSetup: upload failed, error=" << static_cast<int>(errorCode);
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
const QString portEff = srv.port.isEmpty() ? QString::fromLatin1(amnezia::protocols::xray::defaultPort) : srv.port;
|
||||
|
||||
|
||||
XrayServerInboundInputs inputs;
|
||||
inputs.clients = clients;
|
||||
inputs.realityPrivateKey = realityPrivateKey;
|
||||
inputs.realityShortId = realityShortId;
|
||||
|
||||
const QJsonObject inboundJson = srv.toServerInboundJson(inputs);
|
||||
if (useAtomicApply) {
|
||||
errorCode = uploadServerConfigAtomically(credentials, container, portEff, inboundJson);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
} else {
|
||||
const QString json = QString::fromUtf8(QJsonDocument(inboundJson).toJson());
|
||||
errorCode = m_sshSession->uploadTextFileToContainer(container, credentials, json,
|
||||
QString::fromLatin1(amnezia::protocols::xray::serverConfigPath),
|
||||
libssh::ScpOverwriteMode::ScpOverwriteExisting);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
|
||||
XrayProtocolConfig updated =
|
||||
buildClientProtocolConfig(credentials, container, srv, clientId, errorCode, realityPublicKey, realityShortId);
|
||||
buildClientProtocolConfig(credentials, container, srv, clientId, errorCode,
|
||||
realityPublicKey, realityShortId, tlsPin);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Xray writeServerConfigForSetup: buildClientProtocolConfig failed, error="
|
||||
<< static_cast<int>(errorCode);
|
||||
@@ -471,6 +358,217 @@ ErrorCode XrayConfigurator::writeServerConfigForSetup(const ServerCredentials &c
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
QJsonArray XrayConfigurator::collectServerClients(const ServerCredentials &credentials, DockerContainer container,
|
||||
const QString &flowValue, const QString &fallbackClientId,
|
||||
ErrorCode &outError) const
|
||||
{
|
||||
outError = ErrorCode::NoError;
|
||||
const QString configPath = QString::fromLatin1(amnezia::protocols::xray::serverConfigPath);
|
||||
|
||||
QString currentConfig;
|
||||
for (int attempt = 0; attempt < 3; ++attempt) {
|
||||
ErrorCode readError = ErrorCode::NoError;
|
||||
currentConfig = m_sshSession->getTextFileFromContainer(container, credentials, configPath, readError);
|
||||
if (readError == ErrorCode::NoError && !currentConfig.trimmed().isEmpty()) {
|
||||
break;
|
||||
}
|
||||
currentConfig.clear();
|
||||
if (attempt < 2) {
|
||||
QThread::msleep(500);
|
||||
}
|
||||
}
|
||||
|
||||
QJsonArray existing;
|
||||
if (currentConfig.trimmed().isEmpty()) {
|
||||
QString probeOut;
|
||||
auto collect = [&probeOut](const QString &data, libssh::Client &) {
|
||||
probeOut += data + "\n";
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
const QString probe =
|
||||
QStringLiteral("if [ -s %1 ]; then echo amnezia_config=present; else echo amnezia_config=absent; fi")
|
||||
.arg(configPath);
|
||||
const ErrorCode probeError = m_sshSession->runContainerScript(credentials, container, probe, collect, collect);
|
||||
|
||||
if (probeError != ErrorCode::NoError || !probeOut.contains(QLatin1String("amnezia_config="))) {
|
||||
outError = ErrorCode::XrayServerConfigInvalid;
|
||||
return {};
|
||||
}
|
||||
if (probeOut.contains(QLatin1String("amnezia_config=present"))) {
|
||||
outError = ErrorCode::XrayServerConfigInvalid;
|
||||
return {};
|
||||
}
|
||||
} else {
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(currentConfig.toUtf8());
|
||||
if (!doc.isObject()) {
|
||||
outError = ErrorCode::XrayServerConfigInvalid;
|
||||
return {};
|
||||
}
|
||||
existing = XrayServerConfig::clientsFromServerInboundJson(doc.object());
|
||||
}
|
||||
|
||||
QJsonArray clients =
|
||||
XrayServerConfig::applyFlowToClients(existing, flowValue, XrayClientListFilter::DropWithoutId);
|
||||
|
||||
if (!fallbackClientId.isEmpty()
|
||||
&& XrayServerConfig::indexOfClient(clients, fallbackClientId) < 0) {
|
||||
clients.append(XrayServerConfig::makeClientEntry(fallbackClientId, flowValue));
|
||||
}
|
||||
|
||||
return clients;
|
||||
}
|
||||
|
||||
ErrorCode XrayConfigurator::ensureTlsCertificate(const ServerCredentials &credentials, DockerContainer container,
|
||||
QString &outFingerprint) const
|
||||
{
|
||||
outFingerprint.clear();
|
||||
|
||||
QString stdOut;
|
||||
auto collect = [&stdOut](const QString &data, libssh::Client &) {
|
||||
stdOut += data + QLatin1Char('\n');
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
|
||||
const QString certPath = QString::fromLatin1(amnezia::protocols::xray::tlsCertPath);
|
||||
const QString keyPath = QString::fromLatin1(amnezia::protocols::xray::tlsKeyPath);
|
||||
const QString script = QStringLiteral(
|
||||
"has_key() { [ -f \"$1\" ] && [ -n \"$(tr -d '[:space:]' < \"$1\" 2>/dev/null)\" ]; }\n"
|
||||
"CERT=%1\n"
|
||||
"KEY=%2\n"
|
||||
"if has_key \"$CERT\" && has_key \"$KEY\"; then echo amnezia_tls=present;\n"
|
||||
"else\n"
|
||||
" openssl req -x509 -newkey rsa:2048 -nodes -days 3650 -subj \"/CN=amnezia-xray\" "
|
||||
"-keyout \"$KEY\" -out \"$CERT\" || true\n"
|
||||
" if has_key \"$CERT\" && has_key \"$KEY\"; then echo amnezia_tls=created;\n"
|
||||
" else echo amnezia_tls=failed; exit 1; fi\n"
|
||||
"fi\n"
|
||||
"FP=$(openssl x509 -noout -fingerprint -sha256 -in \"$CERT\" 2>/dev/null "
|
||||
"| sed 's/.*Fingerprint=//' | tr -d ':' | tr 'A-F' 'a-f')\n"
|
||||
"if [ -z \"$FP\" ]; then echo amnezia_tls=failed; exit 1; fi\n"
|
||||
"echo amnezia_tls_fp=$FP\n"
|
||||
"exit 0\n")
|
||||
.arg(certPath, keyPath);
|
||||
|
||||
const ErrorCode scriptError = m_sshSession->runContainerScript(credentials, container, script, collect, collect);
|
||||
const bool present = stdOut.contains(QLatin1String("amnezia_tls=present"));
|
||||
const bool created = stdOut.contains(QLatin1String("amnezia_tls=created"));
|
||||
outFingerprint = tlsFingerprintFromEnsureOutput(stdOut);
|
||||
if (scriptError != ErrorCode::NoError || (!present && !created) || outFingerprint.isEmpty()) {
|
||||
outFingerprint.clear();
|
||||
return ErrorCode::XrayTlsNotSupported;
|
||||
}
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode XrayConfigurator::uploadServerConfigAtomically(const ServerCredentials &credentials,
|
||||
DockerContainer container, const QString &listenPort,
|
||||
const QJsonObject &serverConfig) const
|
||||
{
|
||||
const QString livePath = QString::fromLatin1(amnezia::protocols::xray::serverConfigPath);
|
||||
const QString stagedPath = QStringLiteral("/opt/amnezia/xray/server.new.json");
|
||||
|
||||
const QString json = QString::fromUtf8(QJsonDocument(serverConfig).toJson());
|
||||
ErrorCode errorCode = m_sshSession->uploadTextFileToContainer(container, credentials, json, stagedPath,
|
||||
libssh::ScpOverwriteMode::ScpOverwriteExisting);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
const QString swapScript = QStringLiteral(
|
||||
"LIVE=%1\n"
|
||||
"STAGED=%2\n"
|
||||
"BACKUP=\"$LIVE.bak\"\n"
|
||||
"if [ ! -s \"$STAGED\" ]; then echo \"amnezia_apply=missing\"; exit 0; fi\n"
|
||||
"if ! xray -test -format json -config \"$STAGED\" > /tmp/xray_test.log 2>&1; then\n"
|
||||
" echo \"amnezia_apply=invalid\"\n"
|
||||
" rm -f \"$STAGED\"\n"
|
||||
" exit 0\n"
|
||||
"fi\n"
|
||||
"if [ -f \"$LIVE\" ]; then cp \"$LIVE\" \"$BACKUP\"; fi\n"
|
||||
"mv \"$STAGED\" \"$LIVE\"\n"
|
||||
"echo \"amnezia_apply=swapped\"\n")
|
||||
.arg(livePath, stagedPath);
|
||||
|
||||
QString swapOut;
|
||||
auto collectSwap = [&swapOut](const QString &data, libssh::Client &) {
|
||||
swapOut += data + "\n";
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
errorCode = m_sshSession->runContainerScript(credentials, container, swapScript, collectSwap, collectSwap);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
if (swapOut.contains(QLatin1String("amnezia_apply=missing"))) {
|
||||
return ErrorCode::XrayServerConfigRejected;
|
||||
}
|
||||
if (swapOut.contains(QLatin1String("amnezia_apply=invalid"))) {
|
||||
return ErrorCode::XrayServerConfigRejected;
|
||||
}
|
||||
if (!swapOut.contains(QLatin1String("amnezia_apply=swapped"))) {
|
||||
return ErrorCode::XrayServerConfigInvalid;
|
||||
}
|
||||
|
||||
if (restartXrayContainer(credentials, container) && xrayProcessIsUp(credentials, container)) {
|
||||
m_sshSession->runContainerScript(credentials, container,
|
||||
QStringLiteral("rm -f %1.bak").arg(livePath));
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
const QString restoreScript = QStringLiteral(
|
||||
"LIVE=%1\n"
|
||||
"BACKUP=\"$LIVE.bak\"\n"
|
||||
"if [ -f \"$BACKUP\" ]; then cp \"$BACKUP\" \"$LIVE\"; echo amnezia_restore=done; "
|
||||
"else echo amnezia_restore=nobackup; fi\n")
|
||||
.arg(livePath);
|
||||
QString restoreOut;
|
||||
auto collectRestore = [&restoreOut](const QString &data, libssh::Client &) {
|
||||
restoreOut += data + "\n";
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
m_sshSession->runContainerScript(credentials, container, restoreScript, collectRestore, collectRestore);
|
||||
if (!restoreOut.contains(QLatin1String("amnezia_restore=done"))) {
|
||||
return ErrorCode::XrayServerNotServing;
|
||||
}
|
||||
if (restartXrayContainer(credentials, container) && xrayProcessIsUp(credentials, container)) {
|
||||
return ErrorCode::XrayServerConfigRolledBack;
|
||||
}
|
||||
return ErrorCode::XrayServerNotServing;
|
||||
}
|
||||
|
||||
bool XrayConfigurator::restartXrayContainer(const ServerCredentials &credentials, DockerContainer container) const
|
||||
{
|
||||
const ErrorCode errorCode = m_sshSession->runScript(
|
||||
credentials,
|
||||
SshSession::replaceVars(QStringLiteral("sudo docker restart $CONTAINER_NAME"),
|
||||
amnezia::genBaseVars(credentials, container, QString(), QString())));
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool XrayConfigurator::xrayProcessIsUp(const ServerCredentials &credentials, DockerContainer container) const
|
||||
{
|
||||
const QString script = QStringLiteral(
|
||||
"for i in 1 2 3 4 5 6; do\n"
|
||||
" if pgrep xray >/dev/null 2>&1 || ps 2>/dev/null | grep -v grep | grep -q \"[x]ray\"; then\n"
|
||||
" echo amnezia_serving=yes\n"
|
||||
" exit 0\n"
|
||||
" fi\n"
|
||||
" sleep 1\n"
|
||||
"done\n"
|
||||
"echo amnezia_serving=no\n");
|
||||
QString out;
|
||||
auto collect = [&out](const QString &data, libssh::Client &) {
|
||||
out += data + "\n";
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
const ErrorCode errorCode = m_sshSession->runContainerScript(credentials, container, script, collect, collect);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return false;
|
||||
}
|
||||
return out.contains(QLatin1String("amnezia_serving=yes"));
|
||||
}
|
||||
QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentials, DockerContainer container,
|
||||
const ContainerConfig &containerConfig,
|
||||
const DnsSettings &dnsSettings,
|
||||
@@ -489,17 +587,21 @@ QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentia
|
||||
|
||||
XrayProtocolConfig XrayConfigurator::buildClientProtocolConfig(const ServerCredentials &credentials,
|
||||
DockerContainer container,
|
||||
const XrayServerConfig &srv, const QString &clientId,
|
||||
const XrayServerConfig &srv,
|
||||
const QString &clientId,
|
||||
ErrorCode &errorCode,
|
||||
const QString &prefetchedRealityPublicKey,
|
||||
const QString &prefetchedRealityShortId) const
|
||||
const QString &prefetchedRealityShortId,
|
||||
const QString &prefetchedTlsPin) const
|
||||
{
|
||||
QString xrayPublicKey = prefetchedRealityPublicKey;
|
||||
QString xrayShortId = prefetchedRealityShortId;
|
||||
QString tlsPin = prefetchedTlsPin;
|
||||
|
||||
const QString securityEff = effectiveSecurity(srv);
|
||||
|
||||
if (securityEff == QLatin1String("reality")) {
|
||||
|
||||
if (securityEff == QLatin1String(amnezia::protocols::xray::securityReality)) {
|
||||
if (xrayPublicKey.isEmpty() || xrayShortId.isEmpty()) {
|
||||
errorCode = readRealityKeyFiles(container, credentials, xrayPublicKey, xrayShortId);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
@@ -508,243 +610,40 @@ XrayProtocolConfig XrayConfigurator::buildClientProtocolConfig(const ServerCrede
|
||||
}
|
||||
}
|
||||
|
||||
QJsonObject userObj;
|
||||
userObj[amnezia::protocols::xray::id] = clientId;
|
||||
userObj[amnezia::protocols::xray::encryption] = QStringLiteral("none");
|
||||
const QString flowValue = effectiveClientFlow(srv);
|
||||
if (!flowValue.isEmpty()) {
|
||||
userObj[amnezia::protocols::xray::flow] = flowValue;
|
||||
if (securityEff == QLatin1String(amnezia::protocols::xray::securityTls) && !srv.isThirdPartyConfig) {
|
||||
if (tlsPin.isEmpty()) {
|
||||
errorCode = ensureTlsCertificate(credentials, container, tlsPin);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
if (tlsPin.isEmpty()) {
|
||||
errorCode = ErrorCode::XrayTlsNotSupported;
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
QJsonObject vnextEntry;
|
||||
vnextEntry[amnezia::protocols::xray::address] = credentials.hostName;
|
||||
vnextEntry[amnezia::protocols::xray::port] =
|
||||
srv.port.isEmpty() ? QString(amnezia::protocols::xray::defaultPort).toInt() : srv.port.toInt();
|
||||
vnextEntry[amnezia::protocols::xray::users] = QJsonArray { userObj };
|
||||
|
||||
QJsonObject outboundSettings;
|
||||
outboundSettings[amnezia::protocols::xray::vnext] = QJsonArray { vnextEntry };
|
||||
|
||||
QJsonObject outbound;
|
||||
outbound[QStringLiteral("protocol")] = QStringLiteral("vless");
|
||||
outbound[amnezia::protocols::xray::settings] = outboundSettings;
|
||||
|
||||
QJsonObject streamObj = buildStreamSettings(srv, clientId);
|
||||
if (securityEff == QLatin1String("reality")) {
|
||||
QJsonObject rs = streamObj[amnezia::protocols::xray::realitySettings].toObject();
|
||||
rs[amnezia::protocols::xray::publicKey] = xrayPublicKey;
|
||||
rs[amnezia::protocols::xray::shortId] = xrayShortId;
|
||||
rs[amnezia::protocols::xray::spiderX] = QString();
|
||||
streamObj[amnezia::protocols::xray::realitySettings] = rs;
|
||||
}
|
||||
|
||||
outbound[amnezia::protocols::xray::streamSettings] = streamObj;
|
||||
|
||||
QJsonObject inboundObj;
|
||||
inboundObj[QStringLiteral("listen")] = amnezia::protocols::xray::defaultLocalListenAddr;
|
||||
inboundObj[amnezia::protocols::xray::port] = amnezia::protocols::xray::defaultLocalProxyPort;
|
||||
inboundObj[QStringLiteral("protocol")] = QStringLiteral("socks");
|
||||
inboundObj[amnezia::protocols::xray::settings] = QJsonObject { { QStringLiteral("udp"), true } };
|
||||
|
||||
QJsonObject clientJson;
|
||||
clientJson[QStringLiteral("log")] = QJsonObject { { QStringLiteral("loglevel"), QStringLiteral("error") } };
|
||||
clientJson[amnezia::protocols::xray::inbounds] = QJsonArray { inboundObj };
|
||||
clientJson[amnezia::protocols::xray::outbounds] = QJsonArray { outbound };
|
||||
|
||||
const QString config = QString::fromUtf8(QJsonDocument(clientJson).toJson(QJsonDocument::Compact));
|
||||
|
||||
XrayProtocolConfig protocolConfig;
|
||||
protocolConfig.serverConfig = srv;
|
||||
|
||||
XrayClientOutboundInputs inputs;
|
||||
inputs.serverAddress = credentials.hostName;
|
||||
inputs.clientId = clientId;
|
||||
inputs.realityPublicKey = xrayPublicKey;
|
||||
inputs.realityShortId = xrayShortId;
|
||||
inputs.tlsPinnedPeerCertSha256 = tlsPin;
|
||||
|
||||
XrayClientConfig clientConfig;
|
||||
clientConfig.nativeConfig = config;
|
||||
clientConfig.localPort = QString(amnezia::protocols::xray::defaultLocalProxyPort);
|
||||
const QJsonObject clientOutbound = protocolConfig.toClientOutboundJson(inputs);
|
||||
clientConfig.nativeConfig = QString::fromUtf8(QJsonDocument(clientOutbound).toJson(QJsonDocument::Compact));
|
||||
clientConfig.localPort = QString::fromLatin1(amnezia::protocols::xray::defaultLocalProxyPort);
|
||||
clientConfig.id = clientId;
|
||||
protocolConfig.setClientConfig(clientConfig);
|
||||
|
||||
return protocolConfig;
|
||||
}
|
||||
|
||||
QJsonObject XrayConfigurator::buildStreamSettings(const XrayServerConfig &srv, const QString &clientId) const
|
||||
{
|
||||
QJsonObject streamSettings;
|
||||
const auto &xhttp = srv.xhttp;
|
||||
const auto &mkcp = srv.mkcp;
|
||||
namespace px = amnezia::protocols::xray;
|
||||
|
||||
QString networkValue = QStringLiteral("tcp");
|
||||
if (srv.transport == QLatin1String("xhttp"))
|
||||
networkValue = QStringLiteral("xhttp");
|
||||
else if (srv.transport == QLatin1String("mkcp"))
|
||||
networkValue = QStringLiteral("kcp");
|
||||
streamSettings[px::network] = networkValue;
|
||||
|
||||
const QString securityEff = effectiveSecurity(srv);
|
||||
streamSettings[px::security] = securityEff;
|
||||
|
||||
if (securityEff == QLatin1String("tls")) {
|
||||
QJsonObject tlsSettings;
|
||||
const QString sniEff = srv.sni.isEmpty() ? QString::fromLatin1(px::defaultSni) : srv.sni;
|
||||
tlsSettings[px::serverName] = sniEff;
|
||||
const QString alpnEff = srv.alpn.isEmpty() ? QString::fromLatin1(px::defaultAlpn) : srv.alpn;
|
||||
QJsonArray alpnArray;
|
||||
for (const QString &a : alpnEff.split(QLatin1Char(','))) {
|
||||
QString t = a.trimmed();
|
||||
if (t.isEmpty())
|
||||
continue;
|
||||
if (t.compare(QLatin1String("HTTP/2"), Qt::CaseInsensitive) == 0)
|
||||
t = QStringLiteral("h2");
|
||||
else if (t.compare(QLatin1String("HTTP/1.1"), Qt::CaseInsensitive) == 0)
|
||||
t = QStringLiteral("http/1.1");
|
||||
alpnArray.append(t);
|
||||
}
|
||||
if (!alpnArray.isEmpty())
|
||||
tlsSettings[QStringLiteral("alpn")] = alpnArray;
|
||||
const QString fpEff = srv.fingerprint.isEmpty() ? QString::fromLatin1(px::defaultFingerprint) : srv.fingerprint;
|
||||
tlsSettings[px::fingerprint] = fpEff;
|
||||
streamSettings[QStringLiteral("tlsSettings")] = tlsSettings;
|
||||
}
|
||||
|
||||
if (securityEff == QLatin1String("reality")) {
|
||||
QJsonObject realSettings;
|
||||
const QString fpEff = srv.fingerprint.isEmpty() ? QString::fromLatin1(px::defaultFingerprint) : srv.fingerprint;
|
||||
realSettings[px::fingerprint] = fpEff;
|
||||
const QString sniEff = srv.sni.isEmpty() ? QString::fromLatin1(px::defaultSni) : srv.sni;
|
||||
realSettings[px::serverName] = sniEff;
|
||||
streamSettings[px::realitySettings] = realSettings;
|
||||
}
|
||||
|
||||
// XHTTP — JSON must match Xray-core SplitHTTPConfig (flat xPadding fields, see transport_internet.go)
|
||||
if (srv.transport == QLatin1String("xhttp")) {
|
||||
QJsonObject xo;
|
||||
const QString hostEff = xhttp.host.isEmpty() ? QString::fromLatin1(px::defaultXhttpHost) : xhttp.host;
|
||||
xo[QStringLiteral("host")] = hostEff;
|
||||
if (!xhttp.path.isEmpty())
|
||||
xo[QStringLiteral("path")] = xhttp.path;
|
||||
QString modeEff = normalizeXhttpMode(xhttp.mode);
|
||||
if (modeEff == QLatin1String("auto") || modeEff == QLatin1String("packet-up")) {
|
||||
modeEff = QStringLiteral("stream-one");
|
||||
}
|
||||
xo[QStringLiteral("mode")] = modeEff;
|
||||
|
||||
// No "Host" in headers: xray rejects it when the top-level "host" field is set.
|
||||
|
||||
const QString methodEff =
|
||||
xhttp.uplinkMethod.isEmpty() ? QString::fromLatin1(px::defaultXhttpUplinkMethod) : xhttp.uplinkMethod;
|
||||
xo[QStringLiteral("uplinkHTTPMethod")] = methodEff.toUpper();
|
||||
|
||||
xo[QStringLiteral("noGRPCHeader")] = xhttp.disableGrpc;
|
||||
xo[QStringLiteral("noSSEHeader")] = xhttp.disableSse;
|
||||
|
||||
const QString sessPl = normalizeSessionSeqPlacement(xhttp.sessionPlacement);
|
||||
if (!sessPl.isEmpty())
|
||||
xo[QStringLiteral("sessionIDPlacement")] = sessPl;
|
||||
const QString seqPl = normalizeSessionSeqPlacement(xhttp.seqPlacement);
|
||||
if (!seqPl.isEmpty())
|
||||
xo[QStringLiteral("seqPlacement")] = seqPl;
|
||||
if (!xhttp.sessionKey.isEmpty())
|
||||
xo[QStringLiteral("sessionIDKey")] = xhttp.sessionKey;
|
||||
if (!xhttp.seqKey.isEmpty())
|
||||
xo[QStringLiteral("seqKey")] = xhttp.seqKey;
|
||||
|
||||
const QString uDataPl = normalizeUplinkDataPlacement(xhttp.uplinkDataPlacement);
|
||||
const bool uDataNeedsPacketUp =
|
||||
uDataPl == QLatin1String("header") || uDataPl == QLatin1String("cookie");
|
||||
if (!(uDataNeedsPacketUp && modeEff != QLatin1String("packet-up")))
|
||||
xo[QStringLiteral("uplinkDataPlacement")] = uDataPl;
|
||||
if (!xhttp.uplinkDataKey.isEmpty())
|
||||
xo[QStringLiteral("uplinkDataKey")] = xhttp.uplinkDataKey;
|
||||
|
||||
const QString ucs = xhttp.uplinkChunkSize.isEmpty() ? QString::fromLatin1(px::defaultXhttpUplinkChunkSize)
|
||||
: xhttp.uplinkChunkSize;
|
||||
if (!ucs.isEmpty() && ucs != QLatin1String("0")) {
|
||||
xo[QStringLiteral("uplinkChunkSize")] = ucs.toInt();
|
||||
}
|
||||
|
||||
if (!xhttp.scMaxBufferedPosts.isEmpty())
|
||||
xo[QStringLiteral("scMaxBufferedPosts")] = xhttp.scMaxBufferedPosts.toLongLong();
|
||||
|
||||
putIntRangeIfAny(xo, "scMaxEachPostBytes", xhttp.scMaxEachPostBytesMin, xhttp.scMaxEachPostBytesMax,
|
||||
px::defaultXhttpScMaxEachPostBytesMin, px::defaultXhttpScMaxEachPostBytesMax);
|
||||
putIntRangeIfAny(xo, "scMinPostsIntervalMs", xhttp.scMinPostsIntervalMsMin, xhttp.scMinPostsIntervalMsMax,
|
||||
px::defaultXhttpScMinPostsIntervalMsMin, px::defaultXhttpScMinPostsIntervalMsMax);
|
||||
putIntRangeIfAny(xo, "scStreamUpServerSecs", xhttp.scStreamUpServerSecsMin, xhttp.scStreamUpServerSecsMax,
|
||||
px::defaultXhttpScStreamUpServerSecsMin, px::defaultXhttpScStreamUpServerSecsMax);
|
||||
|
||||
const auto &pad = xhttp.xPadding;
|
||||
xo[QStringLiteral("xPaddingObfsMode")] = pad.obfsMode;
|
||||
if (pad.obfsMode) {
|
||||
if (!pad.bytesMin.isEmpty() || !pad.bytesMax.isEmpty()) {
|
||||
const int fromV = pad.bytesMin.isEmpty()
|
||||
? QString::fromLatin1(px::defaultXPaddingBytesMin).toInt()
|
||||
: pad.bytesMin.toInt();
|
||||
int toV = pad.bytesMax.isEmpty()
|
||||
? QString::fromLatin1(px::defaultXPaddingBytesMax).toInt()
|
||||
: pad.bytesMax.toInt();
|
||||
if (toV < fromV)
|
||||
toV = fromV;
|
||||
xo[QStringLiteral("xPaddingBytes")] = makeRangeString(QString::number(fromV), QString::number(toV));
|
||||
}
|
||||
xo[QStringLiteral("xPaddingKey")] =
|
||||
pad.key.isEmpty() ? QString::fromLatin1(px::defaultXPaddingKey) : pad.key;
|
||||
xo[QStringLiteral("xPaddingHeader")] =
|
||||
pad.header.isEmpty() ? QString::fromLatin1(px::defaultXPaddingHeader) : pad.header;
|
||||
xo[QStringLiteral("xPaddingPlacement")] = normalizeXPaddingPlacement(
|
||||
pad.placement.isEmpty() ? QString::fromLatin1(px::defaultXPaddingPlacement) : pad.placement);
|
||||
xo[QStringLiteral("xPaddingMethod")] = normalizeXPaddingMethod(
|
||||
pad.method.isEmpty() ? QString::fromLatin1(px::defaultXPaddingMethod) : pad.method);
|
||||
}
|
||||
|
||||
// xmux: Xray has no "enabled" flag; omit object when UI disables multiplex tuning.
|
||||
if (xhttp.xmux.enabled) {
|
||||
QJsonObject mux;
|
||||
auto addMuxRange = [&](const char *key, const QString &a, const QString &b) {
|
||||
// omit empty / 0-0 ranges (xray may reject "0-0")
|
||||
const bool aZero = a.isEmpty() || a == QLatin1String("0");
|
||||
const bool bZero = b.isEmpty() || b == QLatin1String("0");
|
||||
if (aZero && bZero)
|
||||
return;
|
||||
const QString aV = a.isEmpty() ? QStringLiteral("0") : a;
|
||||
const QString bV = b.isEmpty() ? QStringLiteral("0") : b;
|
||||
mux[QString::fromUtf8(key)] = makeRangeString(aV, bV);
|
||||
};
|
||||
addMuxRange("maxConcurrency", xhttp.xmux.maxConcurrencyMin, xhttp.xmux.maxConcurrencyMax);
|
||||
addMuxRange("maxConnections", xhttp.xmux.maxConnectionsMin, xhttp.xmux.maxConnectionsMax);
|
||||
addMuxRange("cMaxReuseTimes", xhttp.xmux.cMaxReuseTimesMin, xhttp.xmux.cMaxReuseTimesMax);
|
||||
addMuxRange("hMaxRequestTimes", xhttp.xmux.hMaxRequestTimesMin, xhttp.xmux.hMaxRequestTimesMax);
|
||||
addMuxRange("hMaxReusableSecs", xhttp.xmux.hMaxReusableSecsMin, xhttp.xmux.hMaxReusableSecsMax);
|
||||
if (!xhttp.xmux.hKeepAlivePeriod.isEmpty())
|
||||
mux[QStringLiteral("hKeepAlivePeriod")] = xhttp.xmux.hKeepAlivePeriod.toLongLong();
|
||||
if (!mux.isEmpty())
|
||||
xo[QStringLiteral("xmux")] = mux;
|
||||
}
|
||||
|
||||
streamSettings[QStringLiteral("xhttpSettings")] = xo;
|
||||
}
|
||||
|
||||
if (srv.transport == QLatin1String("mkcp")) {
|
||||
QJsonObject kcpObj;
|
||||
const QString ttiEff = mkcp.tti.isEmpty() ? QString::fromLatin1(px::defaultMkcpTti) : mkcp.tti;
|
||||
const QString upEff = mkcp.uplinkCapacity.isEmpty() ? QString::fromLatin1(px::defaultMkcpUplinkCapacity)
|
||||
: mkcp.uplinkCapacity;
|
||||
const QString downEff = mkcp.downlinkCapacity.isEmpty() ? QString::fromLatin1(px::defaultMkcpDownlinkCapacity)
|
||||
: mkcp.downlinkCapacity;
|
||||
const QString rbufEff = mkcp.readBufferSize.isEmpty() ? QString::fromLatin1(px::defaultMkcpReadBufferSize)
|
||||
: mkcp.readBufferSize;
|
||||
const QString wbufEff = mkcp.writeBufferSize.isEmpty() ? QString::fromLatin1(px::defaultMkcpWriteBufferSize)
|
||||
: mkcp.writeBufferSize;
|
||||
kcpObj[QStringLiteral("tti")] = ttiEff.toInt();
|
||||
kcpObj[QStringLiteral("uplinkCapacity")] = upEff.toInt();
|
||||
kcpObj[QStringLiteral("downlinkCapacity")] = downEff.toInt();
|
||||
kcpObj[QStringLiteral("readBufferSize")] = rbufEff.toInt();
|
||||
kcpObj[QStringLiteral("writeBufferSize")] = wbufEff.toInt();
|
||||
kcpObj[QStringLiteral("congestion")] = mkcp.congestion;
|
||||
streamSettings[QStringLiteral("kcpSettings")] = kcpObj;
|
||||
}
|
||||
|
||||
return streamSettings;
|
||||
}
|
||||
|
||||
ProtocolConfig XrayConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container,
|
||||
const ContainerConfig &containerConfig,
|
||||
const DnsSettings &dnsSettings,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#define XRAY_CONFIGURATOR_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include "configuratorBase.h"
|
||||
@@ -33,9 +34,24 @@ public:
|
||||
amnezia::ErrorCode writeServerConfigForSetup(const amnezia::ServerCredentials &credentials,
|
||||
amnezia::DockerContainer container,
|
||||
amnezia::ContainerConfig &containerConfig,
|
||||
const amnezia::DnsSettings &dnsSettings);
|
||||
const amnezia::DnsSettings &dnsSettings,
|
||||
bool useAtomicApply = false);
|
||||
|
||||
private:
|
||||
QJsonArray collectServerClients(const amnezia::ServerCredentials &credentials,
|
||||
amnezia::DockerContainer container, const QString &flowValue,
|
||||
const QString &fallbackClientId, amnezia::ErrorCode &outError) const;
|
||||
|
||||
amnezia::ErrorCode uploadServerConfigAtomically(const amnezia::ServerCredentials &credentials,
|
||||
amnezia::DockerContainer container, const QString &listenPort,
|
||||
const QJsonObject &serverConfig) const;
|
||||
|
||||
bool restartXrayContainer(const amnezia::ServerCredentials &credentials,
|
||||
amnezia::DockerContainer container) const;
|
||||
|
||||
bool xrayProcessIsUp(const amnezia::ServerCredentials &credentials,
|
||||
amnezia::DockerContainer container) const;
|
||||
|
||||
amnezia::ErrorCode readContainerKeyFile(amnezia::DockerContainer container,
|
||||
const amnezia::ServerCredentials &credentials,
|
||||
const QString &path, QString &out) const;
|
||||
@@ -44,24 +60,23 @@ private:
|
||||
const amnezia::DnsSettings &dnsSettings,
|
||||
amnezia::ErrorCode &errorCode);
|
||||
|
||||
amnezia::ErrorCode uploadServerConfigJson(const amnezia::ServerCredentials &credentials, amnezia::DockerContainer container,
|
||||
const amnezia::DnsSettings &dnsSettings, const QJsonObject &serverConfig) const;
|
||||
|
||||
amnezia::XrayProtocolConfig buildClientProtocolConfig(const amnezia::ServerCredentials &credentials,
|
||||
amnezia::DockerContainer container,
|
||||
const amnezia::XrayServerConfig &srv,
|
||||
const QString &clientId,
|
||||
amnezia::ErrorCode &errorCode,
|
||||
const QString &prefetchedRealityPublicKey = {},
|
||||
const QString &prefetchedRealityShortId = {}) const;
|
||||
const QString &prefetchedRealityShortId = {},
|
||||
const QString &prefetchedTlsPin = {}) const;
|
||||
|
||||
amnezia::ErrorCode readRealityKeyFiles(amnezia::DockerContainer container,
|
||||
const amnezia::ServerCredentials &credentials,
|
||||
QString &outPublicKey,
|
||||
QString &outShortId) const;
|
||||
|
||||
QJsonObject buildStreamSettings(const amnezia::XrayServerConfig &srv,
|
||||
const QString &clientId) const;
|
||||
amnezia::ErrorCode ensureTlsCertificate(const amnezia::ServerCredentials &credentials,
|
||||
amnezia::DockerContainer container,
|
||||
QString &outFingerprint) const;
|
||||
};
|
||||
|
||||
#endif // XRAY_CONFIGURATOR_H
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
#include "core/configurators/configuratorBase.h"
|
||||
#include "core/configurators/xrayConfigurator.h"
|
||||
#include "core/models/protocols/xrayProtocolConfig.h"
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
#include "core/utils/protocolEnum.h"
|
||||
@@ -60,6 +61,70 @@ namespace
|
||||
{
|
||||
Logger logger("InstallController");
|
||||
|
||||
QString effectiveXrayPort(const XrayProtocolConfig *cfg)
|
||||
{
|
||||
if (!cfg || cfg->serverConfig.port.isEmpty()) {
|
||||
return QString::fromLatin1(protocols::xray::defaultPort);
|
||||
}
|
||||
return cfg->serverConfig.port;
|
||||
}
|
||||
|
||||
QString normalizeXrayRelease(QString raw)
|
||||
{
|
||||
raw = raw.trimmed();
|
||||
if (raw.startsWith(QLatin1String("v"), Qt::CaseInsensitive)) {
|
||||
raw.remove(0, 1);
|
||||
}
|
||||
return raw.trimmed();
|
||||
}
|
||||
|
||||
QString parseXrayVersionOutput(const QString &output)
|
||||
{
|
||||
static const QRegularExpression re(QStringLiteral(R"(Xray\s+v?(\d+\.\d+\.\d+))"),
|
||||
QRegularExpression::CaseInsensitiveOption);
|
||||
const auto match = re.match(output);
|
||||
if (!match.hasMatch()) {
|
||||
return {};
|
||||
}
|
||||
return match.captured(1);
|
||||
}
|
||||
|
||||
enum class XrayBinaryProbeResult {
|
||||
Match,
|
||||
Mismatch,
|
||||
Failed
|
||||
};
|
||||
|
||||
XrayBinaryProbeResult probeXrayServerBinary(const ServerCredentials &credentials, SshSession &sshSession)
|
||||
{
|
||||
const QString expected =
|
||||
normalizeXrayRelease(QString::fromLatin1(protocols::xray::expectedServerXrayRelease));
|
||||
|
||||
QString stdOut;
|
||||
auto collect = [&stdOut](const QString &data, libssh::Client &) {
|
||||
stdOut += data;
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
const QString script = SshSession::replaceVars(
|
||||
QStringLiteral("sudo docker exec $CONTAINER_NAME xray version 2>/dev/null | head -1"),
|
||||
amnezia::genBaseVars(credentials, DockerContainer::Xray, QString(), QString()));
|
||||
const ErrorCode errorCode = sshSession.runScript(credentials, script, collect, collect);
|
||||
const QString live = parseXrayVersionOutput(stdOut);
|
||||
|
||||
if (errorCode != ErrorCode::NoError || live.isEmpty()) {
|
||||
return XrayBinaryProbeResult::Failed;
|
||||
}
|
||||
if (live != expected) {
|
||||
return XrayBinaryProbeResult::Mismatch;
|
||||
}
|
||||
return XrayBinaryProbeResult::Match;
|
||||
}
|
||||
|
||||
bool xrayServerBinaryMismatchesExpected(const ServerCredentials &credentials, SshSession &sshSession)
|
||||
{
|
||||
return probeXrayServerBinary(credentials, sshSession) == XrayBinaryProbeResult::Mismatch;
|
||||
}
|
||||
|
||||
bool dockerDaemonContainerMissing(const QString &out, const QString &containerDockerName)
|
||||
{
|
||||
if (!out.contains(QLatin1String("Error response from daemon"), Qt::CaseInsensitive)) {
|
||||
@@ -75,15 +140,34 @@ namespace
|
||||
return false;
|
||||
}
|
||||
|
||||
bool containerKeepsIdentityInDataVolume(DockerContainer container)
|
||||
{
|
||||
return container == DockerContainer::MtProxy
|
||||
|| container == DockerContainer::Telemt
|
||||
|| container == DockerContainer::TProxy
|
||||
|| container == DockerContainer::Xray;
|
||||
}
|
||||
|
||||
QString buildRemoveContainerScript(const amnezia::ScriptVars &vars, bool removeDataVolume)
|
||||
{
|
||||
QString script = SshSession::replaceVars(amnezia::scriptData(SharedScriptType::remove_container), vars);
|
||||
if (removeDataVolume) {
|
||||
script += QLatin1String("\nsudo docker volume rm -f $CONTAINER_NAME-data 2>/dev/null || true");
|
||||
script += QLatin1String(
|
||||
"\nfor attempt in 1 2 3 4 5; do"
|
||||
"\n sudo docker volume rm -f $CONTAINER_NAME-data && break"
|
||||
"\n sleep 1"
|
||||
"\ndone"
|
||||
"\necho \"amnezia_volume_left=$(sudo docker volume ls -q -f name=^$CONTAINER_NAME-data$ | head -1)\"");
|
||||
script = SshSession::replaceVars(script, vars);
|
||||
}
|
||||
return script;
|
||||
}
|
||||
|
||||
bool dataVolumeSurvivedRemoval(const QString &out)
|
||||
{
|
||||
static const QRegularExpression reLeft(QStringLiteral("amnezia_volume_left=(\\S+)"));
|
||||
return reLeft.match(out).hasMatch();
|
||||
}
|
||||
}
|
||||
|
||||
InstallController::InstallController(SecureServersRepository *serversRepository,
|
||||
@@ -102,7 +186,7 @@ InstallController::~InstallController()
|
||||
}
|
||||
|
||||
ErrorCode InstallController::setupContainer(const ServerCredentials &credentials, DockerContainer container, ContainerConfig &config,
|
||||
bool isUpdate)
|
||||
bool isUpdate, bool acceptXrayKeyLoss)
|
||||
{
|
||||
SshSession sshSession;
|
||||
ErrorCode e = ErrorCode::NoError;
|
||||
@@ -129,12 +213,38 @@ ErrorCode InstallController::setupContainer(const ServerCredentials &credentials
|
||||
if (e)
|
||||
return e;
|
||||
|
||||
QMap<QString, QString> xrayStateToMigrate;
|
||||
if (isUpdate) {
|
||||
if (!acceptXrayKeyLoss) {
|
||||
e = readXrayStateBeforeVolumeMigration(credentials, container, sshSession, xrayStateToMigrate);
|
||||
if (e)
|
||||
return e;
|
||||
}
|
||||
}
|
||||
|
||||
const amnezia::ScriptVars removeContainerVars =
|
||||
amnezia::genBaseVars(credentials, container, QString(), QString());
|
||||
const bool removeDataVolume = !isUpdate && (container == DockerContainer::MtProxy
|
||||
|| container == DockerContainer::Telemt || container == DockerContainer::TProxy);
|
||||
sshSession.runScript(credentials, buildRemoveContainerScript(removeContainerVars, removeDataVolume));
|
||||
const bool removeDataVolume = !isUpdate && containerKeepsIdentityInDataVolume(container);
|
||||
QString removeOut;
|
||||
auto collectRemoveOut = [&removeOut](const QString &data, libssh::Client &) {
|
||||
removeOut += data + "\n";
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
const ErrorCode removeError =
|
||||
sshSession.runScript(credentials, buildRemoveContainerScript(removeContainerVars, removeDataVolume),
|
||||
collectRemoveOut, collectRemoveOut);
|
||||
qDebug().noquote() << "InstallController::setupContainer removeContainer finished";
|
||||
|
||||
if (removeDataVolume) {
|
||||
if (removeError != ErrorCode::NoError) {
|
||||
return ErrorCode::ServerDataVolumeNotRemoved;
|
||||
}
|
||||
if (dataVolumeSurvivedRemoval(removeOut)) {
|
||||
return ErrorCode::ServerDataVolumeNotRemoved;
|
||||
}
|
||||
}
|
||||
|
||||
qDebug().noquote() << "buildContainerWorker start";
|
||||
e = buildContainerWorker(credentials, container, config, sshSession);
|
||||
if (e)
|
||||
return e;
|
||||
@@ -143,6 +253,10 @@ ErrorCode InstallController::setupContainer(const ServerCredentials &credentials
|
||||
if (e)
|
||||
return e;
|
||||
|
||||
e = restoreXrayStateIntoDataVolume(credentials, container, sshSession, xrayStateToMigrate);
|
||||
if (e)
|
||||
return e;
|
||||
|
||||
e = configureContainerWorker(credentials, container, config, sshSession);
|
||||
if (e)
|
||||
return e;
|
||||
@@ -161,20 +275,41 @@ ErrorCode InstallController::setupContainer(const ServerCredentials &credentials
|
||||
}
|
||||
|
||||
ErrorCode InstallController::updateServerConfig(const QString &serverId, DockerContainer container, const ContainerConfig &oldConfig,
|
||||
ContainerConfig &newConfig)
|
||||
ContainerConfig &newConfig, bool acceptXrayKeyLoss)
|
||||
{
|
||||
if (!isUpdateDockerContainerRequired(container, oldConfig, newConfig)) {
|
||||
const bool serverSettingsChanged = isUpdateDockerContainerRequired(container, oldConfig, newConfig);
|
||||
|
||||
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
|
||||
if (!adminConfig.has_value()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
if (container == DockerContainer::MtProxy) {
|
||||
ServerCredentials credentials = adminConfig->credentials();
|
||||
SshSession sshSession;
|
||||
|
||||
bool reinstallRequired = false;
|
||||
bool portReinstall = false;
|
||||
bool binaryMismatch = false;
|
||||
if (serverSettingsChanged) {
|
||||
if (!credentials.isValid()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
portReinstall = isReinstallContainerRequired(container, oldConfig, newConfig);
|
||||
reinstallRequired = portReinstall;
|
||||
}
|
||||
if (container == DockerContainer::Xray) {
|
||||
if (credentials.isValid()) {
|
||||
binaryMismatch = xrayServerBinaryMismatchesExpected(credentials, sshSession);
|
||||
if (binaryMismatch) {
|
||||
reinstallRequired = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!serverSettingsChanged && !reinstallRequired) {
|
||||
if (container == DockerContainer::MtProxy) {
|
||||
MtProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
|
||||
} else if (container == DockerContainer::Telemt) {
|
||||
ServerCredentials credentials = adminConfig->credentials();
|
||||
SshSession sshSession;
|
||||
TelemtInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
|
||||
} else if (container == DockerContainer::TProxy) {
|
||||
ServerCredentials credentials = adminConfig->credentials();
|
||||
@@ -186,22 +321,24 @@ ErrorCode InstallController::updateServerConfig(const QString &serverId, DockerC
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
|
||||
if (!adminConfig.has_value()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
ServerCredentials credentials = adminConfig->credentials();
|
||||
if (!credentials.isValid()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
SshSession sshSession;
|
||||
|
||||
bool reinstallRequired = isReinstallContainerRequired(container, oldConfig, newConfig);
|
||||
qDebug() << "InstallController::updateServerConfig for container" << container << "reinstall required is" << reinstallRequired;
|
||||
|
||||
ErrorCode errorCode = ErrorCode::NoError;
|
||||
if (reinstallRequired) {
|
||||
errorCode = setupContainer(credentials, container, newConfig, true);
|
||||
if (container == DockerContainer::Xray) {
|
||||
const QString oldPort = effectiveXrayPort(oldConfig.getXrayProtocolConfig());
|
||||
const QString newPort = effectiveXrayPort(newConfig.getXrayProtocolConfig());
|
||||
if (oldPort != newPort) {
|
||||
errorCode = isServerPortBusy(credentials, container, newConfig, sshSession);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
errorCode = setupContainer(credentials, container, newConfig, true, acceptXrayKeyLoss);
|
||||
|
||||
// Reinstall pulls the latest container image, so the server runs the latest protocol version
|
||||
if (errorCode == ErrorCode::NoError && container == DockerContainer::Awg2) {
|
||||
@@ -209,7 +346,12 @@ ErrorCode InstallController::updateServerConfig(const QString &serverId, DockerC
|
||||
awgConfig->serverConfig.protocolVersion = protocols::awg::awgV3;
|
||||
}
|
||||
}
|
||||
} else if (container != DockerContainer::Xray && container != DockerContainer::SSXray) {
|
||||
} else if (container == DockerContainer::Xray) {
|
||||
DnsSettings dnsSettings = { m_appSettingsRepository->primaryDns(), m_appSettingsRepository->secondaryDns() };
|
||||
XrayConfigurator xrayConfigurator(&sshSession);
|
||||
errorCode = xrayConfigurator.writeServerConfigForSetup(credentials, container, newConfig, dnsSettings,
|
||||
true);
|
||||
} else if (container != DockerContainer::SSXray) {
|
||||
errorCode = configureContainerWorker(credentials, container, newConfig, sshSession);
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
errorCode = startupContainerWorker(credentials, container, newConfig, sshSession);
|
||||
@@ -232,8 +374,10 @@ ErrorCode InstallController::updateServerConfig(const QString &serverId, DockerC
|
||||
TProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
|
||||
}
|
||||
if (reinstallRequired) {
|
||||
if (container != DockerContainer::Xray) {
|
||||
clearCachedProfile(serverId, container);
|
||||
}
|
||||
}
|
||||
adminConfig->updateContainerConfig(container, newConfig);
|
||||
m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin);
|
||||
}
|
||||
@@ -441,6 +585,9 @@ ErrorCode InstallController::prepareContainerConfig(DockerContainer container, c
|
||||
void InstallController::adminAppendRequested(const QString &serverId, DockerContainer container,
|
||||
const ContainerConfig &containerConfig, const QString &clientName)
|
||||
{
|
||||
if (container == DockerContainer::Xray) {
|
||||
return;
|
||||
}
|
||||
if (ContainerUtils::containerService(container) == ServiceType::Other
|
||||
|| !containerConfig.protocolConfig.hasClientConfig()) {
|
||||
return;
|
||||
@@ -570,6 +717,10 @@ ErrorCode InstallController::configureContainerWorker(const ServerCredentials &c
|
||||
return ErrorCode::ServerContainerMissingError;
|
||||
}
|
||||
|
||||
if (stdOut.contains(QLatin1String("amnezia_xray_keygen=failed"))) {
|
||||
return ErrorCode::XrayKeyGenerationFailed;
|
||||
}
|
||||
|
||||
updateContainerConfigAfterInstallation(container, config, stdOut);
|
||||
|
||||
if (container == DockerContainer::MtProxy) {
|
||||
@@ -606,6 +757,127 @@ ErrorCode InstallController::startupContainerWorker(const ServerCredentials &cre
|
||||
baseVars));
|
||||
}
|
||||
|
||||
ErrorCode InstallController::readXrayStateBeforeVolumeMigration(const ServerCredentials &credentials,
|
||||
DockerContainer container, SshSession &sshSession,
|
||||
QMap<QString, QString> &outFiles)
|
||||
{
|
||||
outFiles.clear();
|
||||
|
||||
if (container != DockerContainer::Xray) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
const amnezia::ScriptVars vars = amnezia::genBaseVars(credentials, container, QString(), QString());
|
||||
|
||||
QString stdOut;
|
||||
auto collect = [&stdOut](const QString &data, libssh::Client &) {
|
||||
stdOut += data + "\n";
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
|
||||
const QString probe = QStringLiteral(
|
||||
"echo \"amnezia_container=$(sudo docker ps -a -q -f name=^$CONTAINER_NAME$ | head -1)\"\n"
|
||||
"if [ -n \"$(sudo docker ps -a -q -f name=^$CONTAINER_NAME$ | head -1)\" ] && "
|
||||
"[ -z \"$(sudo docker ps -q -f name=^$CONTAINER_NAME$ | head -1)\" ]; then\n"
|
||||
" sudo docker start $CONTAINER_NAME > /dev/null 2>&1\n"
|
||||
" sleep 2\n"
|
||||
"fi\n"
|
||||
"echo \"amnezia_running=$(sudo docker ps -q -f name=^$CONTAINER_NAME$ | head -1)\"\n"
|
||||
"echo \"amnezia_mounted=$(sudo docker inspect -f "
|
||||
"'{{range .Mounts}}{{if and (eq .Destination \"%1\") "
|
||||
"(eq .Name \"$CONTAINER_NAME-data\")}}yes{{end}}{{end}}' "
|
||||
"$CONTAINER_NAME 2>/dev/null | head -1)\"")
|
||||
.arg(QString::fromLatin1(amnezia::protocols::xray::dataDir));
|
||||
ErrorCode errorCode = sshSession.runScript(credentials, SshSession::replaceVars(probe, vars), collect, collect);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return ErrorCode::XrayKeyMigrationFailed;
|
||||
}
|
||||
|
||||
static const QRegularExpression reContainer(QStringLiteral("amnezia_container=(\\S+)"));
|
||||
static const QRegularExpression reRunning(QStringLiteral("amnezia_running=(\\S+)"));
|
||||
static const QRegularExpression reMounted(QStringLiteral("amnezia_mounted=(\\S+)"));
|
||||
const bool containerExists = reContainer.match(stdOut).hasMatch();
|
||||
const bool containerRunning = reRunning.match(stdOut).hasMatch();
|
||||
const bool dataDirIsVolume = reMounted.match(stdOut).hasMatch();
|
||||
|
||||
if (!containerExists) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
if (dataDirIsVolume) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
if (!containerRunning) {
|
||||
return ErrorCode::XrayKeyMigrationNeedsConfirm;
|
||||
}
|
||||
|
||||
const QStringList requiredPaths = {
|
||||
QString::fromLatin1(amnezia::protocols::xray::PrivateKeyPath),
|
||||
QString::fromLatin1(amnezia::protocols::xray::PublicKeyPath),
|
||||
QString::fromLatin1(amnezia::protocols::xray::uuidPath),
|
||||
QString::fromLatin1(amnezia::protocols::xray::shortidPath),
|
||||
};
|
||||
|
||||
for (const QString &path : requiredPaths) {
|
||||
QString content;
|
||||
bool read = false;
|
||||
for (int attempt = 0; attempt < 3 && !read; ++attempt) {
|
||||
ErrorCode fileError = ErrorCode::NoError;
|
||||
content = QString::fromUtf8(sshSession.getTextFileFromContainer(container, credentials, path, fileError));
|
||||
if (fileError == ErrorCode::NoError && !content.trimmed().isEmpty()) {
|
||||
read = true;
|
||||
break;
|
||||
}
|
||||
if (attempt < 2) {
|
||||
QThread::msleep(500);
|
||||
}
|
||||
}
|
||||
if (!read) {
|
||||
return ErrorCode::XrayKeyMigrationFailed;
|
||||
}
|
||||
outFiles.insert(path, content);
|
||||
}
|
||||
|
||||
QString serverConfig;
|
||||
bool configRead = false;
|
||||
for (int attempt = 0; attempt < 3 && !configRead; ++attempt) {
|
||||
ErrorCode configError = ErrorCode::NoError;
|
||||
serverConfig = QString::fromUtf8(sshSession.getTextFileFromContainer(
|
||||
container, credentials, QString::fromLatin1(amnezia::protocols::xray::serverConfigPath), configError));
|
||||
if (configError == ErrorCode::NoError && !serverConfig.trimmed().isEmpty()) {
|
||||
configRead = true;
|
||||
break;
|
||||
}
|
||||
if (attempt < 2) {
|
||||
QThread::msleep(500);
|
||||
}
|
||||
}
|
||||
if (!configRead) {
|
||||
return ErrorCode::XrayKeyMigrationFailed;
|
||||
}
|
||||
outFiles.insert(QString::fromLatin1(amnezia::protocols::xray::serverConfigPath), serverConfig);
|
||||
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode InstallController::restoreXrayStateIntoDataVolume(const ServerCredentials &credentials,
|
||||
DockerContainer container, SshSession &sshSession,
|
||||
const QMap<QString, QString> &files)
|
||||
{
|
||||
if (files.isEmpty()) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
for (auto it = files.constBegin(); it != files.constEnd(); ++it) {
|
||||
const ErrorCode errorCode = sshSession.uploadTextFileToContainer(
|
||||
container, credentials, it.value(), it.key(), libssh::ScpOverwriteMode::ScpOverwriteExisting);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return ErrorCode::XrayKeyMigrationFailed;
|
||||
}
|
||||
}
|
||||
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode InstallController::isServerPortBusy(const ServerCredentials &credentials, DockerContainer container, const ContainerConfig &config, SshSession &sshSession)
|
||||
{
|
||||
if (container == DockerContainer::Dns) {
|
||||
@@ -743,7 +1015,20 @@ bool InstallController::isReinstallContainerRequired(DockerContainer container,
|
||||
}
|
||||
}
|
||||
|
||||
if (container == DockerContainer::Xray || container == DockerContainer::SSXray) {
|
||||
if (container == DockerContainer::Xray) {
|
||||
const auto *oldXrayConfig = oldConfig.getXrayProtocolConfig();
|
||||
const auto *newXrayConfig = newConfig.getXrayProtocolConfig();
|
||||
|
||||
if (oldXrayConfig && newXrayConfig) {
|
||||
const QString oldPort = effectiveXrayPort(oldXrayConfig);
|
||||
const QString newPort = effectiveXrayPort(newXrayConfig);
|
||||
if (oldPort != newPort) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (container == DockerContainer::SSXray) {
|
||||
const auto *oldXrayConfig = oldConfig.getXrayProtocolConfig();
|
||||
const auto *newXrayConfig = newConfig.getXrayProtocolConfig();
|
||||
|
||||
@@ -1050,10 +1335,19 @@ ErrorCode InstallController::removeContainer(const QString &serverId, DockerCont
|
||||
SshSession sshSession;
|
||||
const amnezia::ScriptVars removeContainerVars =
|
||||
amnezia::genBaseVars(credentials, container, QString(), QString());
|
||||
const bool removeDataVolume = (container == DockerContainer::MtProxy || container == DockerContainer::Telemt
|
||||
|| container == DockerContainer::TProxy);
|
||||
ErrorCode errorCode =
|
||||
sshSession.runScript(credentials, buildRemoveContainerScript(removeContainerVars, removeDataVolume));
|
||||
const bool removeDataVolume = containerKeepsIdentityInDataVolume(container);
|
||||
QString removeOut;
|
||||
auto collectRemoveOut = [&removeOut](const QString &data, libssh::Client &) {
|
||||
removeOut += data + "\n";
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
ErrorCode errorCode = sshSession.runScript(
|
||||
credentials, buildRemoveContainerScript(removeContainerVars, removeDataVolume), collectRemoveOut,
|
||||
collectRemoveOut);
|
||||
|
||||
if (errorCode == ErrorCode::NoError && removeDataVolume && dataVolumeSurvivedRemoval(removeOut)) {
|
||||
errorCode = ErrorCode::ServerDataVolumeNotRemoved;
|
||||
}
|
||||
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
QMap<DockerContainer, ContainerConfig> containers = adminConfig->containers;
|
||||
@@ -1155,6 +1449,13 @@ bool InstallController::isUpdateDockerContainerRequired(DockerContainer containe
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else if (container == DockerContainer::Xray) {
|
||||
const auto *oldXray = oldConfig.getXrayProtocolConfig();
|
||||
const auto *newXray = newConfig.getXrayProtocolConfig();
|
||||
if (oldXray && newXray && oldXray->serverConfig.hasEqualServerSettings(newXray->serverConfig)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} else if (container == DockerContainer::MtProxy) {
|
||||
const auto *oldMt = oldConfig.getMtProxyProtocolConfig();
|
||||
const auto *newMt = newConfig.getMtProxyProtocolConfig();
|
||||
|
||||
@@ -33,10 +33,10 @@ public:
|
||||
QObject *parent = nullptr);
|
||||
~InstallController();
|
||||
|
||||
ErrorCode setupContainer(const ServerCredentials &credentials, DockerContainer container, ContainerConfig &config, bool isUpdate = false);
|
||||
ErrorCode setupContainer(const ServerCredentials &credentials, DockerContainer container, ContainerConfig &config, bool isUpdate = false, bool acceptXrayKeyLoss = false);
|
||||
|
||||
// Updates server-side container settings (admin self-hosted only): reconfigures the container over SSH.
|
||||
ErrorCode updateServerConfig(const QString &serverId, DockerContainer container, const ContainerConfig &oldConfig, ContainerConfig &newConfig);
|
||||
ErrorCode updateServerConfig(const QString &serverId, DockerContainer container, const ContainerConfig &oldConfig, ContainerConfig &newConfig, bool acceptXrayKeyLoss = false);
|
||||
|
||||
// Updates client-local settings only: rewrites the stored container config for any self-hosted/native server. No SSH.
|
||||
ErrorCode updateClientConfig(const QString &serverId, DockerContainer container, ContainerConfig &newConfig);
|
||||
@@ -106,6 +106,11 @@ private:
|
||||
ErrorCode configureContainerWorker(const ServerCredentials &credentials, DockerContainer container, ContainerConfig &config, SshSession &sshSession);
|
||||
ErrorCode startupContainerWorker(const ServerCredentials &credentials, DockerContainer container, const ContainerConfig &config, SshSession &sshSession);
|
||||
|
||||
ErrorCode readXrayStateBeforeVolumeMigration(const ServerCredentials &credentials, DockerContainer container,
|
||||
SshSession &sshSession, QMap<QString, QString> &outFiles);
|
||||
ErrorCode restoreXrayStateIntoDataVolume(const ServerCredentials &credentials, DockerContainer container,
|
||||
SshSession &sshSession, const QMap<QString, QString> &files);
|
||||
|
||||
ErrorCode isServerPortBusy(const ServerCredentials &credentials, DockerContainer container, const ContainerConfig &config, SshSession &sshSession);
|
||||
ErrorCode isUserInSudo(const ServerCredentials &credentials, SshSession &sshSession);
|
||||
ErrorCode isServerDpkgBusy(const ServerCredentials &credentials, SshSession &sshSession);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QDateTime>
|
||||
|
||||
#include "core/utils/containerEnum.h"
|
||||
@@ -55,6 +56,33 @@ int UsersController::clientIndexById(const QString &clientId, const QJsonArray &
|
||||
return -1;
|
||||
}
|
||||
|
||||
QString UsersController::readXrayVolumeUuid(const DockerContainer container, const ServerCredentials &credentials,
|
||||
SshSession *sshSession) const
|
||||
{
|
||||
ErrorCode error = ErrorCode::NoError;
|
||||
const QString uuid = QString::fromUtf8(sshSession->getTextFileFromContainer(
|
||||
container, credentials, QString::fromLatin1(amnezia::protocols::xray::uuidPath), error));
|
||||
if (error != ErrorCode::NoError) {
|
||||
return {};
|
||||
}
|
||||
return uuid.trimmed();
|
||||
}
|
||||
|
||||
int UsersController::stripXrayVolumeUuidFromTable(QJsonArray &clientsTable, const QString &volumeUuid)
|
||||
{
|
||||
if (volumeUuid.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
int removed = 0;
|
||||
for (int i = clientsTable.size() - 1; i >= 0; --i) {
|
||||
if (clientsTable.at(i).toObject().value(configKey::clientId).toString() == volumeUuid) {
|
||||
clientsTable.removeAt(i);
|
||||
++removed;
|
||||
}
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
void UsersController::migration(const QByteArray &clientsTableString, QJsonArray &clientsTable)
|
||||
{
|
||||
QJsonObject clientsTableObj = QJsonDocument::fromJson(clientsTableString).object();
|
||||
@@ -248,7 +276,8 @@ ErrorCode UsersController::getWireGuardClients(const DockerContainer container,
|
||||
}
|
||||
|
||||
ErrorCode UsersController::getXrayClients(const DockerContainer container, const ServerCredentials& credentials,
|
||||
SshSession* sshSession, int &count, QJsonArray &clientsTable)
|
||||
SshSession* sshSession, int &count, QJsonArray &clientsTable,
|
||||
const QString &volumeDefaultUuid)
|
||||
{
|
||||
ErrorCode error = ErrorCode::NoError;
|
||||
|
||||
@@ -283,6 +312,7 @@ ErrorCode UsersController::getXrayClients(const DockerContainer container, const
|
||||
}
|
||||
|
||||
const QJsonArray clients = settings[protocols::xray::clients].toArray();
|
||||
const QString xrayDefaultUuid = volumeDefaultUuid;
|
||||
for (const auto &clientValue : clients) {
|
||||
const QJsonObject clientObj = clientValue.toObject();
|
||||
if (!clientObj.contains(protocols::xray::id)) {
|
||||
@@ -291,9 +321,6 @@ ErrorCode UsersController::getXrayClients(const DockerContainer container, const
|
||||
}
|
||||
QString clientId = clientObj[protocols::xray::id].toString();
|
||||
|
||||
QString xrayDefaultUuid = sshSession->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::uuidPath, error);
|
||||
xrayDefaultUuid.replace("\n", "");
|
||||
|
||||
if (!isClientExists(clientId, clientsTable) && clientId != xrayDefaultUuid) {
|
||||
QJsonObject client;
|
||||
client[configKey::clientId] = clientId;
|
||||
@@ -339,6 +366,9 @@ ErrorCode UsersController::updateClients(const QString &serverId, const DockerCo
|
||||
|
||||
m_clientsTable = QJsonDocument::fromJson(clientsTableString).array();
|
||||
|
||||
const QString xrayVolumeUuid =
|
||||
(container == DockerContainer::Xray) ? readXrayVolumeUuid(container, credentials, &sshSession) : QString();
|
||||
|
||||
if (m_clientsTable.isEmpty()) {
|
||||
migration(clientsTableString, m_clientsTable);
|
||||
|
||||
@@ -349,7 +379,7 @@ ErrorCode UsersController::updateClients(const QString &serverId, const DockerCo
|
||||
} else if (container == DockerContainer::WireGuard || ContainerUtils::isAwgContainer(container)) {
|
||||
error = getWireGuardClients(container, credentials, &sshSession, count, m_clientsTable);
|
||||
} else if (container == DockerContainer::Xray) {
|
||||
error = getXrayClients(container, credentials, &sshSession, count, m_clientsTable);
|
||||
error = getXrayClients(container, credentials, &sshSession, count, m_clientsTable, xrayVolumeUuid);
|
||||
}
|
||||
if (error != ErrorCode::NoError) {
|
||||
emit clientsUpdated(QJsonArray());
|
||||
@@ -365,6 +395,15 @@ ErrorCode UsersController::updateClients(const QString &serverId, const DockerCo
|
||||
}
|
||||
}
|
||||
|
||||
if (container == DockerContainer::Xray) {
|
||||
const int stripped = stripXrayVolumeUuidFromTable(m_clientsTable, xrayVolumeUuid);
|
||||
if (stripped > 0) {
|
||||
const QByteArray strippedTable = QJsonDocument(m_clientsTable).toJson();
|
||||
const ErrorCode writeError =
|
||||
sshSession.uploadTextFileToContainer(container, credentials, strippedTable, clientsTableFile);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<WgShowData> data;
|
||||
wgShow(container, credentials, &sshSession, data);
|
||||
|
||||
@@ -424,6 +463,13 @@ ErrorCode UsersController::appendClient(const QString &serverId, const QString &
|
||||
return error;
|
||||
}
|
||||
|
||||
if (container == DockerContainer::Xray) {
|
||||
const QString volumeUuid = readXrayVolumeUuid(container, credentials, &sshSession);
|
||||
if (!volumeUuid.isEmpty() && clientId == volumeUuid) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
}
|
||||
|
||||
int existingIndex = clientIndexById(clientId, m_clientsTable);
|
||||
if (existingIndex >= 0) {
|
||||
return renameClient(serverId, existingIndex, clientName, container, true);
|
||||
@@ -634,6 +680,19 @@ ErrorCode UsersController::revokeXray(const int row,
|
||||
|
||||
ErrorCode error = ErrorCode::NoError;
|
||||
|
||||
auto client = clientsTable.at(row).toObject();
|
||||
QString clientId = client.value(configKey::clientId).toString();
|
||||
|
||||
const QString volumeUuid = readXrayVolumeUuid(container, credentials, sshSession);
|
||||
if (!volumeUuid.isEmpty() && clientId == volumeUuid) {
|
||||
clientsTable.removeAt(row);
|
||||
const QByteArray clientsTableString = QJsonDocument(clientsTable).toJson();
|
||||
const QString clientsTableFile = QString("/opt/amnezia/%1/clientsTable")
|
||||
.arg(ContainerUtils::containerTypeToString(container));
|
||||
error = sshSession->uploadTextFileToContainer(container, credentials, clientsTableString, clientsTableFile);
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
const QString serverConfigPath = amnezia::protocols::xray::serverConfigPath;
|
||||
const QString configString = sshSession->getTextFileFromContainer(container, credentials, serverConfigPath, error);
|
||||
if (error != ErrorCode::NoError) {
|
||||
@@ -647,9 +706,6 @@ ErrorCode UsersController::revokeXray(const int row,
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
auto client = clientsTable.at(row).toObject();
|
||||
QString clientId = client.value(configKey::clientId).toString();
|
||||
|
||||
QJsonObject configObj = serverConfig.object();
|
||||
if (!configObj.contains(protocols::xray::inbounds)) {
|
||||
logger.error() << "Missing inbounds in xray config";
|
||||
|
||||
@@ -64,7 +64,12 @@ private:
|
||||
ErrorCode getWireGuardClients(const DockerContainer container, const ServerCredentials &credentials,
|
||||
SshSession* sshSession, int &count, QJsonArray &clientsTable);
|
||||
ErrorCode getXrayClients(const DockerContainer container, const ServerCredentials& credentials,
|
||||
SshSession* sshSession, int &count, QJsonArray &clientsTable);
|
||||
SshSession* sshSession, int &count, QJsonArray &clientsTable,
|
||||
const QString &volumeDefaultUuid);
|
||||
|
||||
QString readXrayVolumeUuid(const DockerContainer container, const ServerCredentials &credentials,
|
||||
SshSession *sshSession) const;
|
||||
static int stripXrayVolumeUuidFromTable(QJsonArray &clientsTable, const QString &volumeUuid);
|
||||
|
||||
ErrorCode wgShow(const DockerContainer container, const ServerCredentials &credentials,
|
||||
SshSession* sshSession, std::vector<WgShowData> &data);
|
||||
|
||||
@@ -87,8 +87,8 @@ ContainerConfig InstallerBase::createBaseConfig(DockerContainer container, int p
|
||||
srv.flow = protocols::xray::defaultFlow;
|
||||
srv.site = protocols::xray::defaultSite;
|
||||
srv.sni = protocols::xray::defaultSni;
|
||||
srv.fingerprint = protocols::xray::defaultFingerprint;
|
||||
srv.alpn = protocols::xray::defaultAlpn;
|
||||
srv.fingerprint = protocols::xray::defaultFingerprint;
|
||||
config.protocolConfig = xrayConfig;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#include "xrayInstaller.h"
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonArray>
|
||||
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
@@ -11,6 +10,7 @@
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
#include "core/utils/selfhosted/sshSession.h"
|
||||
#include "core/configurators/xrayConfigurator.h"
|
||||
#include "core/models/protocols/xrayProtocolConfig.h"
|
||||
#include "logger.h"
|
||||
|
||||
@@ -19,37 +19,21 @@ namespace
|
||||
Logger logger("XrayInstaller");
|
||||
|
||||
// Xray expects uTLS preset names (chrome, firefox, …). Old Amnezia/server templates used "Mozilla/5.0".
|
||||
QString normalizeXrayFingerprint(const QString &fp)
|
||||
QString describeServerJsonStatus(amnezia::XrayServerJsonStatus status)
|
||||
{
|
||||
if (fp.isEmpty() || fp.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)) {
|
||||
return QString::fromLatin1(protocols::xray::defaultFingerprint);
|
||||
}
|
||||
return fp;
|
||||
}
|
||||
|
||||
// Parse an xray int range: "from-to" string, plain int, or legacy {from,to} object.
|
||||
void parseIntRange(const QJsonValue &v, QString &minOut, QString &maxOut)
|
||||
{
|
||||
if (v.isString()) {
|
||||
const QString s = v.toString().trimmed();
|
||||
const int dash = s.indexOf(QLatin1Char('-'), 1);
|
||||
if (dash > 0) {
|
||||
minOut = s.left(dash).trimmed();
|
||||
maxOut = s.mid(dash + 1).trimmed();
|
||||
} else if (!s.isEmpty()) {
|
||||
minOut = s;
|
||||
maxOut = s;
|
||||
}
|
||||
} else if (v.isDouble()) {
|
||||
minOut = QString::number(v.toInt());
|
||||
maxOut = minOut;
|
||||
} else if (v.isObject()) {
|
||||
const QJsonObject o = v.toObject();
|
||||
if (o.contains(QLatin1String("from")) || o.contains(QLatin1String("to"))) {
|
||||
minOut = QString::number(o.value(QLatin1String("from")).toInt());
|
||||
maxOut = QString::number(o.value(QLatin1String("to")).toInt());
|
||||
}
|
||||
switch (status) {
|
||||
case amnezia::XrayServerJsonStatus::MissingInbounds:
|
||||
return QStringLiteral("server config missing 'inbounds' field");
|
||||
case amnezia::XrayServerJsonStatus::EmptyInbounds:
|
||||
return QStringLiteral("server config has empty 'inbounds' array");
|
||||
case amnezia::XrayServerJsonStatus::MissingStreamSettings:
|
||||
return QStringLiteral("inbound missing 'streamSettings' field");
|
||||
case amnezia::XrayServerJsonStatus::MissingSettings:
|
||||
return QStringLiteral("inbound missing 'settings' field");
|
||||
case amnezia::XrayServerJsonStatus::Ok:
|
||||
break;
|
||||
}
|
||||
return QStringLiteral("ok");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,267 +62,16 @@ ErrorCode XrayInstaller::extractConfigFromContainer(DockerContainer container, c
|
||||
logger.error() << "Failed to parse server config JSON";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
QJsonObject serverConfig = doc.object();
|
||||
|
||||
if (!serverConfig.contains(protocols::xray::inbounds)) {
|
||||
logger.error() << "Server config missing 'inbounds' field";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
QJsonArray inbounds = serverConfig[protocols::xray::inbounds].toArray();
|
||||
if (inbounds.isEmpty()) {
|
||||
logger.error() << "Server config has empty 'inbounds' array";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
QJsonObject inbound = inbounds[0].toObject();
|
||||
if (!inbound.contains(protocols::xray::streamSettings)) {
|
||||
logger.error() << "Inbound missing 'streamSettings' field";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
QJsonObject streamSettings = inbound[protocols::xray::streamSettings].toObject();
|
||||
auto *xrayConfig = config.getXrayProtocolConfig();
|
||||
if (!xrayConfig) {
|
||||
logger.error() << "No XrayProtocolConfig in ContainerConfig";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
XrayServerConfig &srv = xrayConfig->serverConfig;
|
||||
|
||||
// ── Port ─────────────────────────────────────────────────────────
|
||||
if (inbound.contains(protocols::xray::port)) {
|
||||
srv.port = QString::number(inbound[protocols::xray::port].toInt());
|
||||
}
|
||||
|
||||
// ── Network (transport) ───────────────────────────────────────────
|
||||
QString networkVal = streamSettings.value(protocols::xray::network).toString("tcp");
|
||||
if (networkVal == "xhttp") {
|
||||
srv.transport = "xhttp";
|
||||
} else if (networkVal == "kcp") {
|
||||
srv.transport = "mkcp";
|
||||
} else {
|
||||
srv.transport = "raw";
|
||||
}
|
||||
|
||||
// ── Security ──────────────────────────────────────────────────────
|
||||
srv.security = streamSettings.value(protocols::xray::security).toString("reality");
|
||||
|
||||
// ── Reality settings ──────────────────────────────────────────────
|
||||
if (srv.security == "reality") {
|
||||
QJsonObject rs = streamSettings.value(protocols::xray::realitySettings).toObject();
|
||||
|
||||
// serverNames array → site + sni
|
||||
if (rs.contains(protocols::xray::serverNames)) {
|
||||
QString sniVal = rs[protocols::xray::serverNames].toArray().first().toString();
|
||||
srv.sni = sniVal;
|
||||
srv.site = sniVal;
|
||||
} else if (rs.contains(protocols::xray::serverName)) {
|
||||
srv.sni = rs[protocols::xray::serverName].toString();
|
||||
srv.site = srv.sni;
|
||||
}
|
||||
|
||||
srv.fingerprint = normalizeXrayFingerprint(rs.value(protocols::xray::fingerprint).toString());
|
||||
}
|
||||
|
||||
// ── TLS settings ──────────────────────────────────────────────────
|
||||
if (srv.security == "tls") {
|
||||
QJsonObject tls = streamSettings.value("tlsSettings").toObject();
|
||||
srv.sni = tls.value(protocols::xray::serverName).toString();
|
||||
srv.fingerprint = normalizeXrayFingerprint(tls.value(protocols::xray::fingerprint).toString());
|
||||
|
||||
QJsonArray alpnArr = tls.value("alpn").toArray();
|
||||
QStringList alpnList;
|
||||
for (const QJsonValue &v : alpnArr) {
|
||||
QString t = v.toString().trimmed();
|
||||
if (t.compare(QLatin1String("HTTP/2"), Qt::CaseInsensitive) == 0)
|
||||
t = QStringLiteral("h2");
|
||||
else if (t.compare(QLatin1String("HTTP/1.1"), Qt::CaseInsensitive) == 0)
|
||||
t = QStringLiteral("http/1.1");
|
||||
if (!t.isEmpty())
|
||||
alpnList << t;
|
||||
}
|
||||
srv.alpn = alpnList.join(",");
|
||||
}
|
||||
|
||||
// ── Flow (from users array) ───────────────────────────────────────
|
||||
if (inbound.contains(protocols::xray::settings)) {
|
||||
QJsonObject s = inbound[protocols::xray::settings].toObject();
|
||||
QJsonArray clientsArr = s.value(protocols::xray::clients).toArray();
|
||||
if (!clientsArr.isEmpty()) {
|
||||
srv.flow = clientsArr[0].toObject().value(protocols::xray::flow).toString();
|
||||
}
|
||||
}
|
||||
|
||||
// ── XHTTP settings (Xray-core SplitHTTPConfig + legacy Amnezia keys) ──
|
||||
if (srv.transport == "xhttp") {
|
||||
QJsonObject xhttpObj = streamSettings.value("xhttpSettings").toObject();
|
||||
{
|
||||
const QString m = xhttpObj.value("mode").toString();
|
||||
if (m.isEmpty() || m == QLatin1String("auto"))
|
||||
srv.xhttp.mode = QStringLiteral("Auto");
|
||||
else if (m == QLatin1String("packet-up"))
|
||||
srv.xhttp.mode = QStringLiteral("Packet-up");
|
||||
else if (m == QLatin1String("stream-up"))
|
||||
srv.xhttp.mode = QStringLiteral("Stream-up");
|
||||
else if (m == QLatin1String("stream-one"))
|
||||
srv.xhttp.mode = QStringLiteral("Stream-one");
|
||||
else
|
||||
srv.xhttp.mode = m;
|
||||
}
|
||||
|
||||
srv.xhttp.host = xhttpObj.value("host").toString();
|
||||
srv.xhttp.path = xhttpObj.value("path").toString();
|
||||
|
||||
if (xhttpObj.contains(QLatin1String("uplinkHTTPMethod")))
|
||||
srv.xhttp.uplinkMethod = xhttpObj.value("uplinkHTTPMethod").toString();
|
||||
else
|
||||
srv.xhttp.uplinkMethod = xhttpObj.value("method").toString();
|
||||
|
||||
srv.xhttp.disableGrpc = xhttpObj.value("noGRPCHeader").toBool(true);
|
||||
srv.xhttp.disableSse = xhttpObj.value("noSSEHeader").toBool(true);
|
||||
|
||||
auto sessionSeqUi = [](const QString &core) -> QString {
|
||||
if (core.isEmpty() || core == QLatin1String("path"))
|
||||
return QStringLiteral("Path");
|
||||
if (core == QLatin1String("cookie"))
|
||||
return QStringLiteral("Cookie");
|
||||
if (core == QLatin1String("header"))
|
||||
return QStringLiteral("Header");
|
||||
if (core == QLatin1String("query"))
|
||||
return QStringLiteral("Query");
|
||||
return core;
|
||||
};
|
||||
QString sess = xhttpObj.value("sessionIDPlacement").toString();
|
||||
if (sess.isEmpty())
|
||||
sess = xhttpObj.value("sessionPlacement").toString();
|
||||
if (sess.isEmpty())
|
||||
sess = xhttpObj.value("scSessionPlacement").toString();
|
||||
srv.xhttp.sessionPlacement = sessionSeqUi(sess);
|
||||
|
||||
QString seq = xhttpObj.value("seqPlacement").toString();
|
||||
if (seq.isEmpty())
|
||||
seq = xhttpObj.value("scSeqPlacement").toString();
|
||||
srv.xhttp.seqPlacement = sessionSeqUi(seq);
|
||||
|
||||
auto uplinkDataUi = [](const QString &core) -> QString {
|
||||
if (core.isEmpty() || core == QLatin1String("body"))
|
||||
return QStringLiteral("Body");
|
||||
if (core == QLatin1String("auto"))
|
||||
return QStringLiteral("Auto");
|
||||
if (core == QLatin1String("header"))
|
||||
return QStringLiteral("Header");
|
||||
if (core == QLatin1String("cookie"))
|
||||
return QStringLiteral("Cookie");
|
||||
return core;
|
||||
};
|
||||
QString udata = xhttpObj.value("uplinkDataPlacement").toString();
|
||||
if (udata.isEmpty())
|
||||
udata = xhttpObj.value("scUplinkDataPlacement").toString();
|
||||
srv.xhttp.uplinkDataPlacement = uplinkDataUi(udata);
|
||||
|
||||
srv.xhttp.sessionKey = xhttpObj.value("sessionIDKey").toString();
|
||||
if (srv.xhttp.sessionKey.isEmpty())
|
||||
srv.xhttp.sessionKey = xhttpObj.value("sessionKey").toString();
|
||||
srv.xhttp.seqKey = xhttpObj.value("seqKey").toString();
|
||||
srv.xhttp.uplinkDataKey = xhttpObj.value("uplinkDataKey").toString();
|
||||
|
||||
if (xhttpObj.contains(QLatin1String("uplinkChunkSize"))) {
|
||||
QString ucMin, ucMax;
|
||||
parseIntRange(xhttpObj.value("uplinkChunkSize"), ucMin, ucMax);
|
||||
if (!ucMin.isEmpty())
|
||||
srv.xhttp.uplinkChunkSize = ucMin;
|
||||
} else if (xhttpObj.contains(QLatin1String("xhttpUplinkChunkSize"))) {
|
||||
srv.xhttp.uplinkChunkSize = QString::number(xhttpObj.value("xhttpUplinkChunkSize").toInt());
|
||||
}
|
||||
if (xhttpObj.contains(QLatin1String("scMaxBufferedPosts"))) {
|
||||
srv.xhttp.scMaxBufferedPosts = QString::number(xhttpObj.value("scMaxBufferedPosts").toVariant().toLongLong());
|
||||
}
|
||||
|
||||
auto readRange = [&](const char *key, QString &minOut, QString &maxOut) {
|
||||
parseIntRange(xhttpObj.value(QLatin1String(key)), minOut, maxOut);
|
||||
};
|
||||
readRange("scMaxEachPostBytes", srv.xhttp.scMaxEachPostBytesMin, srv.xhttp.scMaxEachPostBytesMax);
|
||||
readRange("scMinPostsIntervalMs", srv.xhttp.scMinPostsIntervalMsMin, srv.xhttp.scMinPostsIntervalMsMax);
|
||||
readRange("scStreamUpServerSecs", srv.xhttp.scStreamUpServerSecsMin, srv.xhttp.scStreamUpServerSecsMax);
|
||||
|
||||
auto loadPaddingFromObject = [&](const QJsonObject &pad) {
|
||||
if (pad.contains(QLatin1String("xPaddingObfsMode")))
|
||||
srv.xhttp.xPadding.obfsMode = pad.value("xPaddingObfsMode").toBool(true);
|
||||
srv.xhttp.xPadding.key = pad.value("xPaddingKey").toString();
|
||||
srv.xhttp.xPadding.header = pad.value("xPaddingHeader").toString();
|
||||
srv.xhttp.xPadding.placement = pad.value("xPaddingPlacement").toString();
|
||||
srv.xhttp.xPadding.method = pad.value("xPaddingMethod").toString();
|
||||
QString bytesMin, bytesMax;
|
||||
parseIntRange(pad.value("xPaddingBytes"), bytesMin, bytesMax);
|
||||
if (!bytesMin.isEmpty()) {
|
||||
srv.xhttp.xPadding.bytesMin = bytesMin;
|
||||
srv.xhttp.xPadding.bytesMax = bytesMax;
|
||||
}
|
||||
QString pl = srv.xhttp.xPadding.placement.toLower();
|
||||
if (pl == QLatin1String("cookie"))
|
||||
srv.xhttp.xPadding.placement = QStringLiteral("Cookie");
|
||||
else if (pl == QLatin1String("header"))
|
||||
srv.xhttp.xPadding.placement = QStringLiteral("Header");
|
||||
else if (pl == QLatin1String("query"))
|
||||
srv.xhttp.xPadding.placement = QStringLiteral("Query");
|
||||
else if (pl == QLatin1String("queryinheader"))
|
||||
srv.xhttp.xPadding.placement = QStringLiteral("Query in header");
|
||||
QString met = srv.xhttp.xPadding.method.toLower();
|
||||
if (met == QLatin1String("repeat-x"))
|
||||
srv.xhttp.xPadding.method = QStringLiteral("Repeat-x");
|
||||
else if (met == QLatin1String("tokenish"))
|
||||
srv.xhttp.xPadding.method = QStringLiteral("Tokenish");
|
||||
};
|
||||
if (xhttpObj.contains(QLatin1String("xPaddingObfsMode")) || xhttpObj.contains(QLatin1String("xPaddingKey"))
|
||||
|| xhttpObj.contains(QLatin1String("xPaddingBytes"))) {
|
||||
loadPaddingFromObject(xhttpObj);
|
||||
} else if (xhttpObj.contains(QLatin1String("xPadding")) && xhttpObj.value("xPadding").isObject()) {
|
||||
const QJsonObject nested = xhttpObj.value("xPadding").toObject();
|
||||
if (!nested.isEmpty()) {
|
||||
loadPaddingFromObject(nested);
|
||||
if (!nested.contains(QLatin1String("xPaddingObfsMode")))
|
||||
srv.xhttp.xPadding.obfsMode = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (xhttpObj.contains(QLatin1String("xmux"))) {
|
||||
QJsonObject mux = xhttpObj.value("xmux").toObject();
|
||||
srv.xhttp.xmux.enabled = true;
|
||||
|
||||
auto readMuxRange = [&](const char *key, QString &minOut, QString &maxOut) {
|
||||
parseIntRange(mux.value(QLatin1String(key)), minOut, maxOut);
|
||||
};
|
||||
readMuxRange("maxConcurrency", srv.xhttp.xmux.maxConcurrencyMin, srv.xhttp.xmux.maxConcurrencyMax);
|
||||
readMuxRange("maxConnections", srv.xhttp.xmux.maxConnectionsMin, srv.xhttp.xmux.maxConnectionsMax);
|
||||
readMuxRange("cMaxReuseTimes", srv.xhttp.xmux.cMaxReuseTimesMin, srv.xhttp.xmux.cMaxReuseTimesMax);
|
||||
readMuxRange("hMaxRequestTimes", srv.xhttp.xmux.hMaxRequestTimesMin, srv.xhttp.xmux.hMaxRequestTimesMax);
|
||||
readMuxRange("hMaxReusableSecs", srv.xhttp.xmux.hMaxReusableSecsMin, srv.xhttp.xmux.hMaxReusableSecsMax);
|
||||
|
||||
if (mux.contains(QLatin1String("hKeepAlivePeriod")))
|
||||
srv.xhttp.xmux.hKeepAlivePeriod = QString::number(mux.value("hKeepAlivePeriod").toVariant().toLongLong());
|
||||
}
|
||||
}
|
||||
|
||||
// ── mKCP settings ─────────────────────────────────────────────────
|
||||
if (srv.transport == "mkcp") {
|
||||
QJsonObject kcp = streamSettings.value("kcpSettings").toObject();
|
||||
if (kcp.contains("tti")) {
|
||||
srv.mkcp.tti = QString::number(kcp["tti"].toInt());
|
||||
}
|
||||
if (kcp.contains("uplinkCapacity")) {
|
||||
srv.mkcp.uplinkCapacity = QString::number(kcp["uplinkCapacity"].toInt());
|
||||
}
|
||||
if (kcp.contains("downlinkCapacity")) {
|
||||
srv.mkcp.downlinkCapacity = QString::number(kcp["downlinkCapacity"].toInt());
|
||||
}
|
||||
if (kcp.contains("readBufferSize")) {
|
||||
srv.mkcp.readBufferSize = QString::number(kcp["readBufferSize"].toInt());
|
||||
}
|
||||
if (kcp.contains("writeBufferSize")) {
|
||||
srv.mkcp.writeBufferSize = QString::number(kcp["writeBufferSize"].toInt());
|
||||
}
|
||||
srv.mkcp.congestion = kcp.value("congestion").toBool(true);
|
||||
const amnezia::XrayServerJsonStatus status =
|
||||
XrayServerConfig::fromServerInboundJson(doc.object(), xrayConfig->serverConfig);
|
||||
if (status != amnezia::XrayServerJsonStatus::Ok) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
return ErrorCode::NoError;
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,9 +1,11 @@
|
||||
#ifndef XRAYPROTOCOLCONFIG_H
|
||||
#define XRAYPROTOCOLCONFIG_H
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <optional>
|
||||
|
||||
namespace amnezia
|
||||
@@ -16,8 +18,8 @@ struct XrayXPaddingConfig {
|
||||
bool obfsMode = true; // xPaddingObfsMode
|
||||
QString key; // xPaddingKey
|
||||
QString header; // xPaddingHeader
|
||||
QString placement = protocols::xray::defaultXPaddingPlacement; // xPaddingPlacement: Cookie|Header|Query|Body
|
||||
QString method = protocols::xray::defaultXPaddingMethod; // xPaddingMethod: Repeat-x|Random|Zero
|
||||
QString placement; // xPaddingPlacement: Cookie|Header|Query|Query in header
|
||||
QString method; // xPaddingMethod: Repeat-x|Tokenish
|
||||
|
||||
QJsonObject toJson() const;
|
||||
static XrayXPaddingConfig fromJson(const QJsonObject &json);
|
||||
@@ -45,33 +47,35 @@ struct XrayXmuxConfig {
|
||||
|
||||
// ── XHTTP transport ───────────────────────────────────────────────────────────
|
||||
struct XrayXhttpConfig {
|
||||
QString mode = protocols::xray::defaultXhttpMode; // Auto|Packet-up|Stream-up|Stream-one
|
||||
QString host = protocols::xray::defaultXhttpHost;
|
||||
QString mode;
|
||||
QString host;
|
||||
QString path;
|
||||
QString uplinkMethod = protocols::xray::defaultXhttpUplinkMethod; // POST|PUT|PATCH
|
||||
bool disableGrpc = true;
|
||||
bool disableSse = true;
|
||||
|
||||
// Session & Sequence
|
||||
QString sessionPlacement = protocols::xray::defaultXhttpSessionPlacement;
|
||||
QString sessionKey = protocols::xray::defaultXhttpSessionKey;
|
||||
QString seqPlacement = protocols::xray::defaultXhttpSeqPlacement;
|
||||
QString sessionPlacement;
|
||||
QString sessionKey;
|
||||
QString seqPlacement;
|
||||
QString seqKey;
|
||||
QString uplinkDataPlacement = protocols::xray::defaultXhttpUplinkDataPlacement;
|
||||
QString uplinkDataPlacement;
|
||||
QString uplinkDataKey;
|
||||
|
||||
// Traffic Shaping
|
||||
QString uplinkChunkSize = protocols::xray::defaultXhttpUplinkChunkSize;
|
||||
QString uplinkMethod;
|
||||
QString uplinkChunkSize;
|
||||
QString scMinPostsIntervalMsMin;
|
||||
QString scMinPostsIntervalMsMax;
|
||||
|
||||
XrayXmuxConfig xmux;
|
||||
|
||||
QString scMaxBufferedPosts;
|
||||
QString scMaxEachPostBytesMin = protocols::xray::defaultXhttpScMaxEachPostBytesMin;
|
||||
QString scMaxEachPostBytesMax = protocols::xray::defaultXhttpScMaxEachPostBytesMax;
|
||||
QString scMinPostsIntervalMsMin = protocols::xray::defaultXhttpScMinPostsIntervalMsMin;
|
||||
QString scMinPostsIntervalMsMax = protocols::xray::defaultXhttpScMinPostsIntervalMsMax;
|
||||
QString scStreamUpServerSecsMin = protocols::xray::defaultXhttpScStreamUpServerSecsMin;
|
||||
QString scStreamUpServerSecsMax = protocols::xray::defaultXhttpScStreamUpServerSecsMax;
|
||||
QString scMaxEachPostBytesMin;
|
||||
QString scMaxEachPostBytesMax;
|
||||
QString scStreamUpServerSecsMin;
|
||||
QString scStreamUpServerSecsMax;
|
||||
|
||||
XrayXPaddingConfig xPadding;
|
||||
XrayXmuxConfig xmux;
|
||||
|
||||
QJsonObject toJson() const;
|
||||
/// Reads only keys present in JSON (no Amnezia UI defaults). Use XrayConfigModel::applyDefaultsToServerConfig for UI.
|
||||
@@ -81,16 +85,40 @@ struct XrayXhttpConfig {
|
||||
// ── mKCP transport ────────────────────────────────────────────────────────────
|
||||
struct XrayMkcpConfig {
|
||||
QString tti;
|
||||
QString mtu;
|
||||
QString uplinkCapacity;
|
||||
QString downlinkCapacity;
|
||||
QString readBufferSize;
|
||||
QString writeBufferSize;
|
||||
bool congestion = true;
|
||||
QString cwndMultiplier;
|
||||
QString maxSendingWindow;
|
||||
|
||||
QJsonObject toJson() const;
|
||||
static XrayMkcpConfig fromJson(const QJsonObject &json);
|
||||
};
|
||||
|
||||
enum class XrayStreamSide {
|
||||
Server,
|
||||
Client,
|
||||
};
|
||||
|
||||
struct XrayServerInboundInputs {
|
||||
QJsonArray clients;
|
||||
QString realityPrivateKey;
|
||||
QString realityShortId;
|
||||
};
|
||||
|
||||
enum class XrayServerJsonStatus {
|
||||
Ok,
|
||||
MissingInbounds,
|
||||
EmptyInbounds,
|
||||
MissingStreamSettings,
|
||||
MissingSettings,
|
||||
};
|
||||
|
||||
enum class XrayClientListFilter {
|
||||
KeepAll,
|
||||
DropWithoutId,
|
||||
};
|
||||
|
||||
// ── Server config (settings editable by user) ─────────────────────────────────
|
||||
struct XrayServerConfig {
|
||||
QString port;
|
||||
@@ -99,11 +127,13 @@ struct XrayServerConfig {
|
||||
QString site;
|
||||
bool isThirdPartyConfig = false;
|
||||
|
||||
int settingsVersion = 0;
|
||||
|
||||
QString security;
|
||||
QString flow;
|
||||
QString fingerprint;
|
||||
QString sni;
|
||||
QString alpn;
|
||||
QString fingerprint = protocols::xray::defaultFingerprint;
|
||||
|
||||
QString transport;
|
||||
XrayXhttpConfig xhttp;
|
||||
@@ -113,7 +143,64 @@ struct XrayServerConfig {
|
||||
|
||||
static XrayServerConfig fromJson(const QJsonObject &json);
|
||||
|
||||
void applyDefaults(bool fillFlowDefault = false);
|
||||
|
||||
QJsonObject streamSettingsJson(XrayStreamSide side) const;
|
||||
|
||||
QJsonObject serverStreamSettings() const;
|
||||
|
||||
QJsonObject clientStreamSettings() const;
|
||||
|
||||
QJsonObject toServerInboundJson(const XrayServerInboundInputs &inputs) const;
|
||||
|
||||
static XrayServerJsonStatus fromServerInboundJson(const QJsonObject &serverJson, XrayServerConfig &outServerConfig);
|
||||
|
||||
static QJsonArray clientsFromServerInboundJson(const QJsonObject &serverJson);
|
||||
static XrayServerJsonStatus setClientsInServerInboundJson(QJsonObject &serverJson, const QJsonArray &clients);
|
||||
|
||||
static QJsonObject makeClientEntry(const QString &clientId, const QString &flowValue);
|
||||
static QJsonObject applyFlowToClient(const QJsonObject &client, const QString &flowValue);
|
||||
static int indexOfClient(const QJsonArray &clients, const QString &clientId);
|
||||
static QString firstClientId(const QJsonArray &clients);
|
||||
static QJsonArray applyFlowToClients(const QJsonArray &clients, const QString &flowValue,
|
||||
XrayClientListFilter filter = XrayClientListFilter::KeepAll);
|
||||
|
||||
QJsonObject serverView() const;
|
||||
|
||||
QJsonObject issuedConfigView() const;
|
||||
|
||||
bool hasEqualServerSettings(const XrayServerConfig &other) const;
|
||||
|
||||
bool breaksIssuedConfigs(const XrayServerConfig &other) const;
|
||||
};
|
||||
|
||||
namespace xrayEffective
|
||||
{
|
||||
QString xhttpMode(const QString &mode);
|
||||
QString sessionSeqPlacement(const QString &placement);
|
||||
QString uplinkDataPlacement(const QString &placement);
|
||||
QString xPaddingPlacement(const QString &placement);
|
||||
QString xPaddingMethod(const QString &method);
|
||||
QString range(const QString &minV, const QString &maxV);
|
||||
void putRangeIfAny(QJsonObject &obj, const char *key, QString minV, QString maxV, const char *fallbackMin,
|
||||
const char *fallbackMax);
|
||||
|
||||
QString security(const XrayServerConfig &srv);
|
||||
QString clientFlow(const XrayServerConfig &srv);
|
||||
QString network(const XrayServerConfig &srv);
|
||||
QString xhttpModeSent(const XrayServerConfig &srv);
|
||||
}
|
||||
|
||||
QString sanitizeNativeConfig(const QString &nativeConfig);
|
||||
|
||||
struct XrayClientOutboundInputs {
|
||||
QString serverAddress;
|
||||
QString clientId;
|
||||
QString realityPublicKey;
|
||||
QString realityShortId;
|
||||
QString tlsPinnedPeerCertSha256;
|
||||
|
||||
static std::optional<XrayClientOutboundInputs> fromClientNativeJson(const QJsonObject &nativeJson);
|
||||
};
|
||||
|
||||
// ── Client config (generated, not edited by user) ─────────────────────────────
|
||||
@@ -124,6 +211,9 @@ struct XrayClientConfig {
|
||||
|
||||
QJsonObject toJson() const;
|
||||
static XrayClientConfig fromJson(const QJsonObject &json);
|
||||
|
||||
static QString idFromNativeJson(const QJsonObject &nativeJson);
|
||||
static QString localPortFromNativeJson(const QJsonObject &nativeJson);
|
||||
};
|
||||
|
||||
// ── Top-level protocol config ──────────────────────────────────────────────────
|
||||
@@ -140,7 +230,14 @@ struct XrayProtocolConfig {
|
||||
|
||||
bool needsClientHydration = false;
|
||||
|
||||
QJsonObject toClientOutboundJson(const XrayClientOutboundInputs &inputs) const;
|
||||
|
||||
bool fromClientOutboundJson(const QJsonObject &nativeJson);
|
||||
|
||||
bool hydrateServerConfigFromClientNative();
|
||||
|
||||
bool regenerateClientConfigFromNative();
|
||||
|
||||
};
|
||||
|
||||
} // namespace amnezia
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "xrayProtocol.h"
|
||||
|
||||
#include "core/models/protocols/xrayProtocolConfig.h"
|
||||
#include "core/protocols/protocolUtils.h"
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
@@ -87,22 +88,7 @@ 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);
|
||||
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";
|
||||
}
|
||||
xrayConfigStr = amnezia::sanitizeNativeConfig(xrayConfigStr);
|
||||
|
||||
return IpcClient::withInterface(
|
||||
[&](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
|
||||
@@ -178,6 +178,8 @@ namespace amnezia
|
||||
|
||||
// ── Xray-specific keys ────────────────────────────────────────
|
||||
|
||||
constexpr QLatin1String xraySettingsVersion("xray_settings_version");
|
||||
|
||||
// Security
|
||||
constexpr QLatin1String xraySecurity("xray_security"); // none | tls | reality
|
||||
constexpr QLatin1String xrayFlow("xray_flow"); // "" | xtls-rprx-vision | xtls-rprx-vision-udp443
|
||||
@@ -216,11 +218,11 @@ namespace amnezia
|
||||
|
||||
// Transport — mKCP
|
||||
constexpr QLatin1String mkcpTti("mkcp_tti");
|
||||
constexpr QLatin1String mkcpMtu("mkcp_mtu");
|
||||
constexpr QLatin1String mkcpUplinkCapacity("mkcp_uplink_capacity");
|
||||
constexpr QLatin1String mkcpDownlinkCapacity("mkcp_downlink_capacity");
|
||||
constexpr QLatin1String mkcpReadBufferSize("mkcp_read_buffer_size");
|
||||
constexpr QLatin1String mkcpWriteBufferSize("mkcp_write_buffer_size");
|
||||
constexpr QLatin1String mkcpCongestion("mkcp_congestion"); // bool
|
||||
constexpr QLatin1String mkcpCwndMultiplier("mkcp_cwnd_multiplier");
|
||||
constexpr QLatin1String mkcpMaxSendingWindow("mkcp_max_sending_window");
|
||||
|
||||
// xPadding
|
||||
constexpr QLatin1String xPaddingBytesMin("xpadding_bytes_min");
|
||||
|
||||
@@ -48,11 +48,15 @@ namespace amnezia
|
||||
|
||||
namespace xray
|
||||
{
|
||||
constexpr char dataDir[] = "/opt/amnezia/xray";
|
||||
constexpr char serverConfigPath[] = "/opt/amnezia/xray/server.json";
|
||||
constexpr char expectedServerXrayRelease[] = "v26.7.28";
|
||||
constexpr char uuidPath[] = "/opt/amnezia/xray/xray_uuid.key";
|
||||
constexpr char PublicKeyPath[] = "/opt/amnezia/xray/xray_public.key";
|
||||
constexpr char PrivateKeyPath[] = "/opt/amnezia/xray/xray_private.key";
|
||||
constexpr char shortidPath[] = "/opt/amnezia/xray/xray_short_id.key";
|
||||
constexpr char tlsCertPath[] = "/opt/amnezia/xray/tls_cert.pem";
|
||||
constexpr char tlsKeyPath[] = "/opt/amnezia/xray/tls_key.pem";
|
||||
constexpr char defaultSite[] = "www.googletagmanager.com";
|
||||
|
||||
constexpr char defaultPort[] = "443";
|
||||
@@ -60,6 +64,8 @@ namespace amnezia
|
||||
constexpr char defaultLocalAddr[] = "10.33.0.2";
|
||||
constexpr char defaultLocalListenAddr[] = "127.0.0.1";
|
||||
|
||||
constexpr int settingsVersionCurrent = 2;
|
||||
|
||||
constexpr char defaultSecurity[] = "reality";
|
||||
constexpr char defaultFlow[] = "xtls-rprx-vision";
|
||||
constexpr char defaultTransport[] = "raw";
|
||||
@@ -67,7 +73,7 @@ namespace amnezia
|
||||
constexpr char defaultSni[] = "www.googletagmanager.com";
|
||||
constexpr char defaultAlpn[] = "h2";
|
||||
|
||||
constexpr char defaultXhttpMode[] = "Auto";
|
||||
constexpr char defaultXhttpMode[] = "Stream-one";
|
||||
constexpr char defaultXhttpUplinkMethod[] = "POST";
|
||||
constexpr char defaultXhttpSessionPlacement[] = "Path";
|
||||
constexpr char defaultXhttpSessionKey[] = "";
|
||||
@@ -75,9 +81,10 @@ namespace amnezia
|
||||
constexpr char defaultXhttpUplinkDataPlacement[] = "Body";
|
||||
|
||||
constexpr char defaultXhttpHost[] = "www.googletagmanager.com";
|
||||
constexpr char defaultXhttpPath[] = "/";
|
||||
constexpr char defaultXhttpUplinkChunkSize[] = "0";
|
||||
constexpr char defaultXhttpScMaxEachPostBytesMin[] = "1";
|
||||
constexpr char defaultXhttpScMaxEachPostBytesMax[] = "100";
|
||||
constexpr char defaultXhttpScMaxEachPostBytesMin[] = "1000000";
|
||||
constexpr char defaultXhttpScMaxEachPostBytesMax[] = "1000000";
|
||||
constexpr char defaultXhttpScMinPostsIntervalMsMin[] = "100";
|
||||
constexpr char defaultXhttpScMinPostsIntervalMsMax[] = "800";
|
||||
constexpr char defaultXhttpScStreamUpServerSecsMin[] = "1";
|
||||
@@ -93,13 +100,16 @@ namespace amnezia
|
||||
constexpr char defaultMkcpTti[] = "50";
|
||||
constexpr char defaultMkcpUplinkCapacity[] = "5";
|
||||
constexpr char defaultMkcpDownlinkCapacity[] = "20";
|
||||
constexpr char defaultMkcpReadBufferSize[] = "2";
|
||||
constexpr char defaultMkcpWriteBufferSize[] = "2";
|
||||
constexpr char defaultMkcpMtu[] = "1350";
|
||||
constexpr char defaultMkcpCwndMultiplier[] = "1";
|
||||
|
||||
constexpr char outbounds[] = "outbounds";
|
||||
constexpr char inbounds[] = "inbounds";
|
||||
constexpr char settings[] = "settings";
|
||||
constexpr char streamSettings[] = "streamSettings";
|
||||
constexpr char tag[] = "tag";
|
||||
constexpr char sockopt[] = "sockopt";
|
||||
constexpr char dialerProxy[] = "dialerProxy";
|
||||
constexpr char vnext[] = "vnext";
|
||||
constexpr char users[] = "users";
|
||||
constexpr char servers[] = "servers";
|
||||
@@ -120,6 +130,92 @@ namespace amnezia
|
||||
constexpr char spiderX[] = "spiderX";
|
||||
constexpr char user[] = "user";
|
||||
constexpr char pass[] = "pass";
|
||||
|
||||
constexpr char logBlock[] = "log";
|
||||
constexpr char logLevel[] = "loglevel";
|
||||
constexpr char logLevelError[] = "error";
|
||||
constexpr char protocol[] = "protocol";
|
||||
constexpr char protocolVless[] = "vless";
|
||||
constexpr char protocolFreedom[] = "freedom";
|
||||
constexpr char protocolSocks[] = "socks";
|
||||
constexpr char decryption[] = "decryption";
|
||||
constexpr char decryptionNone[] = "none";
|
||||
constexpr char listen[] = "listen";
|
||||
constexpr char udp[] = "udp";
|
||||
|
||||
constexpr char tlsSettings[] = "tlsSettings";
|
||||
constexpr char xhttpSettings[] = "xhttpSettings";
|
||||
constexpr char kcpSettings[] = "kcpSettings";
|
||||
constexpr char alpn[] = "alpn";
|
||||
constexpr char certificates[] = "certificates";
|
||||
constexpr char certificateFile[] = "certificateFile";
|
||||
constexpr char keyFile[] = "keyFile";
|
||||
constexpr char allowInsecure[] = "allowInsecure";
|
||||
constexpr char pinnedPeerCertSha256[] = "pinnedPeerCertSha256";
|
||||
|
||||
constexpr char dest[] = "dest";
|
||||
constexpr char privateKey[] = "privateKey";
|
||||
constexpr char shortIds[] = "shortIds";
|
||||
|
||||
constexpr char xhttpHost[] = "host";
|
||||
constexpr char xhttpPath[] = "path";
|
||||
constexpr char xhttpMode[] = "mode";
|
||||
constexpr char noGrpcHeader[] = "noGRPCHeader";
|
||||
constexpr char noSseHeader[] = "noSSEHeader";
|
||||
constexpr char sessionIdPlacement[] = "sessionIDPlacement";
|
||||
constexpr char sessionIdKey[] = "sessionIDKey";
|
||||
constexpr char seqPlacement[] = "seqPlacement";
|
||||
constexpr char seqKey[] = "seqKey";
|
||||
constexpr char uplinkDataPlacement[] = "uplinkDataPlacement";
|
||||
constexpr char uplinkDataKey[] = "uplinkDataKey";
|
||||
constexpr char uplinkHttpMethod[] = "uplinkHTTPMethod";
|
||||
constexpr char uplinkChunkSize[] = "uplinkChunkSize";
|
||||
constexpr char scMaxBufferedPosts[] = "scMaxBufferedPosts";
|
||||
constexpr char scMaxEachPostBytes[] = "scMaxEachPostBytes";
|
||||
constexpr char scMinPostsIntervalMs[] = "scMinPostsIntervalMs";
|
||||
constexpr char scStreamUpServerSecs[] = "scStreamUpServerSecs";
|
||||
constexpr char xPaddingObfsMode[] = "xPaddingObfsMode";
|
||||
constexpr char xPaddingBytes[] = "xPaddingBytes";
|
||||
constexpr char xPaddingKey[] = "xPaddingKey";
|
||||
constexpr char xPaddingHeader[] = "xPaddingHeader";
|
||||
constexpr char xPaddingPlacement[] = "xPaddingPlacement";
|
||||
constexpr char xPaddingMethod[] = "xPaddingMethod";
|
||||
|
||||
constexpr char xmux[] = "xmux";
|
||||
constexpr char xmuxMaxConcurrency[] = "maxConcurrency";
|
||||
constexpr char xmuxMaxConnections[] = "maxConnections";
|
||||
constexpr char xmuxCMaxReuseTimes[] = "cMaxReuseTimes";
|
||||
constexpr char xmuxHMaxRequestTimes[] = "hMaxRequestTimes";
|
||||
constexpr char xmuxHMaxReusableSecs[] = "hMaxReusableSecs";
|
||||
constexpr char xmuxHKeepAlivePeriod[] = "hKeepAlivePeriod";
|
||||
|
||||
constexpr char kcpTti[] = "tti";
|
||||
constexpr char kcpMtu[] = "mtu";
|
||||
constexpr char kcpUplinkCapacity[] = "uplinkCapacity";
|
||||
constexpr char kcpDownlinkCapacity[] = "downlinkCapacity";
|
||||
constexpr char kcpCwndMultiplier[] = "cwndMultiplier";
|
||||
constexpr char kcpMaxSendingWindow[] = "maxSendingWindow";
|
||||
|
||||
constexpr char legacyXhttpMethod[] = "method";
|
||||
constexpr char legacySessionPlacement[] = "sessionPlacement";
|
||||
constexpr char legacyScSessionPlacement[] = "scSessionPlacement";
|
||||
constexpr char legacyScSeqPlacement[] = "scSeqPlacement";
|
||||
constexpr char legacyScUplinkDataPlacement[] = "scUplinkDataPlacement";
|
||||
constexpr char legacySessionKey[] = "sessionKey";
|
||||
constexpr char legacyUplinkChunkSize[] = "xhttpUplinkChunkSize";
|
||||
constexpr char legacyXPaddingBlock[] = "xPadding";
|
||||
constexpr char legacyRangeFrom[] = "from";
|
||||
constexpr char legacyRangeTo[] = "to";
|
||||
|
||||
constexpr char networkTcp[] = "tcp";
|
||||
constexpr char networkXhttp[] = "xhttp";
|
||||
constexpr char networkKcp[] = "kcp";
|
||||
constexpr char transportRaw[] = "raw";
|
||||
constexpr char transportXhttp[] = "xhttp";
|
||||
constexpr char transportMkcp[] = "mkcp";
|
||||
constexpr char securityNone[] = "none";
|
||||
constexpr char securityTls[] = "tls";
|
||||
constexpr char securityReality[] = "reality";
|
||||
}
|
||||
|
||||
namespace cloak
|
||||
|
||||
@@ -37,9 +37,17 @@ namespace amnezia
|
||||
ServerLinuxKernelTooOld = 214,
|
||||
XrayServerConfigInvalid = 215,
|
||||
XrayServerNoVlessClients = 216,
|
||||
XrayRealityKeysReadFailed = 217,
|
||||
XrayKeysReadFailed = 217,
|
||||
ServerContainerRuntimeNotSupported = 218,
|
||||
ContainerRuntimeServiceNotRunning = 219,
|
||||
XrayKeyMigrationFailed = 220,
|
||||
XrayTlsNotSupported = 221,
|
||||
XrayServerConfigRejected = 222,
|
||||
XrayServerConfigRolledBack = 223,
|
||||
XrayServerNotServing = 224,
|
||||
ServerDataVolumeNotRemoved = 225,
|
||||
XrayKeyGenerationFailed = 226,
|
||||
XrayKeyMigrationNeedsConfirm = 227,
|
||||
|
||||
// Ssh connection errors
|
||||
SshRequestDeniedError = 300,
|
||||
|
||||
@@ -36,9 +36,45 @@ QString errorString(ErrorCode code) {
|
||||
case(ErrorCode::XrayServerNoVlessClients):
|
||||
errorMessage = QObject::tr("Server error: XRay server has no VLESS clients");
|
||||
break;
|
||||
case(ErrorCode::XrayRealityKeysReadFailed):
|
||||
case(ErrorCode::XrayKeysReadFailed):
|
||||
errorMessage = QObject::tr("Server error: failed to read XRay Reality keys from the server");
|
||||
break;
|
||||
case(ErrorCode::XrayTlsNotSupported):
|
||||
errorMessage = QObject::tr("Server error: could not create a TLS certificate on the XRay server. "
|
||||
"The previous settings were left unchanged. Try again, or use Reality.");
|
||||
break;
|
||||
case(ErrorCode::XrayServerConfigRejected):
|
||||
errorMessage = QObject::tr("Server error: XRay rejected the new configuration. "
|
||||
"Nothing was changed, the server keeps running with the previous settings.");
|
||||
break;
|
||||
case(ErrorCode::XrayServerConfigRolledBack):
|
||||
errorMessage = QObject::tr("Server error: XRay did not start with the new configuration. "
|
||||
"The previous settings were restored and the server is running again.");
|
||||
break;
|
||||
case(ErrorCode::XrayServerNotServing):
|
||||
errorMessage = QObject::tr("Server error: XRay is not accepting connections and the previous configuration "
|
||||
"could not be restored. The server needs attention: check it over SSH, "
|
||||
"or reinstall the protocol.");
|
||||
break;
|
||||
case(ErrorCode::ServerDataVolumeNotRemoved):
|
||||
errorMessage = QObject::tr("Server error: the previous data volume could not be removed. "
|
||||
"Installing on top of it would reuse the old keys, so the configurations you "
|
||||
"expected to revoke would keep working. Check the server and try again.");
|
||||
break;
|
||||
case(ErrorCode::XrayKeyGenerationFailed):
|
||||
errorMessage = QObject::tr("Server error: XRay could not generate its keys on the server. "
|
||||
"Check the server and try again.");
|
||||
break;
|
||||
case(ErrorCode::XrayKeyMigrationFailed):
|
||||
errorMessage = QObject::tr("Server error: could not preserve the XRay keys stored on the server. "
|
||||
"The settings were not changed, so the existing configurations keep working. "
|
||||
"Check that the server is reachable and try again.");
|
||||
break;
|
||||
case(ErrorCode::XrayKeyMigrationNeedsConfirm):
|
||||
errorMessage = QObject::tr("The old XRay container will not start, so its keys cannot be preserved. "
|
||||
"Applying these settings will recreate it with new keys, and all existing "
|
||||
"configurations will stop working.");
|
||||
break;
|
||||
case(ErrorCode::ServerContainerRuntimeNotSupported): errorMessage = QObject::tr("Server error: The default container runtime available for installation on this server is not supported.\n Install Docker Engine on the server manually and try again."); break;
|
||||
case(ErrorCode::ContainerRuntimeServiceNotRunning): errorMessage = QObject::tr("Container runtime error: The container runtime service is not running.\n Check the container runtime service on the server, or wait about a minute and try again."); break;
|
||||
|
||||
|
||||
@@ -74,7 +74,6 @@ QString amnezia::scriptName(ProtocolScriptType type)
|
||||
case ProtocolScriptType::openvpn_template: return QLatin1String("template.ovpn");
|
||||
case ProtocolScriptType::wireguard_template: return QLatin1String("template.conf");
|
||||
case ProtocolScriptType::awg_template: return QLatin1String("template.conf");
|
||||
case ProtocolScriptType::xray_template: return QLatin1String("template.json");
|
||||
default: return QString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,8 +38,7 @@ enum ProtocolScriptType {
|
||||
container_startup,
|
||||
openvpn_template,
|
||||
wireguard_template,
|
||||
awg_template,
|
||||
xray_template
|
||||
awg_template
|
||||
};
|
||||
|
||||
enum ClientScriptType {
|
||||
|
||||
@@ -65,6 +65,5 @@
|
||||
<file>xray/Dockerfile</file>
|
||||
<file>xray/run_container.sh</file>
|
||||
<file>xray/start.sh</file>
|
||||
<file>xray/template.json</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
@@ -1,8 +1,40 @@
|
||||
cd /opt/amnezia/xray
|
||||
XRAY_CLIENT_ID=$(xray uuid) && echo $XRAY_CLIENT_ID > /opt/amnezia/xray/xray_uuid.key
|
||||
XRAY_SHORT_ID=$(openssl rand -hex 8) && echo $XRAY_SHORT_ID > /opt/amnezia/xray/xray_short_id.key
|
||||
XRAY_DIR=/opt/amnezia/xray
|
||||
|
||||
# Parse x25519 keypair by label (v26.7 output has an extra Hash32 line; line-index parsing breaks).
|
||||
# /opt/amnezia/xray is a docker volume, so these files outlive the container.
|
||||
# Regenerating any of them revokes every config already handed out, so each one is
|
||||
# created only when it is really missing.
|
||||
#
|
||||
# "Really missing" is checked by content, not by file size. A failed generation used
|
||||
# to leave a file holding a single newline, which is one byte, so a size test would
|
||||
# call it present and the key would never be repaired. With the volume that state is
|
||||
# permanent: nothing short of manual SSH would clear it.
|
||||
has_key() {
|
||||
[ -f "$1" ] && [ -n "$(tr -d '[:space:]' < "$1" 2>/dev/null)" ]
|
||||
}
|
||||
|
||||
if ! has_key $XRAY_DIR/xray_uuid.key; then
|
||||
XRAY_CLIENT_ID=$(xray uuid) && [ -n "$XRAY_CLIENT_ID" ] && printf '%s\n' "$XRAY_CLIENT_ID" > $XRAY_DIR/xray_uuid.key
|
||||
fi
|
||||
|
||||
if ! has_key $XRAY_DIR/xray_short_id.key; then
|
||||
XRAY_SHORT_ID=$(openssl rand -hex 8) && [ -n "$XRAY_SHORT_ID" ] && printf '%s\n' "$XRAY_SHORT_ID" > $XRAY_DIR/xray_short_id.key
|
||||
fi
|
||||
|
||||
# The private key is the one that must never change: it is what every issued config
|
||||
# is bound to. If only the public half is missing it gets derived from the private
|
||||
# one rather than rotating the pair.
|
||||
if has_key $XRAY_DIR/xray_private.key && ! has_key $XRAY_DIR/xray_public.key; then
|
||||
XRAY_PRIVATE_KEY=$(tr -d '[:space:]' < $XRAY_DIR/xray_private.key)
|
||||
DERIVED=$(xray x25519 -i "$XRAY_PRIVATE_KEY" 2>/dev/null)
|
||||
XRAY_PUBLIC_KEY=$(printf '%s\n' "$DERIVED" | sed -n 's/.*(PublicKey):[[:space:]]*//p' | head -1)
|
||||
[ -z "$XRAY_PUBLIC_KEY" ] && XRAY_PUBLIC_KEY=$(printf '%s\n' "$DERIVED" | sed -n 's/.*[Pp]ublic[ ]*[Kk]ey:[[:space:]]*//p' | head -1)
|
||||
XRAY_PUBLIC_KEY=$(echo $XRAY_PUBLIC_KEY | tr -d ' ')
|
||||
if [ -n "$XRAY_PUBLIC_KEY" ]; then
|
||||
printf '%s\n' "$XRAY_PUBLIC_KEY" > $XRAY_DIR/xray_public.key
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! has_key $XRAY_DIR/xray_private.key || ! has_key $XRAY_DIR/xray_public.key; then
|
||||
KEYPAIR=$(xray x25519)
|
||||
XRAY_PRIVATE_KEY=$(printf '%s\n' "$KEYPAIR" | sed -n 's/.*[Pp]rivate[ ]*[Kk]ey:[[:space:]]*//p' | head -1)
|
||||
XRAY_PUBLIC_KEY=$(printf '%s\n' "$KEYPAIR" | sed -n 's/.*(PublicKey):[[:space:]]*//p' | head -1)
|
||||
@@ -11,8 +43,15 @@ XRAY_PUBLIC_KEY=$(printf '%s\n' "$KEYPAIR" | sed -n 's/.*(PublicKey):[[:space:]]
|
||||
XRAY_PRIVATE_KEY=$(echo $XRAY_PRIVATE_KEY | tr -d ' ')
|
||||
XRAY_PUBLIC_KEY=$(echo $XRAY_PUBLIC_KEY | tr -d ' ')
|
||||
|
||||
|
||||
echo $XRAY_PUBLIC_KEY > /opt/amnezia/xray/xray_public.key
|
||||
echo $XRAY_PRIVATE_KEY > /opt/amnezia/xray/xray_private.key
|
||||
|
||||
# server.json is written by the client (writeServerConfigForSetup); this script only makes keys.
|
||||
# Both or neither. A half-written pair hands clients a public key the server is
|
||||
# not using, and it would look complete enough never to be repaired.
|
||||
if [ -n "$XRAY_PRIVATE_KEY" ] && [ -n "$XRAY_PUBLIC_KEY" ]; then
|
||||
printf '%s\n' "$XRAY_PRIVATE_KEY" > $XRAY_DIR/xray_private.key.tmp
|
||||
printf '%s\n' "$XRAY_PUBLIC_KEY" > $XRAY_DIR/xray_public.key.tmp
|
||||
mv $XRAY_DIR/xray_private.key.tmp $XRAY_DIR/xray_private.key
|
||||
mv $XRAY_DIR/xray_public.key.tmp $XRAY_DIR/xray_public.key
|
||||
else
|
||||
rm -f $XRAY_DIR/xray_private.key.tmp $XRAY_DIR/xray_public.key.tmp
|
||||
echo "amnezia_xray_keygen=failed"
|
||||
fi
|
||||
fi
|
||||
@@ -6,6 +6,7 @@ sudo docker run -d \
|
||||
--cap-add=NET_ADMIN \
|
||||
-p $XRAY_SERVER_PORT:$XRAY_SERVER_PORT/tcp \
|
||||
-p $XRAY_SERVER_PORT:$XRAY_SERVER_PORT/udp \
|
||||
-v $CONTAINER_NAME-data:/opt/amnezia/xray \
|
||||
--name $CONTAINER_NAME $CONTAINER_NAME
|
||||
|
||||
sudo docker network connect amnezia-dns-net $CONTAINER_NAME
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
{
|
||||
"log": {
|
||||
"loglevel": "error"
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"listen": "127.0.0.1",
|
||||
"port": 10808,
|
||||
"protocol": "socks",
|
||||
"settings": {
|
||||
"udp": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"protocol": "vless",
|
||||
"settings": {
|
||||
"vnext": [
|
||||
{
|
||||
"address": "$SERVER_IP_ADDRESS",
|
||||
"port": $XRAY_SERVER_PORT,
|
||||
"users": [
|
||||
{
|
||||
"id": "$XRAY_CLIENT_ID",
|
||||
"flow": "xtls-rprx-vision",
|
||||
"encryption": "none"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"streamSettings": {
|
||||
"network": "tcp",
|
||||
"security": "reality",
|
||||
"realitySettings": {
|
||||
"fingerprint": "chrome",
|
||||
"serverName": "$XRAY_SITE_NAME",
|
||||
"publicKey": "$XRAY_PUBLIC_KEY",
|
||||
"shortId": "$XRAY_SHORT_ID",
|
||||
"spiderX": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "core/controllers/selfhosted/installController.h"
|
||||
#include "core/controllers/connectionController.h"
|
||||
#include "core/utils/networkUtilities.h"
|
||||
#include "core/utils/errorStrings.h"
|
||||
#include "core/utils/protocolEnum.h"
|
||||
#include "core/protocols/protocolUtils.h"
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
@@ -303,7 +304,8 @@ void InstallUiController::updateClientConfig(const QString &serverId, int contai
|
||||
emit installationErrorOccurred(errorCode);
|
||||
}
|
||||
|
||||
void InstallUiController::updateServerConfig(const QString &serverId, int containerIndex, int protocolIndex, bool closePage)
|
||||
void InstallUiController::updateServerConfig(const QString &serverId, int containerIndex, int protocolIndex, bool closePage,
|
||||
bool acceptXrayKeyLoss)
|
||||
{
|
||||
DockerContainer container = static_cast<DockerContainer>(containerIndex);
|
||||
Proto protocolType = static_cast<Proto>(protocolIndex);
|
||||
@@ -318,7 +320,13 @@ void InstallUiController::updateServerConfig(const QString &serverId, int contai
|
||||
|| container == DockerContainer::TProxy
|
||||
|| container == DockerContainer::Xray || container == DockerContainer::SSXray;
|
||||
|
||||
|
||||
if (asyncUpdate) {
|
||||
if (m_serverConfigUpdateInProgress) {
|
||||
return;
|
||||
}
|
||||
m_serverConfigUpdateInProgress = true;
|
||||
|
||||
const bool emitBusy = container == DockerContainer::MtProxy || container == DockerContainer::Telemt
|
||||
|| container == DockerContainer::TProxy;
|
||||
if (emitBusy)
|
||||
@@ -329,6 +337,7 @@ void InstallUiController::updateServerConfig(const QString &serverId, int contai
|
||||
[this, watcher, serverId, container, closePage, protocolTypeCopy, emitBusy]() {
|
||||
const ErrorCode errorCode = watcher->result();
|
||||
watcher->deleteLater();
|
||||
m_serverConfigUpdateInProgress = false;
|
||||
if (emitBusy)
|
||||
emit serverIsBusy(false);
|
||||
|
||||
@@ -338,6 +347,8 @@ void InstallUiController::updateServerConfig(const QString &serverId, int contai
|
||||
m_protocolModel->updateModel(updatedConfig);
|
||||
updateProtocolConfigModel(serverId, static_cast<int>(container), static_cast<int>(protocolTypeCopy));
|
||||
emit updateContainerFinished(tr("Settings updated successfully"), closePage);
|
||||
} else if (errorCode == ErrorCode::XrayKeyMigrationNeedsConfirm) {
|
||||
emit xrayKeyMigrationNeedsConfirm(errorString(errorCode));
|
||||
} else {
|
||||
emit installationErrorOccurred(errorCode);
|
||||
}
|
||||
@@ -348,14 +359,14 @@ void InstallUiController::updateServerConfig(const QString &serverId, int contai
|
||||
InstallController *installController = m_installController;
|
||||
QFuture<ErrorCode> future =
|
||||
QtConcurrent::run([installController, serverId, container, oldConfigCopy,
|
||||
newConfigCopy]() mutable -> ErrorCode {
|
||||
return installController->updateServerConfig(serverId, container, oldConfigCopy, newConfigCopy);
|
||||
newConfigCopy, acceptXrayKeyLoss]() mutable -> ErrorCode {
|
||||
return installController->updateServerConfig(serverId, container, oldConfigCopy, newConfigCopy, acceptXrayKeyLoss);
|
||||
});
|
||||
watcher->setFuture(future);
|
||||
return;
|
||||
}
|
||||
|
||||
ErrorCode errorCode = m_installController->updateServerConfig(serverId, container, oldContainerConfig, containerConfig);
|
||||
ErrorCode errorCode = m_installController->updateServerConfig(serverId, container, oldContainerConfig, containerConfig, acceptXrayKeyLoss);
|
||||
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
ContainerConfig updatedConfig = m_serversController->getContainerConfig(serverId, container);
|
||||
|
||||
@@ -66,7 +66,8 @@ public slots:
|
||||
|
||||
void scanServerForInstalledContainers(const QString &serverId);
|
||||
|
||||
void updateServerConfig(const QString &serverId, int containerIndex, int protocolIndex, bool closePage = true);
|
||||
void updateServerConfig(const QString &serverId, int containerIndex, int protocolIndex, bool closePage = true,
|
||||
bool acceptXrayKeyLoss = false);
|
||||
void updateClientConfig(const QString &serverId, int containerIndex, int protocolIndex, bool closePage = true);
|
||||
|
||||
void removeServer(const QString &serverId);
|
||||
@@ -124,6 +125,8 @@ signals:
|
||||
void installationErrorOccurred(ErrorCode errorCode);
|
||||
void wrongInstallationUser(const QString &message);
|
||||
|
||||
void xrayKeyMigrationNeedsConfirm(const QString &message);
|
||||
|
||||
void serverAlreadyExists(int serverIndex);
|
||||
|
||||
void passphraseRequestStarted();
|
||||
@@ -164,6 +167,8 @@ private:
|
||||
|
||||
QString m_privateKeyPassphrase;
|
||||
|
||||
bool m_serverConfigUpdateInProgress = false;
|
||||
|
||||
void updateProtocolConfigModel(const QString &serverId, int containerIndex, int protocolIndex);
|
||||
|
||||
bool buildContainerConfigFromModel(int containerIndex, int protocolIndex, ContainerConfig &containerConfig);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
#include "core/utils/networkUtilities.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
|
||||
#include <QHostAddress>
|
||||
#include <QRegularExpression>
|
||||
@@ -48,8 +49,15 @@ bool XrayConfigModel::setData(const QModelIndex& index, const QVariant& value, i
|
||||
break;
|
||||
case Roles::PortRole: srv.port = str;
|
||||
break;
|
||||
case Roles::TransportRole: srv.transport = str;
|
||||
case Roles::TransportRole: {
|
||||
const bool becameXhttp = srv.transport != str
|
||||
&& str == QLatin1String(protocols::xray::transportXhttp);
|
||||
srv.transport = str;
|
||||
if (becameXhttp) {
|
||||
srv.applyDefaults(false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Roles::SecurityRole: srv.security = str;
|
||||
break;
|
||||
case Roles::FlowRole: srv.flow = str;
|
||||
@@ -58,7 +66,9 @@ bool XrayConfigModel::setData(const QModelIndex& index, const QVariant& value, i
|
||||
// ── Security ──────────────────────────────────────────────────────
|
||||
case Roles::FingerprintRole: srv.fingerprint = str;
|
||||
break;
|
||||
case Roles::SniRole: srv.sni = str;
|
||||
case Roles::SniRole:
|
||||
srv.sni = str;
|
||||
srv.site = str;
|
||||
break;
|
||||
case Roles::AlpnRole: srv.alpn = str;
|
||||
break;
|
||||
@@ -114,11 +124,11 @@ bool XrayConfigModel::setData(const QModelIndex& index, const QVariant& value, i
|
||||
break;
|
||||
case Roles::MkcpDownlinkCapacityRole: mkcp.downlinkCapacity = str;
|
||||
break;
|
||||
case Roles::MkcpReadBufferSizeRole: mkcp.readBufferSize = str;
|
||||
case Roles::MkcpMtuRole: mkcp.mtu = str;
|
||||
break;
|
||||
case Roles::MkcpWriteBufferSizeRole: mkcp.writeBufferSize = str;
|
||||
case Roles::MkcpCwndMultiplierRole: mkcp.cwndMultiplier = str;
|
||||
break;
|
||||
case Roles::MkcpCongestionRole: mkcp.congestion = value.toBool();
|
||||
case Roles::MkcpMaxSendingWindowRole: mkcp.maxSendingWindow = str;
|
||||
break;
|
||||
|
||||
// ── xPadding ──────────────────────────────────────────────────────
|
||||
@@ -228,9 +238,9 @@ QVariant XrayConfigModel::data(const QModelIndex& index, int role) const
|
||||
case Roles::MkcpTtiRole: return mkcp.tti;
|
||||
case Roles::MkcpUplinkCapacityRole: return mkcp.uplinkCapacity;
|
||||
case Roles::MkcpDownlinkCapacityRole: return mkcp.downlinkCapacity;
|
||||
case Roles::MkcpReadBufferSizeRole: return mkcp.readBufferSize;
|
||||
case Roles::MkcpWriteBufferSizeRole: return mkcp.writeBufferSize;
|
||||
case Roles::MkcpCongestionRole: return mkcp.congestion;
|
||||
case Roles::MkcpMtuRole: return mkcp.mtu;
|
||||
case Roles::MkcpCwndMultiplierRole: return mkcp.cwndMultiplier;
|
||||
case Roles::MkcpMaxSendingWindowRole: return mkcp.maxSendingWindow;
|
||||
|
||||
// ── xPadding ──────────────────────────────────────────────────────
|
||||
case Roles::XPaddingBytesMinRole: return pad.bytesMin;
|
||||
@@ -279,6 +289,7 @@ void XrayConfigModel::updateModel(amnezia::DockerContainer container, const amne
|
||||
m_originalProtocolConfig = m_protocolConfig;
|
||||
|
||||
endResetModel();
|
||||
|
||||
if (wasUnsavedChanges != hasUnsavedChanges()) {
|
||||
emit hasUnsavedChangesChanged();
|
||||
}
|
||||
@@ -286,88 +297,77 @@ void XrayConfigModel::updateModel(amnezia::DockerContainer container, const amne
|
||||
|
||||
void XrayConfigModel::applyDefaultsToServerConfig(amnezia::XrayServerConfig &config, bool fillFlowDefault)
|
||||
{
|
||||
if (config.port.isEmpty()) {
|
||||
config.port = protocols::xray::defaultPort;
|
||||
}
|
||||
|
||||
if (config.transportProto.isEmpty()) {
|
||||
config.transportProto = ProtocolUtils::transportProtoToString(
|
||||
ProtocolUtils::defaultTransportProto(amnezia::Proto::Xray), amnezia::Proto::Xray);
|
||||
}
|
||||
|
||||
if (config.site.isEmpty()) {
|
||||
config.site = protocols::xray::defaultSite;
|
||||
}
|
||||
|
||||
if (config.transport.isEmpty()) {
|
||||
config.transport = protocols::xray::defaultTransport;
|
||||
}
|
||||
|
||||
if (config.security.isEmpty()) {
|
||||
config.security = protocols::xray::defaultSecurity;
|
||||
}
|
||||
|
||||
if (fillFlowDefault && config.flow.isEmpty()) {
|
||||
config.flow = protocols::xray::defaultFlow;
|
||||
}
|
||||
|
||||
if (config.fingerprint.isEmpty()) {
|
||||
config.fingerprint = protocols::xray::defaultFingerprint;
|
||||
} else if (config.fingerprint.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)) {
|
||||
config.fingerprint = QString::fromLatin1(protocols::xray::defaultFingerprint);
|
||||
}
|
||||
|
||||
if (config.sni.isEmpty()) {
|
||||
config.sni = protocols::xray::defaultSni;
|
||||
}
|
||||
|
||||
if (config.alpn.isEmpty()) {
|
||||
config.alpn = protocols::xray::defaultAlpn;
|
||||
}
|
||||
|
||||
// XHTTP transport defaults
|
||||
if (config.xhttp.host.isEmpty()) {
|
||||
config.xhttp.host = protocols::xray::defaultXhttpHost;
|
||||
}
|
||||
if (config.xhttp.mode.isEmpty()) {
|
||||
config.xhttp.mode = protocols::xray::defaultXhttpMode;
|
||||
}
|
||||
if (config.xhttp.uplinkMethod.isEmpty()) {
|
||||
config.xhttp.uplinkMethod = protocols::xray::defaultXhttpUplinkMethod;
|
||||
}
|
||||
if (config.xhttp.sessionPlacement.isEmpty()) {
|
||||
config.xhttp.sessionPlacement = protocols::xray::defaultXhttpSessionPlacement;
|
||||
}
|
||||
if (config.xhttp.sessionKey.isEmpty()) {
|
||||
config.xhttp.sessionKey = protocols::xray::defaultXhttpSessionKey;
|
||||
}
|
||||
if (config.xhttp.seqPlacement.isEmpty()) {
|
||||
config.xhttp.seqPlacement = protocols::xray::defaultXhttpSeqPlacement;
|
||||
}
|
||||
if (config.xhttp.uplinkDataPlacement.isEmpty()) {
|
||||
config.xhttp.uplinkDataPlacement = protocols::xray::defaultXhttpUplinkDataPlacement;
|
||||
}
|
||||
|
||||
// xPadding defaults
|
||||
if (config.xhttp.xPadding.placement.isEmpty()) {
|
||||
config.xhttp.xPadding.placement = protocols::xray::defaultXPaddingPlacement;
|
||||
}
|
||||
if (config.xhttp.xPadding.method.isEmpty()) {
|
||||
config.xhttp.xPadding.method = protocols::xray::defaultXPaddingMethod;
|
||||
}
|
||||
config.applyDefaults(fillFlowDefault);
|
||||
}
|
||||
|
||||
amnezia::XrayProtocolConfig XrayConfigModel::getProtocolConfig()
|
||||
{
|
||||
const bool serverSettingsChanged =
|
||||
!m_protocolConfig.serverConfig.hasEqualServerSettings(m_originalProtocolConfig.serverConfig);
|
||||
if (!m_protocolConfig.serverConfig.hasEqualServerSettings(m_originalProtocolConfig.serverConfig)) {
|
||||
m_protocolConfig.clearClientConfig();
|
||||
return m_protocolConfig;
|
||||
}
|
||||
|
||||
if (serverSettingsChanged) {
|
||||
if (m_protocolConfig.serverConfig.toJson() == m_originalProtocolConfig.serverConfig.toJson()) {
|
||||
return m_protocolConfig;
|
||||
}
|
||||
|
||||
if (!m_protocolConfig.regenerateClientConfigFromNative()) {
|
||||
m_protocolConfig.clearClientConfig();
|
||||
}
|
||||
return m_protocolConfig;
|
||||
}
|
||||
|
||||
amnezia::XrayServerConfig XrayConfigModel::pendingServerConfig(const QString &pendingPort) const
|
||||
{
|
||||
amnezia::XrayServerConfig pending = m_protocolConfig.serverConfig;
|
||||
if (!pendingPort.isEmpty()) {
|
||||
pending.port = pendingPort;
|
||||
}
|
||||
return pending;
|
||||
}
|
||||
|
||||
bool XrayConfigModel::pendingChangeTouchesServer(const QString &pendingPort) const
|
||||
{
|
||||
return !pendingServerConfig(pendingPort).hasEqualServerSettings(m_originalProtocolConfig.serverConfig);
|
||||
}
|
||||
|
||||
bool XrayConfigModel::pendingChangeBreaksIssuedConfigs(const QString &pendingPort) const
|
||||
{
|
||||
return pendingServerConfig(pendingPort).breaksIssuedConfigs(m_originalProtocolConfig.serverConfig);
|
||||
}
|
||||
|
||||
bool XrayConfigModel::pendingChangeRequiresReinstall(const QString &pendingPort) const
|
||||
{
|
||||
const auto effectivePort = [](const QString &port) -> QString {
|
||||
return port.isEmpty() ? QString::fromLatin1(protocols::xray::defaultPort) : port;
|
||||
};
|
||||
const amnezia::XrayServerConfig pending = pendingServerConfig(pendingPort);
|
||||
return effectivePort(pending.port) != effectivePort(m_originalProtocolConfig.serverConfig.port);
|
||||
}
|
||||
|
||||
QString XrayConfigModel::saveDescription(const QString &pendingPort) const
|
||||
{
|
||||
const bool touchesServer = pendingChangeTouchesServer(pendingPort);
|
||||
const bool breaksIssued = pendingChangeBreaksIssuedConfigs(pendingPort);
|
||||
const bool requiresReinstall = pendingChangeRequiresReinstall(pendingPort);
|
||||
|
||||
if (requiresReinstall) {
|
||||
if (breaksIssued) {
|
||||
return tr("All users with whom you shared a connection with will no longer be able to connect to it. You will need to share the connection again.");
|
||||
} else {
|
||||
return tr("The server will be recreated. This takes up to a minute, and connections that were already shared keep working.");
|
||||
}
|
||||
} else if (touchesServer) {
|
||||
if (breaksIssued) {
|
||||
return tr("The server configuration will be updated. All users with whom you shared a connection with will no longer be able to connect to it. You will need to share the connection again.");
|
||||
} else {
|
||||
return tr("The server configuration will be updated. The container will not be recreated.");
|
||||
}
|
||||
} else {
|
||||
return tr("The server will not be changed now. The new settings apply the next time you connect.");
|
||||
}
|
||||
}
|
||||
|
||||
bool XrayConfigModel::isServerSettingsEqual() const
|
||||
{
|
||||
return m_protocolConfig.serverConfig.hasEqualServerSettings(m_originalProtocolConfig.serverConfig);
|
||||
@@ -375,7 +375,7 @@ bool XrayConfigModel::isServerSettingsEqual() const
|
||||
|
||||
bool XrayConfigModel::hasUnsavedChanges() const
|
||||
{
|
||||
return !isServerSettingsEqual();
|
||||
return m_protocolConfig.serverConfig.toJson() != m_originalProtocolConfig.serverConfig.toJson();
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> XrayConfigModel::roleNames() const
|
||||
@@ -422,9 +422,9 @@ QHash<int, QByteArray> XrayConfigModel::roleNames() const
|
||||
roles[MkcpTtiRole] = "mkcpTti";
|
||||
roles[MkcpUplinkCapacityRole] = "mkcpUplinkCapacity";
|
||||
roles[MkcpDownlinkCapacityRole] = "mkcpDownlinkCapacity";
|
||||
roles[MkcpReadBufferSizeRole] = "mkcpReadBufferSize";
|
||||
roles[MkcpWriteBufferSizeRole] = "mkcpWriteBufferSize";
|
||||
roles[MkcpCongestionRole] = "mkcpCongestion";
|
||||
roles[MkcpMtuRole] = "mkcpMtu";
|
||||
roles[MkcpCwndMultiplierRole] = "mkcpCwndMultiplier";
|
||||
roles[MkcpMaxSendingWindowRole] = "mkcpMaxSendingWindow";
|
||||
|
||||
// xPadding
|
||||
roles[XPaddingBytesMinRole] = "xPaddingBytesMin";
|
||||
@@ -458,9 +458,10 @@ void XrayConfigModel::resetToDefaults()
|
||||
|
||||
beginResetModel();
|
||||
m_protocolConfig.serverConfig = amnezia::XrayServerConfig{};
|
||||
applyDefaultsToServerConfig(m_protocolConfig.serverConfig);
|
||||
applyDefaultsToServerConfig(m_protocolConfig.serverConfig, true);
|
||||
endResetModel();
|
||||
|
||||
|
||||
if (wasUnsavedChanges != hasUnsavedChanges()) {
|
||||
emit hasUnsavedChangesChanged();
|
||||
}
|
||||
@@ -472,10 +473,12 @@ void XrayConfigModel::applyServerConfig(const amnezia::XrayServerConfig &serverC
|
||||
|
||||
beginResetModel();
|
||||
m_protocolConfig.serverConfig = serverConfig;
|
||||
// Clear client config since server settings changed
|
||||
m_protocolConfig.clearClientConfig();
|
||||
if (!m_protocolConfig.serverConfig.isThirdPartyConfig) {
|
||||
m_protocolConfig.serverConfig.applyDefaults(false);
|
||||
}
|
||||
endResetModel();
|
||||
|
||||
|
||||
if (wasUnsavedChanges != hasUnsavedChanges()) {
|
||||
emit hasUnsavedChangesChanged();
|
||||
}
|
||||
@@ -512,7 +515,7 @@ QStringList XrayConfigModel::alpnOptions()
|
||||
|
||||
QStringList XrayConfigModel::xhttpModeOptions()
|
||||
{
|
||||
return { "Auto", "Packet-up", "Stream-up", "Stream-one" };
|
||||
return { "Stream-up", "Stream-one" };
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::xhttpUplinkMethodOptions()
|
||||
@@ -532,8 +535,7 @@ QStringList XrayConfigModel::xhttpSeqPlacementOptions()
|
||||
|
||||
QStringList XrayConfigModel::xhttpUplinkDataPlacementOptions()
|
||||
{
|
||||
// Matches splithttp uplink payload placement (packet-up / advanced)
|
||||
return { "Body", "Auto", "Header", "Cookie" };
|
||||
return { "Body", "Auto" };
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::xPaddingPlacementOptions()
|
||||
@@ -562,14 +564,14 @@ QString XrayConfigModel::mkcpDefaultDownlinkCapacity()
|
||||
return QString::fromLatin1(protocols::xray::defaultMkcpDownlinkCapacity);
|
||||
}
|
||||
|
||||
QString XrayConfigModel::mkcpDefaultReadBufferSize()
|
||||
QString XrayConfigModel::mkcpDefaultMtu()
|
||||
{
|
||||
return QString::fromLatin1(protocols::xray::defaultMkcpReadBufferSize);
|
||||
return QString::fromLatin1(protocols::xray::defaultMkcpMtu);
|
||||
}
|
||||
|
||||
QString XrayConfigModel::mkcpDefaultWriteBufferSize()
|
||||
QString XrayConfigModel::mkcpDefaultCwndMultiplier()
|
||||
{
|
||||
return QString::fromLatin1(protocols::xray::defaultMkcpWriteBufferSize);
|
||||
return QString::fromLatin1(protocols::xray::defaultMkcpCwndMultiplier);
|
||||
}
|
||||
|
||||
QString XrayConfigModel::portDefault()
|
||||
|
||||
@@ -57,11 +57,11 @@ public:
|
||||
|
||||
// ── Transport — mKCP ──────────────────────────────────────────
|
||||
MkcpTtiRole,
|
||||
MkcpMtuRole,
|
||||
MkcpUplinkCapacityRole,
|
||||
MkcpDownlinkCapacityRole,
|
||||
MkcpReadBufferSizeRole,
|
||||
MkcpWriteBufferSizeRole,
|
||||
MkcpCongestionRole,
|
||||
MkcpCwndMultiplierRole,
|
||||
MkcpMaxSendingWindowRole,
|
||||
|
||||
// ── xPadding ──────────────────────────────────────────────────
|
||||
XPaddingBytesMinRole,
|
||||
@@ -112,8 +112,8 @@ public:
|
||||
Q_INVOKABLE static QString mkcpDefaultTti();
|
||||
Q_INVOKABLE static QString mkcpDefaultUplinkCapacity();
|
||||
Q_INVOKABLE static QString mkcpDefaultDownlinkCapacity();
|
||||
Q_INVOKABLE static QString mkcpDefaultReadBufferSize();
|
||||
Q_INVOKABLE static QString mkcpDefaultWriteBufferSize();
|
||||
Q_INVOKABLE static QString mkcpDefaultMtu();
|
||||
Q_INVOKABLE static QString mkcpDefaultCwndMultiplier();
|
||||
|
||||
Q_INVOKABLE static QString portDefault();
|
||||
Q_INVOKABLE static QString sniDefault();
|
||||
@@ -135,9 +135,17 @@ public:
|
||||
Q_INVOKABLE static bool isValidPath(const QString &path);
|
||||
Q_INVOKABLE QStringList validationErrors() const;
|
||||
|
||||
Q_INVOKABLE bool pendingChangeTouchesServer(const QString &pendingPort) const;
|
||||
Q_INVOKABLE bool pendingChangeBreaksIssuedConfigs(const QString &pendingPort) const;
|
||||
Q_INVOKABLE bool pendingChangeRequiresReinstall(const QString &pendingPort) const;
|
||||
|
||||
Q_INVOKABLE QString saveDescription(const QString &pendingPort) const;
|
||||
|
||||
public slots:
|
||||
void updateModel(amnezia::DockerContainer container, const amnezia::XrayProtocolConfig& protocolConfig);
|
||||
amnezia::XrayProtocolConfig getProtocolConfig();
|
||||
amnezia::XrayServerConfig pendingServerConfig(const QString &pendingPort) const;
|
||||
|
||||
bool isServerSettingsEqual() const;
|
||||
bool hasUnsavedChanges() const;
|
||||
void resetToDefaults();
|
||||
@@ -154,7 +162,7 @@ private:
|
||||
amnezia::XrayProtocolConfig m_protocolConfig;
|
||||
amnezia::XrayProtocolConfig m_originalProtocolConfig;
|
||||
|
||||
void applyDefaultsToServerConfig(amnezia::XrayServerConfig& config, bool fillFlowDefault = true);
|
||||
void applyDefaultsToServerConfig(amnezia::XrayServerConfig& config, bool fillFlowDefault);
|
||||
};
|
||||
|
||||
#endif // XRAYCONFIGMODEL_H
|
||||
@@ -115,15 +115,6 @@ void XrayConfigSnapshotsModel::createFromCurrent(const amnezia::XrayServerConfig
|
||||
persistAll();
|
||||
}
|
||||
|
||||
amnezia::XrayServerConfig XrayConfigSnapshotsModel::applyConfig(int index) const
|
||||
{
|
||||
if (index < 0 || index >= m_configs.size()) {
|
||||
return amnezia::XrayServerConfig {};
|
||||
}
|
||||
|
||||
return m_configs.at(index).serverConfig;
|
||||
}
|
||||
|
||||
void XrayConfigSnapshotsModel::removeConfig(int index)
|
||||
{
|
||||
if (index < 0 || index >= m_configs.size()) {
|
||||
@@ -200,7 +191,7 @@ void XrayConfigSnapshotsModel::createFromCurrentModel()
|
||||
if (!m_xrayConfigModel) {
|
||||
return;
|
||||
}
|
||||
createFromCurrent(m_xrayConfigModel->getProtocolConfig().serverConfig);
|
||||
createFromCurrent(m_xrayConfigModel->pendingServerConfig(QString()));
|
||||
}
|
||||
|
||||
void XrayConfigSnapshotsModel::applyConfigToCurrentModel(int index)
|
||||
@@ -208,9 +199,12 @@ void XrayConfigSnapshotsModel::applyConfigToCurrentModel(int index)
|
||||
if (!m_xrayConfigModel) {
|
||||
return;
|
||||
}
|
||||
amnezia::XrayServerConfig cfg = applyConfig(index);
|
||||
if (cfg.port.isEmpty()) {
|
||||
return; // guard against invalid index
|
||||
if (index < 0 || index >= m_configs.size()) {
|
||||
return;
|
||||
}
|
||||
m_xrayConfigModel->applyServerConfig(cfg);
|
||||
const XrayConfigSnapshot &snapshot = m_configs.at(index);
|
||||
if (snapshot.serverConfig.port.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
m_xrayConfigModel->applyServerConfig(snapshot.serverConfig);
|
||||
}
|
||||
@@ -45,7 +45,6 @@ public slots:
|
||||
void reload();
|
||||
|
||||
Q_INVOKABLE void createFromCurrent(const amnezia::XrayServerConfig &serverConfig);
|
||||
Q_INVOKABLE amnezia::XrayServerConfig applyConfig(int index) const;
|
||||
Q_INVOKABLE void removeConfig(int index);
|
||||
|
||||
Q_INVOKABLE QString exportToJson(int index) const;
|
||||
|
||||
@@ -51,6 +51,14 @@ Item {
|
||||
return String(n)
|
||||
}
|
||||
|
||||
function effectiveOther(value, placeholder) {
|
||||
if (value !== "")
|
||||
return parseInt(value, 10)
|
||||
if (placeholder !== "")
|
||||
return parseInt(placeholder, 10)
|
||||
return NaN
|
||||
}
|
||||
|
||||
function capEdit(tf, holder) {
|
||||
if (tf.text !== "" && parseInt(tf.text, 10) > root.maxLimit) {
|
||||
tf.text = holder.lastValid
|
||||
@@ -86,8 +94,8 @@ Item {
|
||||
textField.onTextEdited: { root.capEdit(minField.textField, minField); root.edited() }
|
||||
textField.onEditingFinished: {
|
||||
var v = root.clampValue(minField.textField.text)
|
||||
if (v !== "" && root.maxValue !== "") {
|
||||
var mx = parseInt(root.maxValue, 10)
|
||||
if (v !== "") {
|
||||
var mx = root.effectiveOther(root.maxValue, root.maxPlaceholder)
|
||||
if (!isNaN(mx) && parseInt(v, 10) > mx)
|
||||
root.maxChanged(v)
|
||||
}
|
||||
@@ -121,8 +129,8 @@ Item {
|
||||
textField.onTextEdited: { root.capEdit(maxField.textField, maxField); root.edited() }
|
||||
textField.onEditingFinished: {
|
||||
var v = root.clampValue(maxField.textField.text)
|
||||
if (v !== "" && root.minValue !== "") {
|
||||
var mn = parseInt(root.minValue, 10)
|
||||
if (v !== "") {
|
||||
var mn = root.effectiveOther(root.minValue, root.minPlaceholder)
|
||||
if (!isNaN(mn) && parseInt(v, 10) < mn)
|
||||
v = String(mn)
|
||||
}
|
||||
|
||||
@@ -79,6 +79,7 @@ PageType {
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: "xtls-rprx-vision-udp443"
|
||||
descriptionText: qsTr("For apps that require QUIC. Browsers may stop loading.")
|
||||
enabled: visionAllowed
|
||||
checked: flow === "xtls-rprx-vision-udp443" && visionAllowed
|
||||
onClicked: flow = "xtls-rprx-vision-udp443"
|
||||
@@ -118,7 +119,7 @@ PageType {
|
||||
text: qsTr("Save")
|
||||
clickedFunc: function () {
|
||||
var headerText = qsTr("Save settings?")
|
||||
var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.")
|
||||
var descriptionText = XrayConfigModel.saveDescription("")
|
||||
var yesButtonText = qsTr("Continue")
|
||||
var noButtonText = qsTr("Cancel")
|
||||
var yesButtonFunction = function () {
|
||||
|
||||
@@ -307,13 +307,14 @@ PageType {
|
||||
enabled: visible
|
||||
text: qsTr("Save")
|
||||
clickedFunc: function () {
|
||||
saveButton.forceActiveFocus()
|
||||
var errs = XrayConfigModel.validationErrors()
|
||||
if (errs.length > 0) {
|
||||
PageController.showErrorMessage(errs.join("\n"))
|
||||
return
|
||||
}
|
||||
var headerText = qsTr("Save settings?")
|
||||
var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.")
|
||||
var descriptionText = XrayConfigModel.saveDescription("")
|
||||
var yesButtonText = qsTr("Continue")
|
||||
var noButtonText = qsTr("Cancel")
|
||||
var yesButtonFunction = function () {
|
||||
|
||||
@@ -39,6 +39,25 @@ PageType {
|
||||
return value === "" ? qsTr("Empty") : value
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: InstallController
|
||||
|
||||
function onXrayKeyMigrationNeedsConfirm(message) {
|
||||
PageController.showBusyIndicator(false)
|
||||
showQuestionDrawer(
|
||||
qsTr("Recreate XRay with new keys?"),
|
||||
message,
|
||||
qsTr("Continue"),
|
||||
qsTr("Cancel"),
|
||||
function() {
|
||||
InstallController.updateServerConfig(ServersUiController.processedServerId,
|
||||
ServersUiController.processedContainerIndex,
|
||||
ProtocolEnum.Xray, true, true)
|
||||
},
|
||||
function() {})
|
||||
}
|
||||
}
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
|
||||
@@ -219,8 +238,10 @@ PageType {
|
||||
PageController.showErrorMessage(errs.join("\n"))
|
||||
return
|
||||
}
|
||||
var pendingPort = textFieldWithHeaderType.textField.text
|
||||
|
||||
var headerText = qsTr("Save settings?")
|
||||
var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.")
|
||||
var descriptionText = XrayConfigModel.saveDescription(pendingPort)
|
||||
var yesButtonText = qsTr("Continue")
|
||||
var noButtonText = qsTr("Cancel")
|
||||
var yesButtonFunction = function() {
|
||||
|
||||
@@ -182,16 +182,42 @@ PageType {
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("readBufferSize")
|
||||
hintText: qsTr("Read buffer size (MB). Range: 1–2147483647.")
|
||||
placeholderText: XrayConfigModel.mkcpDefaultReadBufferSize()
|
||||
textField.text: mkcpReadBufferSize
|
||||
headerText: qsTr("mtu")
|
||||
hintText: qsTr("Maximum packet size in bytes. Range: 576–1460.")
|
||||
placeholderText: XrayConfigModel.mkcpDefaultMtu()
|
||||
textField.text: mkcpMtu
|
||||
textField.maximumLength: 4
|
||||
textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ }
|
||||
textField.onTextEdited: root.editDirty = (textField.text !== mkcpMtu)
|
||||
textField.onEditingFinished: {
|
||||
var v = root.clampInt(textField.text, 576, 1460)
|
||||
if (v !== mkcpMtu) mkcpMtu = v
|
||||
else if (textField.text !== v) textField.text = v
|
||||
if (mkcpMaxSendingWindow !== "") {
|
||||
var floorNow = parseInt(v !== "" ? v : XrayConfigModel.mkcpDefaultMtu(), 10)
|
||||
if (parseInt(mkcpMaxSendingWindow, 10) < floorNow) {
|
||||
mkcpMaxSendingWindow = String(floorNow)
|
||||
}
|
||||
}
|
||||
root.editDirty = false
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("cwndMultiplier")
|
||||
hintText: qsTr("Congestion window multiplier. Minimum: 1.")
|
||||
placeholderText: XrayConfigModel.mkcpDefaultCwndMultiplier()
|
||||
textField.text: mkcpCwndMultiplier
|
||||
textField.maximumLength: 10
|
||||
textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ }
|
||||
textField.onTextEdited: root.editDirty = (textField.text !== mkcpReadBufferSize)
|
||||
textField.onTextEdited: root.editDirty = (textField.text !== mkcpCwndMultiplier)
|
||||
textField.onEditingFinished: {
|
||||
var v = root.clampInt(textField.text, 1, 2147483647)
|
||||
if (v !== mkcpReadBufferSize) mkcpReadBufferSize = v
|
||||
if (v !== mkcpCwndMultiplier) mkcpCwndMultiplier = v
|
||||
else if (textField.text !== v) textField.text = v
|
||||
root.editDirty = false
|
||||
}
|
||||
@@ -202,29 +228,20 @@ PageType {
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("writeBufferSize")
|
||||
hintText: qsTr("Write buffer size (MB). Range: 1–2147483647.")
|
||||
placeholderText: XrayConfigModel.mkcpDefaultWriteBufferSize()
|
||||
textField.text: mkcpWriteBufferSize
|
||||
headerText: qsTr("maxSendingWindow")
|
||||
hintText: qsTr("Send window in bytes. Must not be smaller than mtu. Leave empty to let Xray decide.")
|
||||
textField.text: mkcpMaxSendingWindow
|
||||
textField.maximumLength: 10
|
||||
textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ }
|
||||
textField.onTextEdited: root.editDirty = (textField.text !== mkcpWriteBufferSize)
|
||||
textField.onTextEdited: root.editDirty = (textField.text !== mkcpMaxSendingWindow)
|
||||
textField.onEditingFinished: {
|
||||
var v = root.clampInt(textField.text, 1, 2147483647)
|
||||
if (v !== mkcpWriteBufferSize) mkcpWriteBufferSize = v
|
||||
var mtuFloor = parseInt(mkcpMtu !== "" ? mkcpMtu : XrayConfigModel.mkcpDefaultMtu(), 10)
|
||||
var v = root.clampInt(textField.text, mtuFloor, 2147483647)
|
||||
if (v !== mkcpMaxSendingWindow) mkcpMaxSendingWindow = v
|
||||
else if (textField.text !== v) textField.text = v
|
||||
root.editDirty = false
|
||||
}
|
||||
}
|
||||
|
||||
SwitcherType {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: 16
|
||||
Layout.topMargin: 8
|
||||
text: qsTr("Congestion")
|
||||
checked: mkcpCongestion
|
||||
onToggled: mkcpCongestion = checked
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
@@ -542,7 +559,7 @@ PageType {
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: xhttpUplinkDataPlacement
|
||||
descriptionText: qsTr("Header/Cookie apply only in Packet-up mode")
|
||||
descriptionText: qsTr("Where the uploaded data is carried in the request")
|
||||
headerText: qsTr("UplinkDataPlacement")
|
||||
drawerParent: root
|
||||
listView: ListViewWithRadioButtonType {
|
||||
@@ -769,13 +786,14 @@ PageType {
|
||||
enabled: visible
|
||||
text: qsTr("Save")
|
||||
clickedFunc: function () {
|
||||
saveButton.forceActiveFocus()
|
||||
var errs = XrayConfigModel.validationErrors()
|
||||
if (errs.length > 0) {
|
||||
PageController.showErrorMessage(errs.join("\n"))
|
||||
return
|
||||
}
|
||||
var headerText = qsTr("Save settings?")
|
||||
var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.")
|
||||
var descriptionText = XrayConfigModel.saveDescription("")
|
||||
var yesButtonText = qsTr("Continue")
|
||||
var noButtonText = qsTr("Cancel")
|
||||
var yesButtonFunction = function () {
|
||||
|
||||
@@ -90,8 +90,9 @@ PageType {
|
||||
enabled: visible
|
||||
text: qsTr("Save")
|
||||
clickedFunc: function () {
|
||||
saveButton.forceActiveFocus()
|
||||
var headerText = qsTr("Save settings?")
|
||||
var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.")
|
||||
var descriptionText = XrayConfigModel.saveDescription("")
|
||||
var yesButtonText = qsTr("Continue")
|
||||
var noButtonText = qsTr("Cancel")
|
||||
var yesButtonFunction = function () {
|
||||
|
||||
@@ -219,8 +219,9 @@ PageType {
|
||||
enabled: visible
|
||||
text: qsTr("Save")
|
||||
clickedFunc: function () {
|
||||
saveButton.forceActiveFocus()
|
||||
var headerText = qsTr("Save settings?")
|
||||
var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.")
|
||||
var descriptionText = XrayConfigModel.saveDescription("")
|
||||
var yesButtonText = qsTr("Continue")
|
||||
var noButtonText = qsTr("Cancel")
|
||||
var yesButtonFunction = function () {
|
||||
|
||||
@@ -232,8 +232,9 @@ PageType {
|
||||
enabled: visible
|
||||
text: qsTr("Save")
|
||||
clickedFunc: function () {
|
||||
saveButton.forceActiveFocus()
|
||||
var headerText = qsTr("Save settings?")
|
||||
var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.")
|
||||
var descriptionText = XrayConfigModel.saveDescription("")
|
||||
var yesButtonText = qsTr("Continue")
|
||||
var noButtonText = qsTr("Cancel")
|
||||
var yesButtonFunction = function () {
|
||||
|
||||
Reference in new issue
Block a user