From 7d8992886a74faf61a3c009e7df98e0970655b61 Mon Sep 17 00:00:00 2001 From: dranik Date: Tue, 31 Mar 2026 14:20:11 +0300 Subject: [PATCH] fixed ui --- .../core/configurators/xrayConfigurator.cpp | 433 ++++++++++-------- client/core/configurators/xrayConfigurator.h | 3 + client/core/installers/xrayInstaller.cpp | 152 ++++-- .../models/protocols/xrayProtocolConfig.cpp | 66 +-- .../models/protocols/xrayProtocolConfig.h | 47 +- client/core/protocols/xrayProtocol.cpp | 48 +- client/core/protocols/xrayProtocol.h | 3 + .../core/utils/constants/protocolConstants.h | 34 ++ .../ui/models/protocols/xrayConfigModel.cpp | 175 ++++++- client/ui/models/protocols/xrayConfigModel.h | 24 + .../qml/Controls2/TextFieldWithHeaderType.qml | 10 + .../Pages2/PageProtocolXrayFlowSettings.qml | 53 ++- .../PageProtocolXraySecuritySettings.qml | 135 +++--- .../qml/Pages2/PageProtocolXraySettings.qml | 2 +- .../PageProtocolXrayTransportSettings.qml | 203 ++++---- .../PageProtocolXrayXPaddingBytesSettings.qml | 53 ++- .../PageProtocolXrayXPaddingSettings.qml | 82 ++-- .../Pages2/PageProtocolXrayXmuxSettings.qml | 53 ++- 18 files changed, 1004 insertions(+), 572 deletions(-) mode change 100755 => 100644 client/core/protocols/xrayProtocol.cpp diff --git a/client/core/configurators/xrayConfigurator.cpp b/client/core/configurators/xrayConfigurator.cpp index bf85d6e37..e50110067 100644 --- a/client/core/configurators/xrayConfigurator.cpp +++ b/client/core/configurators/xrayConfigurator.cpp @@ -8,6 +8,7 @@ #include #include "core/models/containerConfig.h" +#include "core/models/protocolConfig.h" #include "core/models/protocols/xrayProtocolConfig.h" #include "core/protocols/protocolUtils.h" #include "core/utils/constants/configKeys.h" @@ -18,15 +19,125 @@ #include "core/utils/selfhosted/scriptsRegistry.h" #include "core/utils/selfhosted/sshSession.h" -namespace { -Logger logger("XrayConfigurator"); -} +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(); + } + + 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); + QJsonObject r; + r[QStringLiteral("from")] = minV.toInt(); + r[QStringLiteral("to")] = maxV.toInt(); + obj[QString::fromUtf8(key)] = r; + } + + // 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; + } + 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; + } + if (changed) { + pc.setNativeConfig(c); + } + } +} // namespace XrayConfigurator::XrayConfigurator(SshSession* sshSession, QObject *parent) : ConfiguratorBase(sshSession, parent) { } +amnezia::ProtocolConfig XrayConfigurator::processConfigWithLocalSettings(const amnezia::ConnectionSettings &settings, + amnezia::ProtocolConfig protocolConfig) +{ + applyDnsToNativeConfig(settings.dns, protocolConfig); + sanitizeXrayNativeConfig(protocolConfig); + return protocolConfig; +} + QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentials, DockerContainer container, const ContainerConfig &containerConfig, const DnsSettings &dnsSettings, @@ -143,222 +254,162 @@ QJsonObject XrayConfigurator::buildStreamSettings(const XrayServerConfig &srv, c QJsonObject streamSettings; const auto &xhttp = srv.xhttp; const auto &mkcp = srv.mkcp; + namespace px = amnezia::protocols::xray; - // network - QString networkValue = "tcp"; - if (srv.transport == "xhttp") - { - networkValue = "xhttp"; - } - else if (srv.transport == "mkcp") - { - networkValue = "kcp"; - } - streamSettings[amnezia::protocols::xray::network] = networkValue; + 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; - // security - streamSettings[amnezia::protocols::xray::security] = srv.security; + streamSettings[px::security] = srv.security; - // TLS settings - if (srv.security == "tls") { + if (srv.security == QLatin1String("tls")) { QJsonObject tlsSettings; - if (!srv.sni.isEmpty()) { - tlsSettings[amnezia::protocols::xray::serverName] = srv.sni; + 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(','))) { + const QString t = a.trimmed(); + if (!t.isEmpty()) + alpnArray.append(t); } - if (!srv.alpn.isEmpty()) { - QJsonArray alpnArray; - // alpn may be comma-separated: "HTTP/2,HTTP/1.1" - for (const QString &a : srv.alpn.split(",")) { - alpnArray.append(a.trimmed()); - } - tlsSettings["alpn"] = alpnArray; - } - if (!srv.fingerprint.isEmpty()) { - tlsSettings[amnezia::protocols::xray::fingerprint] = srv.fingerprint; - } - streamSettings["tlsSettings"] = tlsSettings; + 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; } - // Reality settings - if (srv.security == "reality") { + if (srv.security == QLatin1String("reality")) { QJsonObject realSettings; - if (!srv.fingerprint.isEmpty()) - { - realSettings[amnezia::protocols::xray::fingerprint] = srv.fingerprint; - } - if (!srv.sni.isEmpty()) - { - realSettings[amnezia::protocols::xray::serverName] = srv.sni; - } - // publicKey and shortId are filled in createConfig after fetching from server - streamSettings[amnezia::protocols::xray::realitySettings] = 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 transport settings - if (srv.transport == "xhttp") { - QJsonObject xhttpObj; - - if (!xhttp.host.isEmpty()) - { - xhttpObj["host"] = xhttp.host; - } + // 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()) - { - xhttpObj["path"] = xhttp.path; - } - if (!xhttp.mode.isEmpty()) - { - xhttpObj["mode"] = xhttp.mode; - } + xo[QStringLiteral("path")] = xhttp.path; + xo[QStringLiteral("mode")] = normalizeXhttpMode(xhttp.mode); - // headers - if (xhttp.headersTemplate == "HTTP") { + if (xhttp.headersTemplate.compare(QLatin1String("HTTP"), Qt::CaseInsensitive) == 0) { QJsonObject headers; - headers["Host"] = xhttp.host; - xhttpObj["headers"] = headers; + headers[QStringLiteral("Host")] = hostEff; + xo[QStringLiteral("headers")] = headers; } - if (!xhttp.uplinkMethod.isEmpty()) - { - xhttpObj["method"] = xhttp.uplinkMethod; - } - if (xhttp.disableGrpc) - { - xhttpObj["noGRPCHeader"] = true; - } - if (xhttp.disableSse) - { - xhttpObj["noSSEHeader"] = true; + 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("sessionPlacement")] = sessPl; + const QString seqPl = normalizeSessionSeqPlacement(xhttp.seqPlacement); + if (!seqPl.isEmpty()) + xo[QStringLiteral("seqPlacement")] = seqPl; + if (!xhttp.sessionKey.isEmpty()) + xo[QStringLiteral("sessionKey")] = xhttp.sessionKey; + if (!xhttp.seqKey.isEmpty()) + xo[QStringLiteral("seqKey")] = xhttp.seqKey; + + xo[QStringLiteral("uplinkDataPlacement")] = normalizeUplinkDataPlacement(xhttp.uplinkDataPlacement); + 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")) { + const int v = ucs.toInt(); + QJsonObject chunkR; + chunkR[QStringLiteral("from")] = v; + chunkR[QStringLiteral("to")] = v; + xo[QStringLiteral("uplinkChunkSize")] = chunkR; } - // Session & Sequence - if (!xhttp.sessionPlacement.isEmpty() && xhttp.sessionPlacement != "None") - { - xhttpObj["scSessionPlacement"] = xhttp.sessionPlacement; - } - if (!xhttp.seqPlacement.isEmpty() && xhttp.seqPlacement != "None") - { - xhttpObj["scSeqPlacement"] = xhttp.seqPlacement; - } - if (!xhttp.uplinkDataPlacement.isEmpty()) - { - xhttpObj["scUplinkDataPlacement"] = xhttp.uplinkDataPlacement; - } - - // Traffic shaping - if (!xhttp.uplinkChunkSize.isEmpty() && xhttp.uplinkChunkSize != "0") - xhttpObj["xhttpUplinkChunkSize"] = xhttp.uplinkChunkSize.toInt(); if (!xhttp.scMaxBufferedPosts.isEmpty()) - xhttpObj["scMaxBufferedPosts"] = xhttp.scMaxBufferedPosts.toInt(); + xo[QStringLiteral("scMaxBufferedPosts")] = xhttp.scMaxBufferedPosts.toLongLong(); - // scMaxEachPostBytes range - if (!xhttp.scMaxEachPostBytesMin.isEmpty() || !xhttp.scMaxEachPostBytesMax.isEmpty()) { - QJsonObject range; - range["from"] = xhttp.scMaxEachPostBytesMin.toInt(); - range["to"] = xhttp.scMaxEachPostBytesMax.toInt(); - xhttpObj["scMaxEachPostBytes"] = range; + 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()) { + QJsonObject br; + br[QStringLiteral("from")] = pad.bytesMin.isEmpty() ? 1 : pad.bytesMin.toInt(); + br[QStringLiteral("to")] = pad.bytesMax.isEmpty() ? (pad.bytesMin.isEmpty() ? 256 : pad.bytesMin.toInt()) + : pad.bytesMax.toInt(); + xo[QStringLiteral("xPaddingBytes")] = br; + } + xo[QStringLiteral("xPaddingKey")] = pad.key.isEmpty() ? QStringLiteral("x_padding") : pad.key; + xo[QStringLiteral("xPaddingHeader")] = pad.header.isEmpty() ? QStringLiteral("X-Padding") : 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); } - // scMinPostsIntervalMs range - if (!xhttp.scMinPostsIntervalMsMin.isEmpty() || !xhttp.scMinPostsIntervalMsMax.isEmpty()) { - QJsonObject range; - range["from"] = xhttp.scMinPostsIntervalMsMin.toInt(); - range["to"] = xhttp.scMinPostsIntervalMsMax.toInt(); - xhttpObj["scMinPostsIntervalMs"] = range; - } - - // scStreamUpServerSecs range - if (!xhttp.scStreamUpServerSecsMin.isEmpty() || !xhttp.scStreamUpServerSecsMax.isEmpty()) { - QJsonObject range; - range["from"] = xhttp.scStreamUpServerSecsMin.toInt(); - range["to"] = xhttp.scStreamUpServerSecsMax.toInt(); - xhttpObj["scStreamUpServerSecs"] = range; - } - - // xPadding - if (xhttp.xPadding.obfsMode) { - QJsonObject paddingObj; - if (!xhttp.xPadding.bytesMin.isEmpty() || !xhttp.xPadding.bytesMax.isEmpty()) { - QJsonObject bytesRange; - bytesRange["from"] = xhttp.xPadding.bytesMin.toInt(); - bytesRange["to"] = xhttp.xPadding.bytesMax.toInt(); - paddingObj["xPaddingBytes"] = bytesRange; - } - if (!xhttp.xPadding.key.isEmpty()) - { - paddingObj["xPaddingKey"] = xhttp.xPadding.key; - } - if (!xhttp.xPadding.header.isEmpty()) - { - paddingObj["xPaddingHeader"] = xhttp.xPadding.header; - } - if (!xhttp.xPadding.placement.isEmpty()) - { - paddingObj["xPaddingPlacement"] = xhttp.xPadding.placement; - } - if (!xhttp.xPadding.method.isEmpty()) - { - paddingObj["xPaddingMethod"] = xhttp.xPadding.method; - } - xhttpObj["xPadding"] = paddingObj; - } - - // xmux + // xmux: Xray has no "enabled" flag; omit object when UI disables multiplex tuning. if (xhttp.xmux.enabled) { - QJsonObject muxObj; - muxObj["enabled"] = true; - - auto addRange = [&](const char *key, const QString &minV, const QString &maxV) { - if (!minV.isEmpty() || !maxV.isEmpty()) { - QJsonObject r; - r["from"] = minV.toInt(); - r["to"] = maxV.toInt(); - muxObj[key] = r; - } + QJsonObject mux; + auto addMuxRange = [&](const char *key, const QString &a, const QString &b) { + if (a.isEmpty() && b.isEmpty()) + return; + QJsonObject r; + r[QStringLiteral("from")] = a.isEmpty() ? 0 : a.toInt(); + r[QStringLiteral("to")] = b.isEmpty() ? 0 : b.toInt(); + mux[QString::fromUtf8(key)] = r; }; - - addRange("maxConcurrency", xhttp.xmux.maxConcurrencyMin, xhttp.xmux.maxConcurrencyMax); - addRange("maxConnections", xhttp.xmux.maxConnectionsMin, xhttp.xmux.maxConnectionsMax); - addRange("cMaxReuseTimes", xhttp.xmux.cMaxReuseTimesMin, xhttp.xmux.cMaxReuseTimesMax); - addRange("hMaxRequestTimes", xhttp.xmux.hMaxRequestTimesMin, xhttp.xmux.hMaxRequestTimesMax); - addRange("hMaxReusableSecs", xhttp.xmux.hMaxReusableSecsMin, xhttp.xmux.hMaxReusableSecsMax); - + 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()) - { - muxObj["hKeepAlivePeriod"] = xhttp.xmux.hKeepAlivePeriod.toInt(); - } - - xhttpObj["xmux"] = muxObj; + mux[QStringLiteral("hKeepAlivePeriod")] = xhttp.xmux.hKeepAlivePeriod.toLongLong(); + if (!mux.isEmpty()) + xo[QStringLiteral("xmux")] = mux; } - streamSettings["xhttpSettings"] = xhttpObj; + streamSettings[QStringLiteral("xhttpSettings")] = xo; } - // mKCP transport settings - if (srv.transport == "mkcp") { + if (srv.transport == QLatin1String("mkcp")) { QJsonObject kcpObj; - if (!mkcp.tti.isEmpty()) - { - kcpObj["tti"] = mkcp.tti.toInt(); - } - if (!mkcp.uplinkCapacity.isEmpty()) - { - kcpObj["uplinkCapacity"] = mkcp.uplinkCapacity.toInt(); - } - if (!mkcp.downlinkCapacity.isEmpty()) - { - kcpObj["downlinkCapacity"] = mkcp.downlinkCapacity.toInt(); - } - if (!mkcp.readBufferSize.isEmpty()) - { - kcpObj["readBufferSize"] = mkcp.readBufferSize.toInt(); - } - if (!mkcp.writeBufferSize.isEmpty()) - { - kcpObj["writeBufferSize"] = mkcp.writeBufferSize.toInt(); - } - kcpObj["congestion"] = mkcp.congestion; - streamSettings["kcpSettings"] = 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; @@ -446,7 +497,7 @@ ProtocolConfig XrayConfigurator::createConfig(const ServerCredentials &credentia // Build full client config QJsonObject inboundObj; - inboundObj["listen"] = amnezia::protocols::xray::defaultLocalAddr; + inboundObj["listen"] = amnezia::protocols::xray::defaultLocalListenAddr; inboundObj[amnezia::protocols::xray::port] = amnezia::protocols::xray::defaultLocalProxyPort; inboundObj["protocol"] = "socks"; inboundObj[amnezia::protocols::xray::settings] = QJsonObject { { "udp", true } }; diff --git a/client/core/configurators/xrayConfigurator.h b/client/core/configurators/xrayConfigurator.h index fe1c84cbd..968e85b44 100644 --- a/client/core/configurators/xrayConfigurator.h +++ b/client/core/configurators/xrayConfigurator.h @@ -20,6 +20,9 @@ public: const amnezia::DnsSettings &dnsSettings, amnezia::ErrorCode &errorCode) override; + amnezia::ProtocolConfig processConfigWithLocalSettings(const amnezia::ConnectionSettings &settings, + amnezia::ProtocolConfig protocolConfig) override; + private: QString prepareServerConfig(const amnezia::ServerCredentials &credentials, amnezia::DockerContainer container, const amnezia::ContainerConfig &containerConfig, const amnezia::DnsSettings &dnsSettings, diff --git a/client/core/installers/xrayInstaller.cpp b/client/core/installers/xrayInstaller.cpp index 47ddf52a9..30e61cc2a 100644 --- a/client/core/installers/xrayInstaller.cpp +++ b/client/core/installers/xrayInstaller.cpp @@ -14,8 +14,18 @@ #include "core/models/protocols/xrayProtocolConfig.h" #include "logger.h" -namespace { +namespace +{ Logger logger("XrayInstaller"); + + // Xray expects uTLS preset names (chrome, firefox, …). Old Amnezia/server templates used "Mozilla/5.0". + QString normalizeXrayFingerprint(const QString &fp) + { + if (fp.isEmpty() || fp.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)) { + return QString::fromLatin1(protocols::xray::defaultFingerprint); + } + return fp; + } } using namespace amnezia; @@ -103,14 +113,14 @@ ErrorCode XrayInstaller::extractConfigFromContainer(DockerContainer container, c srv.site = srv.sni; } - srv.fingerprint = rs.value(protocols::xray::fingerprint).toString("Mozilla/5.0"); + 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 = tls.value(protocols::xray::fingerprint).toString("Mozilla/5.0"); + srv.fingerprint = normalizeXrayFingerprint(tls.value(protocols::xray::fingerprint).toString()); QJsonArray alpnArr = tls.value("alpn").toArray(); QStringList alpnList; @@ -129,29 +139,94 @@ ErrorCode XrayInstaller::extractConfigFromContainer(DockerContainer container, c } } - // ── XHTTP settings ──────────────────────────────────────────────── + // ── XHTTP settings (Xray-core SplitHTTPConfig + legacy Amnezia keys) ── if (srv.transport == "xhttp") { QJsonObject xhttpObj = streamSettings.value("xhttpSettings").toObject(); - srv.xhttp.mode = xhttpObj.value("mode").toString("Auto"); + { + 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(); - srv.xhttp.uplinkMethod = xhttpObj.value("method").toString("POST"); + + { + const QJsonObject hdrs = xhttpObj.value("headers").toObject(); + if (hdrs.contains(QLatin1String("Host")) || !hdrs.isEmpty()) + srv.xhttp.headersTemplate = QStringLiteral("HTTP"); + } + + 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); - srv.xhttp.sessionPlacement = xhttpObj.value("scSessionPlacement").toString("Path"); - srv.xhttp.seqPlacement = xhttpObj.value("scSeqPlacement").toString("Path"); - srv.xhttp.uplinkDataPlacement = xhttpObj.value("scUplinkDataPlacement").toString("Body"); + 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("sessionPlacement").toString(); + if (sess.isEmpty()) + sess = xhttpObj.value("scSessionPlacement").toString(); + srv.xhttp.sessionPlacement = sessionSeqUi(sess); - if (xhttpObj.contains("xhttpUplinkChunkSize")) { - srv.xhttp.uplinkChunkSize = QString::number(xhttpObj["xhttpUplinkChunkSize"].toInt()); + 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("sessionKey").toString(); + srv.xhttp.seqKey = xhttpObj.value("seqKey").toString(); + srv.xhttp.uplinkDataKey = xhttpObj.value("uplinkDataKey").toString(); + + if (xhttpObj.contains(QLatin1String("uplinkChunkSize"))) { + QJsonObject uc = xhttpObj.value("uplinkChunkSize").toObject(); + if (!uc.isEmpty()) + srv.xhttp.uplinkChunkSize = QString::number(uc.value("from").toInt()); + } else if (xhttpObj.contains(QLatin1String("xhttpUplinkChunkSize"))) { + srv.xhttp.uplinkChunkSize = QString::number(xhttpObj.value("xhttpUplinkChunkSize").toInt()); } - if (xhttpObj.contains("scMaxBufferedPosts")) { - srv.xhttp.scMaxBufferedPosts = QString::number(xhttpObj["scMaxBufferedPosts"].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) { - QJsonObject r = xhttpObj.value(key).toObject(); + QJsonObject r = xhttpObj.value(QLatin1String(key)).toObject(); if (!r.isEmpty()) { minOut = QString::number(r.value("from").toInt()); maxOut = QString::number(r.value("to").toInt()); @@ -161,28 +236,51 @@ ErrorCode XrayInstaller::extractConfigFromContainer(DockerContainer container, c readRange("scMinPostsIntervalMs", srv.xhttp.scMinPostsIntervalMsMin, srv.xhttp.scMinPostsIntervalMsMax); readRange("scStreamUpServerSecs", srv.xhttp.scStreamUpServerSecsMin, srv.xhttp.scStreamUpServerSecsMax); - // xPadding - if (xhttpObj.contains("xPadding")) { - QJsonObject pad = xhttpObj["xPadding"].toObject(); - srv.xhttp.xPadding.obfsMode = true; + 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("Cookie"); - srv.xhttp.xPadding.method = pad.value("xPaddingMethod").toString("Repeat-x"); + srv.xhttp.xPadding.placement = pad.value("xPaddingPlacement").toString(); + srv.xhttp.xPadding.method = pad.value("xPaddingMethod").toString(); QJsonObject bytesRange = pad.value("xPaddingBytes").toObject(); if (!bytesRange.isEmpty()) { srv.xhttp.xPadding.bytesMin = QString::number(bytesRange.value("from").toInt()); srv.xhttp.xPadding.bytesMax = QString::number(bytesRange.value("to").toInt()); } + 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.value("xPaddingBytes").toObject().isEmpty()) { + 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; + } } - // xmux - if (xhttpObj.contains("xmux")) { - QJsonObject mux = xhttpObj["xmux"].toObject(); - srv.xhttp.xmux.enabled = mux.value("enabled").toBool(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) { - QJsonObject r = mux.value(key).toObject(); + QJsonObject r = mux.value(QLatin1String(key)).toObject(); if (!r.isEmpty()) { minOut = QString::number(r.value("from").toInt()); maxOut = QString::number(r.value("to").toInt()); @@ -194,8 +292,8 @@ ErrorCode XrayInstaller::extractConfigFromContainer(DockerContainer container, c readMuxRange("hMaxRequestTimes", srv.xhttp.xmux.hMaxRequestTimesMin, srv.xhttp.xmux.hMaxRequestTimesMax); readMuxRange("hMaxReusableSecs", srv.xhttp.xmux.hMaxReusableSecsMin, srv.xhttp.xmux.hMaxReusableSecsMax); - if (mux.contains("hKeepAlivePeriod")) - srv.xhttp.xmux.hKeepAlivePeriod = QString::number(mux["hKeepAlivePeriod"].toInt()); + if (mux.contains(QLatin1String("hKeepAlivePeriod"))) + srv.xhttp.xmux.hKeepAlivePeriod = QString::number(mux.value("hKeepAlivePeriod").toVariant().toLongLong()); } } diff --git a/client/core/models/protocols/xrayProtocolConfig.cpp b/client/core/models/protocols/xrayProtocolConfig.cpp index be3edee3b..194559e90 100644 --- a/client/core/models/protocols/xrayProtocolConfig.cpp +++ b/client/core/models/protocols/xrayProtocolConfig.cpp @@ -13,11 +13,6 @@ using namespace ProtocolUtils; namespace amnezia { - -// ═════════════════════════════════════════════════════════════════════════════ -// XrayXPaddingConfig -// ═════════════════════════════════════════════════════════════════════════════ - QJsonObject XrayXPaddingConfig::toJson() const { QJsonObject obj; @@ -37,17 +32,13 @@ XrayXPaddingConfig XrayXPaddingConfig::fromJson(const QJsonObject &json) c.bytesMin = json.value(configKey::xPaddingBytesMin).toString(); c.bytesMax = json.value(configKey::xPaddingBytesMax).toString(); c.obfsMode = json.value(configKey::xPaddingObfsMode).toBool(true); - c.key = json.value(configKey::xPaddingKey).toString("www.googletagmanager.com"); + c.key = json.value(configKey::xPaddingKey).toString(protocols::xray::defaultSite); c.header = json.value(configKey::xPaddingHeader).toString(); - c.placement = json.value(configKey::xPaddingPlacement).toString("Cookie"); - c.method = json.value(configKey::xPaddingMethod).toString("Repeat-x"); + c.placement = json.value(configKey::xPaddingPlacement).toString(protocols::xray::defaultXPaddingPlacement); + c.method = json.value(configKey::xPaddingMethod).toString(protocols::xray::defaultXPaddingMethod); return c; } -// ═════════════════════════════════════════════════════════════════════════════ -// XrayXmuxConfig -// ═════════════════════════════════════════════════════════════════════════════ - QJsonObject XrayXmuxConfig::toJson() const { QJsonObject obj; @@ -84,10 +75,6 @@ XrayXmuxConfig XrayXmuxConfig::fromJson(const QJsonObject &json) return c; } -// ═════════════════════════════════════════════════════════════════════════════ -// XrayXhttpConfig -// ═════════════════════════════════════════════════════════════════════════════ - QJsonObject XrayXhttpConfig::toJson() const { QJsonObject obj; @@ -124,19 +111,19 @@ QJsonObject XrayXhttpConfig::toJson() const XrayXhttpConfig XrayXhttpConfig::fromJson(const QJsonObject &json) { XrayXhttpConfig c; - c.mode = json.value(configKey::xhttpMode).toString("Auto"); - c.host = json.value(configKey::xhttpHost).toString("www.googletagmanager.com"); + c.mode = json.value(configKey::xhttpMode).toString(protocols::xray::defaultXhttpMode); + c.host = json.value(configKey::xhttpHost).toString(protocols::xray::defaultSite); c.path = json.value(configKey::xhttpPath).toString(); - c.headersTemplate = json.value(configKey::xhttpHeadersTemplate).toString("HTTP"); - c.uplinkMethod = json.value(configKey::xhttpUplinkMethod).toString("POST"); + c.headersTemplate = json.value(configKey::xhttpHeadersTemplate).toString(protocols::xray::defaultXhttpHeadersTemplate); + c.uplinkMethod = json.value(configKey::xhttpUplinkMethod).toString(protocols::xray::defaultXhttpUplinkMethod); c.disableGrpc = json.value(configKey::xhttpDisableGrpc).toBool(true); c.disableSse = json.value(configKey::xhttpDisableSse).toBool(true); - c.sessionPlacement = json.value(configKey::xhttpSessionPlacement).toString("Path"); - c.sessionKey = json.value(configKey::xhttpSessionKey).toString("Path"); - c.seqPlacement = json.value(configKey::xhttpSeqPlacement).toString("Path"); + c.sessionPlacement = json.value(configKey::xhttpSessionPlacement).toString(protocols::xray::defaultXhttpSessionPlacement); + c.sessionKey = json.value(configKey::xhttpSessionKey).toString(); + c.seqPlacement = json.value(configKey::xhttpSeqPlacement).toString(protocols::xray::defaultXhttpSessionPlacement); c.seqKey = json.value(configKey::xhttpSeqKey).toString(); - c.uplinkDataPlacement = json.value(configKey::xhttpUplinkDataPlacement).toString("Body"); + c.uplinkDataPlacement = json.value(configKey::xhttpUplinkDataPlacement).toString(protocols::xray::defaultXhttpUplinkDataPlacement); c.uplinkDataKey = json.value(configKey::xhttpUplinkDataKey).toString(); c.uplinkChunkSize = json.value(configKey::xhttpUplinkChunkSize).toString("0"); @@ -154,10 +141,6 @@ XrayXhttpConfig XrayXhttpConfig::fromJson(const QJsonObject &json) return c; } -// ═════════════════════════════════════════════════════════════════════════════ -// XrayMkcpConfig -// ═════════════════════════════════════════════════════════════════════════════ - QJsonObject XrayMkcpConfig::toJson() const { QJsonObject obj; @@ -182,10 +165,6 @@ XrayMkcpConfig XrayMkcpConfig::fromJson(const QJsonObject &json) return c; } -// ═════════════════════════════════════════════════════════════════════════════ -// XrayServerConfig -// ═════════════════════════════════════════════════════════════════════════════ - QJsonObject XrayServerConfig::toJson() const { QJsonObject obj; @@ -224,14 +203,17 @@ XrayServerConfig XrayServerConfig::fromJson(const QJsonObject &json) c.isThirdPartyConfig = json.value(configKey::isThirdPartyConfig).toBool(false); // New: Security - c.security = json.value(configKey::xraySecurity).toString("reality"); - c.flow = json.value(configKey::xrayFlow).toString("xtls-rprx-vision"); - c.fingerprint = json.value(configKey::xrayFingerprint).toString("Mozilla/5.0"); - c.sni = json.value(configKey::xraySni).toString("cdn.example.com"); - c.alpn = json.value(configKey::xrayAlpn).toString("HTTP/2"); + c.security = json.value(configKey::xraySecurity).toString(protocols::xray::defaultSecurity); + c.flow = json.value(configKey::xrayFlow).toString(protocols::xray::defaultFlow); + c.fingerprint = json.value(configKey::xrayFingerprint).toString(protocols::xray::defaultFingerprint); + if (c.fingerprint.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)) { + c.fingerprint = QString::fromLatin1(protocols::xray::defaultFingerprint); + } + c.sni = json.value(configKey::xraySni).toString(protocols::xray::defaultSni); + c.alpn = json.value(configKey::xrayAlpn).toString(protocols::xray::defaultAlpn); // New: Transport - c.transport = json.value(configKey::xrayTransport).toString("raw"); + c.transport = json.value(configKey::xrayTransport).toString(protocols::xray::defaultTransport); c.xhttp = XrayXhttpConfig::fromJson(json.value("xhttp").toObject()); c.mkcp = XrayMkcpConfig::fromJson(json.value("mkcp").toObject()); @@ -249,10 +231,6 @@ bool XrayServerConfig::hasEqualServerSettings(const XrayServerConfig &other) con && sni == other.sni; } -// ═════════════════════════════════════════════════════════════════════════════ -// XrayClientConfig (unchanged logic, kept as-is) -// ═════════════════════════════════════════════════════════════════════════════ - QJsonObject XrayClientConfig::toJson() const { QJsonObject obj; @@ -300,10 +278,6 @@ XrayClientConfig XrayClientConfig::fromJson(const QJsonObject &json) return c; } -// ═════════════════════════════════════════════════════════════════════════════ -// XrayProtocolConfig (unchanged logic, kept as-is) -// ═════════════════════════════════════════════════════════════════════════════ - QJsonObject XrayProtocolConfig::toJson() const { QJsonObject obj = serverConfig.toJson(); diff --git a/client/core/models/protocols/xrayProtocolConfig.h b/client/core/models/protocols/xrayProtocolConfig.h index a4ae2645f..73c160bc7 100644 --- a/client/core/models/protocols/xrayProtocolConfig.h +++ b/client/core/models/protocols/xrayProtocolConfig.h @@ -2,6 +2,7 @@ #define XRAYPROTOCOLCONFIG_H #include +#include "core/utils/constants/protocolConstants.h" #include #include @@ -15,8 +16,8 @@ struct XrayXPaddingConfig { bool obfsMode = true; // xPaddingObfsMode QString key; // xPaddingKey QString header; // xPaddingHeader - QString placement = "Cookie"; // xPaddingPlacement: Cookie|Header|Query|Body - QString method = "Repeat-x"; // xPaddingMethod: Repeat-x|Random|Zero + QString placement = protocols::xray::defaultXPaddingPlacement; // xPaddingPlacement: Cookie|Header|Query|Body + QString method = protocols::xray::defaultXPaddingMethod; // xPaddingMethod: Repeat-x|Random|Zero QJsonObject toJson() const; static XrayXPaddingConfig fromJson(const QJsonObject &json); @@ -44,31 +45,31 @@ struct XrayXmuxConfig { // ── XHTTP transport ─────────────────────────────────────────────────────────── struct XrayXhttpConfig { - QString mode = "Auto"; // Auto|Packet-up|Stream-up|Stream-one - QString host = "www.googletagmanager.com"; + QString mode = protocols::xray::defaultXhttpMode; // Auto|Packet-up|Stream-up|Stream-one + QString host = protocols::xray::defaultXhttpHost; QString path; - QString headersTemplate = "HTTP"; // HTTP|None - QString uplinkMethod = "POST"; // POST|PUT|PATCH + QString headersTemplate = protocols::xray::defaultXhttpHeadersTemplate; // HTTP|None + QString uplinkMethod = protocols::xray::defaultXhttpUplinkMethod; // POST|PUT|PATCH bool disableGrpc = true; bool disableSse = true; // Session & Sequence - QString sessionPlacement = "Path"; // Path|Header|Cookie|None - QString sessionKey = "Path"; - QString seqPlacement = "Path"; + QString sessionPlacement = protocols::xray::defaultXhttpSessionPlacement; + QString sessionKey = protocols::xray::defaultXhttpSessionKey; + QString seqPlacement = protocols::xray::defaultXhttpSeqPlacement; QString seqKey; - QString uplinkDataPlacement = "Body"; // Body|Query + QString uplinkDataPlacement = protocols::xray::defaultXhttpUplinkDataPlacement; QString uplinkDataKey; // Traffic Shaping - QString uplinkChunkSize = "0"; + QString uplinkChunkSize = protocols::xray::defaultXhttpUplinkChunkSize; QString scMaxBufferedPosts; - QString scMaxEachPostBytesMin = "1"; - QString scMaxEachPostBytesMax = "100"; - QString scMinPostsIntervalMsMin = "100"; - QString scMinPostsIntervalMsMax = "800"; - QString scStreamUpServerSecsMin = "1"; - QString scStreamUpServerSecsMax = "100"; + 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; XrayXPaddingConfig xPadding; XrayXmuxConfig xmux; @@ -100,14 +101,14 @@ struct XrayServerConfig { bool isThirdPartyConfig = false; // New: Security - QString security = "reality"; // none|tls|reality - QString flow = "xtls-rprx-vision"; // ""|xtls-rprx-vision|xtls-rprx-vision-udp443 - QString fingerprint = "Mozilla/5.0"; - QString sni = "cdn.example.com"; - QString alpn = "HTTP/2"; // TLS only: HTTP/2|HTTP/1.1|HTTP/2,HTTP/1.1 + QString security = protocols::xray::defaultSecurity; + QString flow = protocols::xray::defaultFlow; + QString fingerprint = protocols::xray::defaultFingerprint; + QString sni = protocols::xray::defaultSni; + QString alpn = protocols::xray::defaultAlpn; // New: Transport - QString transport = "raw"; // raw|xhttp|mkcp + QString transport = protocols::xray::defaultTransport; XrayXhttpConfig xhttp; XrayMkcpConfig mkcp; diff --git a/client/core/protocols/xrayProtocol.cpp b/client/core/protocols/xrayProtocol.cpp old mode 100755 new mode 100644 index 893b8367e..ffa47361a --- a/client/core/protocols/xrayProtocol.cpp +++ b/client/core/protocols/xrayProtocol.cpp @@ -2,6 +2,7 @@ #include "core/protocols/protocolUtils.h" #include "core/utils/constants/configKeys.h" +#include "core/utils/constants/protocolConstants.h" #include "core/utils/ipcClient.h" #include "core/utils/networkUtilities.h" #include "core/utils/serialization/serialization.h" @@ -9,6 +10,7 @@ #include #include +#include #include #include #include @@ -79,12 +81,29 @@ ErrorCode XrayProtocol::start() m_socksPassword = creds.password; m_socksPort = creds.port; - const QString xrayConfigStr = QJsonDocument(m_xrayConfig).toJson(QJsonDocument::Compact); + QString xrayConfigStr = QJsonDocument(m_xrayConfig).toJson(QJsonDocument::Compact); if (xrayConfigStr.isEmpty()) { qCritical() << "Xray config is empty"; return ErrorCode::XrayExecutableCrashed; } + // Fix fingerprint: old configs may contain "Mozilla/5.0" which xray-core rejects. + // Replace with the correct default at runtime so stale stored configs still work. + if (xrayConfigStr.contains("Mozilla/5.0", Qt::CaseInsensitive)) { + xrayConfigStr.replace("Mozilla/5.0", amnezia::protocols::xray::defaultFingerprint, + Qt::CaseInsensitive); + qDebug() << "XrayProtocol: patched legacy fingerprint to" + << amnezia::protocols::xray::defaultFingerprint; + } + + // Fix inbound listen address: old configs may use "10.33.0.2" which doesn't exist + // until TUN is created. xray must listen on 127.0.0.1 so tun2socks can connect. + if (xrayConfigStr.contains(amnezia::protocols::xray::defaultLocalAddr)) { + xrayConfigStr.replace(amnezia::protocols::xray::defaultLocalAddr, + amnezia::protocols::xray::defaultLocalListenAddr); + qDebug() << "XrayProtocol: patched legacy inbound listen address to 127.0.0.1"; + } + return IpcClient::withInterface( [&](QSharedPointer iface) { auto xrayStart = iface->xrayStart(xrayConfigStr); @@ -188,6 +207,33 @@ ErrorCode XrayProtocol::startTun2Socks() connect( m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::finished, this, [this](int exitCode, QProcess::ExitStatus exitStatus) { + // Check stdout for "resource busy" — the TUN device was not yet released + // by the previous tun2socks instance. Retry after a short delay. + bool resourceBusy = false; + if (m_tun2socksProcess) { + auto readOut = m_tun2socksProcess->readAllStandardOutput(); + if (readOut.waitForFinished()) { + resourceBusy = readOut.returnValue().contains("resource busy"); + } + } + + if (resourceBusy && m_tun2socksRetryCount < maxTun2SocksRetries) { + m_tun2socksRetryCount++; + qWarning() << QString("Tun2socks: TUN resource busy, retrying (%1/%2) in %3ms...") + .arg(m_tun2socksRetryCount) + .arg(maxTun2SocksRetries) + .arg(tun2socksRetryDelayMs); + QTimer::singleShot(tun2socksRetryDelayMs, this, [this]() { + if (ErrorCode err = startTun2Socks(); err != ErrorCode::NoError) { + stop(); + setLastError(err); + } + }); + return; + } + + m_tun2socksRetryCount = 0; + if (exitStatus == QProcess::ExitStatus::CrashExit) { qCritical() << "Tun2socks process crashed!"; } else { diff --git a/client/core/protocols/xrayProtocol.h b/client/core/protocols/xrayProtocol.h index e831ab2f4..55b6d1d5c 100644 --- a/client/core/protocols/xrayProtocol.h +++ b/client/core/protocols/xrayProtocol.h @@ -35,6 +35,9 @@ private: int m_socksPort = 10808; QSharedPointer m_tun2socksProcess; + int m_tun2socksRetryCount = 0; + static constexpr int maxTun2SocksRetries = 5; + static constexpr int tun2socksRetryDelayMs = 400; }; #endif // XRAYPROTOCOL_H diff --git a/client/core/utils/constants/protocolConstants.h b/client/core/utils/constants/protocolConstants.h index 01e2a151a..3b8fd9c38 100644 --- a/client/core/utils/constants/protocolConstants.h +++ b/client/core/utils/constants/protocolConstants.h @@ -57,6 +57,40 @@ namespace amnezia constexpr char defaultPort[] = "443"; constexpr char defaultLocalProxyPort[] = "10808"; constexpr char defaultLocalAddr[] = "10.33.0.2"; + constexpr char defaultLocalListenAddr[] = "127.0.0.1"; + + constexpr char defaultSecurity[] = "reality"; + constexpr char defaultFlow[] = "xtls-rprx-vision"; + constexpr char defaultTransport[] = "raw"; + constexpr char defaultFingerprint[] = "chrome"; + constexpr char defaultSni[] = "cdn.example.com"; + constexpr char defaultAlpn[] = "HTTP/2"; + + constexpr char defaultXhttpMode[] = "Auto"; + constexpr char defaultXhttpHeadersTemplate[] = "HTTP"; + constexpr char defaultXhttpUplinkMethod[] = "POST"; + constexpr char defaultXhttpSessionPlacement[] = "Path"; + constexpr char defaultXhttpSessionKey[] = "Path"; + constexpr char defaultXhttpSeqPlacement[] = "Path"; + constexpr char defaultXhttpUplinkDataPlacement[] = "Body"; + + constexpr char defaultXhttpHost[] = "www.googletagmanager.com"; + constexpr char defaultXhttpUplinkChunkSize[] = "0"; + constexpr char defaultXhttpScMaxEachPostBytesMin[] = "1"; + constexpr char defaultXhttpScMaxEachPostBytesMax[] = "100"; + constexpr char defaultXhttpScMinPostsIntervalMsMin[] = "100"; + constexpr char defaultXhttpScMinPostsIntervalMsMax[] = "800"; + constexpr char defaultXhttpScStreamUpServerSecsMin[] = "1"; + constexpr char defaultXhttpScStreamUpServerSecsMax[] = "100"; + + constexpr char defaultXPaddingPlacement[] = "Cookie"; + constexpr char defaultXPaddingMethod[] = "Repeat-x"; + + constexpr char defaultMkcpTti[] = "50"; + constexpr char defaultMkcpUplinkCapacity[] = "5"; + constexpr char defaultMkcpDownlinkCapacity[] = "20"; + constexpr char defaultMkcpReadBufferSize[] = "2"; + constexpr char defaultMkcpWriteBufferSize[] = "2"; constexpr char outbounds[] = "outbounds"; constexpr char inbounds[] = "inbounds"; diff --git a/client/ui/models/protocols/xrayConfigModel.cpp b/client/ui/models/protocols/xrayConfigModel.cpp index d8a7e8d4f..6e215afcf 100644 --- a/client/ui/models/protocols/xrayConfigModel.cpp +++ b/client/ui/models/protocols/xrayConfigModel.cpp @@ -263,31 +263,75 @@ void XrayConfigModel::updateModel(amnezia::DockerContainer container, endResetModel(); } -void XrayConfigModel::applyDefaultsToServerConfig(amnezia::XrayServerConfig& config) +void XrayConfigModel::applyDefaultsToServerConfig(amnezia::XrayServerConfig &config) { - if (config.port.isEmpty()) + if (config.port.isEmpty()) { config.port = protocols::xray::defaultPort; + } - if (config.site.isEmpty()) + if (config.site.isEmpty()) { config.site = protocols::xray::defaultSite; + } - if (config.transport.isEmpty()) - config.transport = "raw"; + if (config.transport.isEmpty()) { + config.transport = protocols::xray::defaultTransport; + } - if (config.security.isEmpty()) - config.security = "reality"; + if (config.security.isEmpty()) { + config.security = protocols::xray::defaultSecurity; + } - if (config.flow.isEmpty()) - config.flow = "xtls-rprx-vision"; + if (config.flow.isEmpty()) { + config.flow = protocols::xray::defaultFlow; + } - if (config.fingerprint.isEmpty()) - config.fingerprint = "Mozilla/5.0"; + 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 = "cdn.example.com"; + if (config.sni.isEmpty()) { + config.sni = protocols::xray::defaultSni; + } - if (config.alpn.isEmpty()) - config.alpn = "HTTP/2"; + 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.headersTemplate.isEmpty()) { + config.xhttp.headersTemplate = protocols::xray::defaultXhttpHeadersTemplate; + } + 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; + } } amnezia::XrayProtocolConfig XrayConfigModel::getProtocolConfig() @@ -396,3 +440,104 @@ void XrayConfigModel::applyServerConfig(const amnezia::XrayServerConfig &serverC m_originalProtocolConfig = m_protocolConfig; endResetModel(); } + +QStringList XrayConfigModel::flowOptions() +{ + return { + "", // Empty (no flow) + "xtls-rprx-vision", + "xtls-rprx-vision-udp443" + }; +} + +QStringList XrayConfigModel::securityOptions() +{ + return { "none", "tls", "reality" }; +} + +QStringList XrayConfigModel::transportOptions() +{ + return { "raw", "xhttp", "mkcp" }; +} + +QStringList XrayConfigModel::fingerprintOptions() +{ + return { "chrome", "firefox", "safari", "ios", "android", "edge", "360", "qq", "random" }; +} + +QStringList XrayConfigModel::alpnOptions() +{ + return { "HTTP/2", "HTTP/1.1", "HTTP/2,HTTP/1.1" }; +} + +QStringList XrayConfigModel::xhttpModeOptions() +{ + return { "Auto", "Packet-up", "Stream-up", "Stream-one" }; +} + +QStringList XrayConfigModel::xhttpHeadersTemplateOptions() +{ + return { "HTTP", "None" }; +} + +QStringList XrayConfigModel::xhttpUplinkMethodOptions() +{ + return { "POST", "PUT", "PATCH" }; +} + +QStringList XrayConfigModel::xhttpSessionPlacementOptions() +{ + return { "Path", "Header", "Cookie", "None" }; +} + +QStringList XrayConfigModel::xhttpSessionKeyOptions() +{ + return { "Path", "Header", "None" }; +} + +QStringList XrayConfigModel::xhttpSeqPlacementOptions() +{ + return { "Path", "Header", "Cookie", "None" }; +} + +QStringList XrayConfigModel::xhttpUplinkDataPlacementOptions() +{ + // Matches splithttp uplink payload placement (packet-up / advanced) + return { "Body", "Auto", "Header", "Cookie" }; +} + +QStringList XrayConfigModel::xPaddingPlacementOptions() +{ + // Xray-core: cookie | header | query | queryInHeader (not "body") + return { "Cookie", "Header", "Query", "Query in header" }; +} + +QStringList XrayConfigModel::xPaddingMethodOptions() +{ + return { "Repeat-x", "Tokenish" }; +} + +QString XrayConfigModel::mkcpDefaultTti() +{ + return QString::fromLatin1(protocols::xray::defaultMkcpTti); +} + +QString XrayConfigModel::mkcpDefaultUplinkCapacity() +{ + return QString::fromLatin1(protocols::xray::defaultMkcpUplinkCapacity); +} + +QString XrayConfigModel::mkcpDefaultDownlinkCapacity() +{ + return QString::fromLatin1(protocols::xray::defaultMkcpDownlinkCapacity); +} + +QString XrayConfigModel::mkcpDefaultReadBufferSize() +{ + return QString::fromLatin1(protocols::xray::defaultMkcpReadBufferSize); +} + +QString XrayConfigModel::mkcpDefaultWriteBufferSize() +{ + return QString::fromLatin1(protocols::xray::defaultMkcpWriteBufferSize); +} diff --git a/client/ui/models/protocols/xrayConfigModel.h b/client/ui/models/protocols/xrayConfigModel.h index ac41f228c..c317e955a 100644 --- a/client/ui/models/protocols/xrayConfigModel.h +++ b/client/ui/models/protocols/xrayConfigModel.h @@ -2,6 +2,7 @@ #define XRAYCONFIGMODEL_H #include +#include #include "core/utils/containerEnum.h" #include "core/utils/containers/containerUtils.h" @@ -93,6 +94,29 @@ public: bool setData(const QModelIndex& index, const QVariant& value, int role) override; QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + // ── Static option lists (for QML DropDown models) ───────────────── + Q_INVOKABLE static QStringList flowOptions(); + Q_INVOKABLE static QStringList securityOptions(); + Q_INVOKABLE static QStringList transportOptions(); + Q_INVOKABLE static QStringList fingerprintOptions(); + Q_INVOKABLE static QStringList alpnOptions(); + Q_INVOKABLE static QStringList xhttpModeOptions(); + Q_INVOKABLE static QStringList xhttpHeadersTemplateOptions(); + Q_INVOKABLE static QStringList xhttpUplinkMethodOptions(); + Q_INVOKABLE static QStringList xhttpSessionPlacementOptions(); + Q_INVOKABLE static QStringList xhttpSessionKeyOptions(); + Q_INVOKABLE static QStringList xhttpSeqPlacementOptions(); + Q_INVOKABLE static QStringList xhttpUplinkDataPlacementOptions(); + Q_INVOKABLE static QStringList xPaddingPlacementOptions(); + Q_INVOKABLE static QStringList xPaddingMethodOptions(); + + // mKCP display defaults (protocolConstants.h — must match xrayConfigurator empty-field behavior) + 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(); + public slots: void updateModel(amnezia::DockerContainer container, const amnezia::XrayProtocolConfig& protocolConfig); amnezia::XrayProtocolConfig getProtocolConfig(); diff --git a/client/ui/qml/Controls2/TextFieldWithHeaderType.qml b/client/ui/qml/Controls2/TextFieldWithHeaderType.qml index 897584303..c18755625 100644 --- a/client/ui/qml/Controls2/TextFieldWithHeaderType.qml +++ b/client/ui/qml/Controls2/TextFieldWithHeaderType.qml @@ -10,6 +10,7 @@ Item { id: root property string headerText + property string subtitleText // optional line under header (e.g. default value hint) property string headerTextDisabledColor: AmneziaStyle.color.charcoalGray property string headerTextColor: AmneziaStyle.color.mutedGray @@ -84,6 +85,15 @@ Item { Layout.fillWidth: true } + SmallTextType { + text: root.subtitleText + visible: root.subtitleText !== "" + color: AmneziaStyle.color.charcoalGray + font.pixelSize: 13 + Layout.fillWidth: true + Layout.topMargin: visible ? 2 : 0 + } + TextField { id: textField diff --git a/client/ui/qml/Pages2/PageProtocolXrayFlowSettings.qml b/client/ui/qml/Pages2/PageProtocolXrayFlowSettings.qml index 6617b239f..53ff9be53 100644 --- a/client/ui/qml/Pages2/PageProtocolXrayFlowSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolXrayFlowSettings.qml @@ -3,6 +3,7 @@ import QtQuick.Controls import QtQuick.Layouts import PageEnum 1.0 +import ProtocolEnum 1.0 import Style 1.0 import "./" @@ -25,10 +26,11 @@ PageType { ListViewType { id: listView anchors.top: backButton.bottom - anchors.bottom: parent.bottom + anchors.bottom: saveButton.top anchors.left: parent.left anchors.right: parent.right + enabled: ServersUiController.isProcessedServerHasWriteAccess() model: XrayConfigModel delegate: ColumnLayout { @@ -86,20 +88,37 @@ PageType { } } - // BasicButtonType { - // id: saveButton - // anchors.bottom: parent.bottom - // anchors.left: parent.left - // anchors.right: parent.right - // anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin - // anchors.leftMargin: 16 - // anchors.rightMargin: 16 - // text: qsTr("Save") - // onClicked: { - // forceActiveFocus() - // PageController.closePage() - // } - // Keys.onEnterPressed: clicked() - // Keys.onReturnPressed: clicked() - // } + BasicButtonType { + id: saveButton + + anchors.left: root.left + anchors.right: root.right + anchors.bottom: root.bottom + anchors.leftMargin: 16 + anchors.rightMargin: 16 + anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin + + enabled: listView.enabled + 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 yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") + var yesButtonFunction = function () { + if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ServersUiController.processedContainerIndex) { + PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection")) + return + } + PageController.goToPage(PageEnum.PageSetupWizardInstalling) + InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.Xray) + } + var noButtonFunction = function () { + if (typeof GC !== "undefined" && !GC.isMobile()) { + saveButton.forceActiveFocus() + } + } + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) + } + } } diff --git a/client/ui/qml/Pages2/PageProtocolXraySecuritySettings.qml b/client/ui/qml/Pages2/PageProtocolXraySecuritySettings.qml index a1dda29e4..0ef419d50 100644 --- a/client/ui/qml/Pages2/PageProtocolXraySecuritySettings.qml +++ b/client/ui/qml/Pages2/PageProtocolXraySecuritySettings.qml @@ -3,6 +3,7 @@ import QtQuick.Controls import QtQuick.Layouts import PageEnum 1.0 +import ProtocolEnum 1.0 import Style 1.0 import "./" @@ -25,10 +26,11 @@ PageType { ListViewType { id: listView anchors.top: backButton.bottom - anchors.bottom: parent.bottom + anchors.bottom: saveButton.top anchors.left: parent.left anchors.right: parent.right + enabled: ServersUiController.isProcessedServerHasWriteAccess() model: XrayConfigModel delegate: ColumnLayout { @@ -99,14 +101,11 @@ PageType { listView: ListViewWithRadioButtonType { rootWidth: root.width model: ListModel { - ListElement { - name: "HTTP/2" - } - ListElement { - name: "HTTP/1.1" - } - ListElement { - name: "HTTP/2,HTTP/1.1" + Component.onCompleted: { + var opts = XrayConfigModel.alpnOptions() + for (var i = 0; i < opts.length; i++) { + append({name: opts[i]}) + } } } clickedFunction: function () { @@ -145,35 +144,11 @@ PageType { listView: ListViewWithRadioButtonType { rootWidth: root.width model: ListModel { - ListElement { - name: "Mozilla/5.0" - } - ListElement { - name: "chrome" - } - ListElement { - name: "firefox" - } - ListElement { - name: "safari" - } - ListElement { - name: "ios" - } - ListElement { - name: "android" - } - ListElement { - name: "edge" - } - ListElement { - name: "360" - } - ListElement { - name: "qq" - } - ListElement { - name: "random" + Component.onCompleted: { + var opts = XrayConfigModel.fingerprintOptions() + for (var i = 0; i < opts.length; i++) { + append({name: opts[i]}) + } } } clickedFunction: function () { @@ -231,35 +206,11 @@ PageType { listView: ListViewWithRadioButtonType { rootWidth: root.width model: ListModel { - ListElement { - name: "Mozilla/5.0" - } - ListElement { - name: "chrome" - } - ListElement { - name: "firefox" - } - ListElement { - name: "safari" - } - ListElement { - name: "ios" - } - ListElement { - name: "android" - } - ListElement { - name: "edge" - } - ListElement { - name: "360" - } - ListElement { - name: "qq" - } - ListElement { - name: "random" + Component.onCompleted: { + var opts = XrayConfigModel.fingerprintOptions() + for (var i = 0; i < opts.length; i++) { + append({name: opts[i]}) + } } } clickedFunction: function () { @@ -304,21 +255,37 @@ PageType { } } - // BasicButtonType { - // id: saveButton - // anchors.bottom: parent.bottom - // anchors.left: parent.left - // anchors.right: parent.right - // anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin - // anchors.leftMargin: 16 - // anchors.rightMargin: 16 - // text: qsTr("Save") - // onClicked: { - // forceActiveFocus() - // PageController.closePage() - // } - // Keys.onEnterPressed: clicked() - // Keys.onReturnPressed: clicked() - // } -} + BasicButtonType { + id: saveButton + anchors.left: root.left + anchors.right: root.right + anchors.bottom: root.bottom + anchors.leftMargin: 16 + anchors.rightMargin: 16 + anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin + + enabled: listView.enabled + 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 yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") + var yesButtonFunction = function () { + if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ServersUiController.processedContainerIndex) { + PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection")) + return + } + PageController.goToPage(PageEnum.PageSetupWizardInstalling) + InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.Xray) + } + var noButtonFunction = function () { + if (typeof GC !== "undefined" && !GC.isMobile()) { + saveButton.forceActiveFocus() + } + } + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) + } + } +} diff --git a/client/ui/qml/Pages2/PageProtocolXraySettings.qml b/client/ui/qml/Pages2/PageProtocolXraySettings.qml index 59d0a6d9f..b0188b7fb 100644 --- a/client/ui/qml/Pages2/PageProtocolXraySettings.qml +++ b/client/ui/qml/Pages2/PageProtocolXraySettings.qml @@ -58,7 +58,7 @@ PageType { Header2TextType { Layout.fillWidth: true - text: qsTr("XRay\nVLESS") + text: qsTr("XRay VLESS settings") wrapMode: Text.WordWrap } diff --git a/client/ui/qml/Pages2/PageProtocolXrayTransportSettings.qml b/client/ui/qml/Pages2/PageProtocolXrayTransportSettings.qml index 8b78eb14d..c62cd82bc 100644 --- a/client/ui/qml/Pages2/PageProtocolXrayTransportSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolXrayTransportSettings.qml @@ -3,6 +3,7 @@ import QtQuick.Controls import QtQuick.Layouts import PageEnum 1.0 +import ProtocolEnum 1.0 import Style 1.0 import "./" @@ -25,10 +26,11 @@ PageType { ListViewType { id: listView anchors.top: backButton.bottom - anchors.bottom: parent.bottom + anchors.bottom: saveButton.top anchors.left: parent.left anchors.right: parent.right + enabled: ServersUiController.isProcessedServerHasWriteAccess() model: XrayConfigModel delegate: ColumnLayout { @@ -106,6 +108,7 @@ PageType { Layout.rightMargin: 16 Layout.topMargin: 8 headerText: qsTr("TTI") + subtitleText: qsTr("Default: %1 ms", "mKCP TTI").arg(XrayConfigModel.mkcpDefaultTti()) textField.text: mkcpTti textField.onEditingFinished: { if (textField.text !== mkcpTti) mkcpTti = textField.text @@ -118,6 +121,7 @@ PageType { Layout.rightMargin: 16 Layout.topMargin: 8 headerText: qsTr("uplinkCapacity") + subtitleText: qsTr("Default: %1 Mbit/s", "mKCP uplink").arg(XrayConfigModel.mkcpDefaultUplinkCapacity()) textField.text: mkcpUplinkCapacity textField.onEditingFinished: { if (textField.text !== mkcpUplinkCapacity) mkcpUplinkCapacity = textField.text @@ -130,6 +134,7 @@ PageType { Layout.rightMargin: 16 Layout.topMargin: 8 headerText: qsTr("downlinkCapacity") + subtitleText: qsTr("Default: %1 Mbit/s", "mKCP downlink").arg(XrayConfigModel.mkcpDefaultDownlinkCapacity()) textField.text: mkcpDownlinkCapacity textField.onEditingFinished: { if (textField.text !== mkcpDownlinkCapacity) mkcpDownlinkCapacity = textField.text @@ -142,6 +147,7 @@ PageType { Layout.rightMargin: 16 Layout.topMargin: 8 headerText: qsTr("readBufferSize") + subtitleText: qsTr("Default: %1 MiB").arg(XrayConfigModel.mkcpDefaultReadBufferSize()) textField.text: mkcpReadBufferSize textField.onEditingFinished: { if (textField.text !== mkcpReadBufferSize) mkcpReadBufferSize = textField.text @@ -154,6 +160,7 @@ PageType { Layout.rightMargin: 16 Layout.topMargin: 8 headerText: qsTr("writeBufferSize") + subtitleText: qsTr("Default: %1 MiB").arg(XrayConfigModel.mkcpDefaultWriteBufferSize()) textField.text: mkcpWriteBufferSize textField.onEditingFinished: { if (textField.text !== mkcpWriteBufferSize) mkcpWriteBufferSize = textField.text @@ -178,10 +185,20 @@ PageType { Layout.fillWidth: true spacing: 0 + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 16 + Layout.bottomMargin: 8 + text: qsTr("Transport Mode") + color: AmneziaStyle.color.mutedGray + } + DropDownType { id: modeDropDown Layout.fillWidth: true - Layout.topMargin: 16 + Layout.topMargin: 0 Layout.leftMargin: 16 Layout.rightMargin: 16 text: xhttpMode @@ -191,17 +208,11 @@ PageType { listView: ListViewWithRadioButtonType { rootWidth: root.width model: ListModel { - ListElement { - name: "Auto" - } - ListElement { - name: "Packet-up" - } - ListElement { - name: "Stream-up" - } - ListElement { - name: "Stream-one" + Component.onCompleted: { + var opts = XrayConfigModel.xhttpModeOptions() + for (var i = 0; i < opts.length; i++) { + append({name: opts[i]}) + } } } clickedFunction: function () { @@ -274,11 +285,11 @@ PageType { listView: ListViewWithRadioButtonType { rootWidth: root.width model: ListModel { - ListElement { - name: "HTTP" - } - ListElement { - name: "None" + Component.onCompleted: { + var opts = XrayConfigModel.xhttpHeadersTemplateOptions() + for (var i = 0; i < opts.length; i++) { + append({name: opts[i]}) + } } } clickedFunction: function () { @@ -317,14 +328,11 @@ PageType { listView: ListViewWithRadioButtonType { rootWidth: root.width model: ListModel { - ListElement { - name: "POST" - } - ListElement { - name: "PUT" - } - ListElement { - name: "PATCH" + Component.onCompleted: { + var opts = XrayConfigModel.xhttpUplinkMethodOptions() + for (var i = 0; i < opts.length; i++) { + append({name: opts[i]}) + } } } clickedFunction: function () { @@ -399,17 +407,11 @@ PageType { listView: ListViewWithRadioButtonType { rootWidth: root.width model: ListModel { - ListElement { - name: "Path" - } - ListElement { - name: "Header" - } - ListElement { - name: "Cookie" - } - ListElement { - name: "None" + Component.onCompleted: { + var opts = XrayConfigModel.xhttpSessionPlacementOptions() + for (var i = 0; i < opts.length; i++) { + append({name: opts[i]}) + } } } clickedFunction: function () { @@ -448,14 +450,11 @@ PageType { listView: ListViewWithRadioButtonType { rootWidth: root.width model: ListModel { - ListElement { - name: "Path" - } - ListElement { - name: "Header" - } - ListElement { - name: "None" + Component.onCompleted: { + var opts = XrayConfigModel.xhttpSessionKeyOptions() + for (var i = 0; i < opts.length; i++) { + append({name: opts[i]}) + } } } clickedFunction: function () { @@ -494,17 +493,11 @@ PageType { listView: ListViewWithRadioButtonType { rootWidth: root.width model: ListModel { - ListElement { - name: "Path" - } - ListElement { - name: "Header" - } - ListElement { - name: "Cookie" - } - ListElement { - name: "None" + Component.onCompleted: { + var opts = XrayConfigModel.xhttpSeqPlacementOptions() + for (var i = 0; i < opts.length; i++) { + append({name: opts[i]}) + } } } clickedFunction: function () { @@ -555,11 +548,11 @@ PageType { listView: ListViewWithRadioButtonType { rootWidth: root.width model: ListModel { - ListElement { - name: "Body" - } - ListElement { - name: "Query" + Component.onCompleted: { + var opts = XrayConfigModel.xhttpUplinkDataPlacementOptions() + for (var i = 0; i < opts.length; i++) { + append({name: opts[i]}) + } } } clickedFunction: function () { @@ -654,25 +647,6 @@ PageType { onMaxChanged: xhttpScMaxEachPostBytesMax = val } - CaptionTextType { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.topMargin: 16 - Layout.bottomMargin: 8 - text: qsTr("scMinPostsIntervalMs") - color: AmneziaStyle.color.mutedGray - } - MinMaxRowType { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - minValue: xhttpScMinPostsIntervalMsMin - maxValue: xhttpScMinPostsIntervalMsMax - onMinChanged: xhttpScMinPostsIntervalMsMin = val - onMaxChanged: xhttpScMinPostsIntervalMsMax = val - } - CaptionTextType { Layout.fillWidth: true Layout.leftMargin: 16 @@ -692,6 +666,25 @@ PageType { onMaxChanged: xhttpScStreamUpServerSecsMax = val } + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 16 + Layout.bottomMargin: 8 + text: qsTr("scMinPostsIntervalMs") + color: AmneziaStyle.color.mutedGray + } + MinMaxRowType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + minValue: xhttpScMinPostsIntervalMsMin + maxValue: xhttpScMinPostsIntervalMsMax + onMinChanged: xhttpScMinPostsIntervalMsMin = val + onMaxChanged: xhttpScMinPostsIntervalMsMax = val + } + // ── Padding and multiplexing ────────────────────────── CaptionTextType { Layout.fillWidth: true @@ -735,21 +728,37 @@ PageType { } } - // BasicButtonType { - // id: saveButton - // anchors.bottom: parent.bottom - // anchors.left: parent.left - // anchors.right: parent.right - // anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin - // anchors.leftMargin: 16 - // anchors.rightMargin: 16 - // text: qsTr("Save") - // onClicked: { - // forceActiveFocus() - // PageController.closePage() - // } - // Keys.onEnterPressed: clicked() - // Keys.onReturnPressed: clicked() - // } -} + BasicButtonType { + id: saveButton + anchors.left: root.left + anchors.right: root.right + anchors.bottom: root.bottom + anchors.leftMargin: 16 + anchors.rightMargin: 16 + anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin + + enabled: listView.enabled + 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 yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") + var yesButtonFunction = function () { + if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ServersUiController.processedContainerIndex) { + PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection")) + return + } + PageController.goToPage(PageEnum.PageSetupWizardInstalling) + InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.Xray) + } + var noButtonFunction = function () { + if (typeof GC !== "undefined" && !GC.isMobile()) { + saveButton.forceActiveFocus() + } + } + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) + } + } +} diff --git a/client/ui/qml/Pages2/PageProtocolXrayXPaddingBytesSettings.qml b/client/ui/qml/Pages2/PageProtocolXrayXPaddingBytesSettings.qml index 6590ad1ed..08116eaf0 100644 --- a/client/ui/qml/Pages2/PageProtocolXrayXPaddingBytesSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolXrayXPaddingBytesSettings.qml @@ -3,6 +3,7 @@ import QtQuick.Controls import QtQuick.Layouts import PageEnum 1.0 +import ProtocolEnum 1.0 import Style 1.0 import "./" @@ -25,10 +26,11 @@ PageType { ListViewType { id: listView anchors.top: backButton.bottom - anchors.bottom: parent.bottom + anchors.bottom: saveButton.top anchors.left: parent.left anchors.right: parent.right + enabled: ServersUiController.isProcessedServerHasWriteAccess() model: XrayConfigModel delegate: ColumnLayout { @@ -69,20 +71,37 @@ PageType { } } - // BasicButtonType { - // id: saveButton - // anchors.bottom: parent.bottom - // anchors.left: parent.left - // anchors.right: parent.right - // anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin - // anchors.leftMargin: 16 - // anchors.rightMargin: 16 - // text: qsTr("Save") - // onClicked: { - // forceActiveFocus() - // PageController.closePage() - // } - // Keys.onEnterPressed: clicked() - // Keys.onReturnPressed: clicked() - // } + BasicButtonType { + id: saveButton + + anchors.left: root.left + anchors.right: root.right + anchors.bottom: root.bottom + anchors.leftMargin: 16 + anchors.rightMargin: 16 + anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin + + enabled: listView.enabled + 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 yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") + var yesButtonFunction = function () { + if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ServersUiController.processedContainerIndex) { + PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection")) + return + } + PageController.goToPage(PageEnum.PageSetupWizardInstalling) + InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.Xray) + } + var noButtonFunction = function () { + if (typeof GC !== "undefined" && !GC.isMobile()) { + saveButton.forceActiveFocus() + } + } + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) + } + } } diff --git a/client/ui/qml/Pages2/PageProtocolXrayXPaddingSettings.qml b/client/ui/qml/Pages2/PageProtocolXrayXPaddingSettings.qml index d2d0ad70d..9176eaf48 100644 --- a/client/ui/qml/Pages2/PageProtocolXrayXPaddingSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolXrayXPaddingSettings.qml @@ -3,6 +3,7 @@ import QtQuick.Controls import QtQuick.Layouts import PageEnum 1.0 +import ProtocolEnum 1.0 import Style 1.0 import "./" @@ -25,10 +26,11 @@ PageType { ListViewType { id: listView anchors.top: backButton.bottom - anchors.bottom: parent.bottom + anchors.bottom: saveButton.top anchors.left: parent.left anchors.right: parent.right + enabled: ServersUiController.isProcessedServerHasWriteAccess() model: XrayConfigModel delegate: ColumnLayout { @@ -106,17 +108,11 @@ PageType { listView: ListViewWithRadioButtonType { rootWidth: root.width model: ListModel { - ListElement { - name: "Cookie" - } - ListElement { - name: "Header" - } - ListElement { - name: "Query" - } - ListElement { - name: "Body" + Component.onCompleted: { + var opts = XrayConfigModel.xPaddingPlacementOptions() + for (var i = 0; i < opts.length; i++) { + append({name: opts[i]}) + } } } clickedFunction: function () { @@ -155,14 +151,11 @@ PageType { listView: ListViewWithRadioButtonType { rootWidth: root.width model: ListModel { - ListElement { - name: "Repeat-x" - } - ListElement { - name: "Random" - } - ListElement { - name: "Zero" + Component.onCompleted: { + var opts = XrayConfigModel.xPaddingMethodOptions() + for (var i = 0; i < opts.length; i++) { + append({name: opts[i]}) + } } } clickedFunction: function () { @@ -194,20 +187,37 @@ PageType { } } - // BasicButtonType { - // id: saveButton - // anchors.bottom: parent.bottom - // anchors.left: parent.left - // anchors.right: parent.right - // anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin - // anchors.leftMargin: 16 - // anchors.rightMargin: 16 - // text: qsTr("Save") - // onClicked: { - // forceActiveFocus() - // PageController.closePage() - // } - // Keys.onEnterPressed: clicked() - // Keys.onReturnPressed: clicked() - // } + BasicButtonType { + id: saveButton + + anchors.left: root.left + anchors.right: root.right + anchors.bottom: root.bottom + anchors.leftMargin: 16 + anchors.rightMargin: 16 + anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin + + enabled: listView.enabled + 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 yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") + var yesButtonFunction = function () { + if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ServersUiController.processedContainerIndex) { + PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection")) + return + } + PageController.goToPage(PageEnum.PageSetupWizardInstalling) + InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.Xray) + } + var noButtonFunction = function () { + if (typeof GC !== "undefined" && !GC.isMobile()) { + saveButton.forceActiveFocus() + } + } + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) + } + } } diff --git a/client/ui/qml/Pages2/PageProtocolXrayXmuxSettings.qml b/client/ui/qml/Pages2/PageProtocolXrayXmuxSettings.qml index 8376a5cc2..24793c4e0 100644 --- a/client/ui/qml/Pages2/PageProtocolXrayXmuxSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolXrayXmuxSettings.qml @@ -3,6 +3,7 @@ import QtQuick.Controls import QtQuick.Layouts import PageEnum 1.0 +import ProtocolEnum 1.0 import Style 1.0 import "./" @@ -25,10 +26,11 @@ PageType { ListViewType { id: listView anchors.top: backButton.bottom - anchors.bottom: parent.bottom + anchors.bottom: saveButton.top anchors.left: parent.left anchors.right: parent.right + enabled: ServersUiController.isProcessedServerHasWriteAccess() model: XrayConfigModel delegate: ColumnLayout { @@ -182,21 +184,38 @@ PageType { } } - // BasicButtonType { - // id: saveButton - // anchors.bottom: parent.bottom - // anchors.left: parent.left - // anchors.right: parent.right - // anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin - // anchors.leftMargin: 16 - // anchors.rightMargin: 16 - // text: qsTr("Save") - // onClicked: { - // forceActiveFocus() - // PageController.closePage() - // } - // Keys.onEnterPressed: clicked() - // Keys.onReturnPressed: clicked() - // } + BasicButtonType { + id: saveButton + + anchors.left: root.left + anchors.right: root.right + anchors.bottom: root.bottom + anchors.leftMargin: 16 + anchors.rightMargin: 16 + anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin + + enabled: listView.enabled + 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 yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") + var yesButtonFunction = function () { + if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ServersUiController.processedContainerIndex) { + PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection")) + return + } + PageController.goToPage(PageEnum.PageSetupWizardInstalling) + InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.Xray) + } + var noButtonFunction = function () { + if (typeof GC !== "undefined" && !GC.isMobile()) { + saveButton.forceActiveFocus() + } + } + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) + } + } }