mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-08-04 23:06:49 +03:00
Compare commits
18 Commits
dev
...
fix/telegr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9718f4aab | ||
|
|
ea0ed764cd | ||
|
|
f72561a099 | ||
|
|
6c5bde7abe | ||
|
|
61c38f0329 | ||
|
|
2cb05eaa1c | ||
|
|
e7b873a44a | ||
|
|
4e224323e4 | ||
|
|
cc28e42741 | ||
|
|
90e41f0127 | ||
|
|
86b79c0af1 | ||
|
|
9ff9d3d5f3 | ||
|
|
308013725a | ||
|
|
dd9eae9d75 | ||
|
|
79065e1839 | ||
|
|
3b602e2747 | ||
|
|
33ab05c565 | ||
|
|
88e4f5023f |
@@ -85,6 +85,12 @@ namespace {
|
||||
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)
|
||||
{
|
||||
@@ -94,10 +100,23 @@ namespace {
|
||||
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;
|
||||
obj[QString::fromUtf8(key)] = makeRangeString(minV, maxV);
|
||||
}
|
||||
|
||||
// vision flow only valid with raw/tcp; drop it for xhttp/mkcp.
|
||||
QString effectiveClientFlow(const amnezia::XrayServerConfig &srv)
|
||||
{
|
||||
const bool rawTransport = srv.transport.isEmpty() || srv.transport == QLatin1String("raw");
|
||||
return rawTransport ? srv.flow : QString();
|
||||
}
|
||||
|
||||
// reality unsupported with mkcp (xray refuses) → fall back to none.
|
||||
QString effectiveSecurity(const amnezia::XrayServerConfig &srv)
|
||||
{
|
||||
if (srv.transport == QLatin1String("mkcp") && srv.security == QLatin1String("reality")) {
|
||||
return QStringLiteral("none");
|
||||
}
|
||||
return srv.security;
|
||||
}
|
||||
|
||||
// Desktop applies this in XrayProtocol::start(); iOS/Android pass JSON straight to libxray — same fixes here.
|
||||
@@ -197,7 +216,7 @@ QJsonObject XrayConfigurator::mergeStreamSettingsForServerInbound(const XrayServ
|
||||
{
|
||||
QJsonObject streamSettings = buildStreamSettings(srv, QString());
|
||||
|
||||
if (srv.security != QLatin1String("reality")) {
|
||||
if (effectiveSecurity(srv) != QLatin1String("reality")) {
|
||||
return streamSettings;
|
||||
}
|
||||
|
||||
@@ -244,10 +263,10 @@ ErrorCode XrayConfigurator::applyServerSettingsToRemote(const ServerCredentials
|
||||
<< "container=" << static_cast<int>(container) << "host=" << credentials.hostName
|
||||
<< "transport=" << srv.transport << "security=" << srv.security << "port=" << srv.port
|
||||
<< "appendClient=" << appendNewClient;
|
||||
const QString flowValue = srv.flow;
|
||||
const QString flowValue = effectiveClientFlow(srv);
|
||||
QString realityPublicKey;
|
||||
QString realityShortId;
|
||||
if (srv.security == QLatin1String("reality")) {
|
||||
if (effectiveSecurity(srv) == QLatin1String("reality")) {
|
||||
errorCode = readRealityKeyFiles(container, credentials, realityPublicKey, realityShortId);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Xray applyServerSettings: readRealityKeyFiles failed, error="
|
||||
@@ -363,6 +382,129 @@ ErrorCode XrayConfigurator::applyServerSettingsToRemote(const ServerCredentials
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode XrayConfigurator::readContainerKeyFile(DockerContainer container, const ServerCredentials &credentials,
|
||||
const QString &path, QString &out) const
|
||||
{
|
||||
out.clear();
|
||||
for (int attempt = 0; attempt < 3; ++attempt) {
|
||||
ErrorCode fileError = ErrorCode::NoError;
|
||||
out = QString::fromUtf8(m_sshSession->getTextFileFromContainer(container, credentials, path, fileError));
|
||||
out.replace(QLatin1Char('\n'), QString());
|
||||
out.replace(QLatin1Char('\r'), QString());
|
||||
if (fileError == ErrorCode::NoError && !out.isEmpty()) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
if (attempt < 2) {
|
||||
QThread::msleep(500);
|
||||
}
|
||||
}
|
||||
logger.error() << "Xray readContainerKeyFile: failed path=" << path;
|
||||
return ErrorCode::XrayRealityKeysReadFailed;
|
||||
}
|
||||
|
||||
ErrorCode XrayConfigurator::writeServerConfigForSetup(const ServerCredentials &credentials, DockerContainer container,
|
||||
ContainerConfig &containerConfig, const DnsSettings &dnsSettings)
|
||||
{
|
||||
Q_UNUSED(dnsSettings);
|
||||
namespace px = amnezia::protocols::xray;
|
||||
|
||||
const auto *xrayCfg = containerConfig.protocolConfig.as<XrayProtocolConfig>();
|
||||
if (!xrayCfg) {
|
||||
logger.error() << "Xray writeServerConfigForSetup: missing XrayProtocolConfig";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
const XrayServerConfig &srv = xrayCfg->serverConfig;
|
||||
if (srv.isThirdPartyConfig) {
|
||||
logger.info() << "Xray writeServerConfigForSetup: skipped (third-party/native profile)";
|
||||
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);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
const QString securityEff = effectiveSecurity(srv);
|
||||
|
||||
QString realityPrivateKey;
|
||||
QString realityPublicKey;
|
||||
QString realityShortId;
|
||||
if (securityEff == QLatin1String("reality")) {
|
||||
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(px::PrivateKeyPath), realityPrivateKey);
|
||||
if (errorCode != ErrorCode::NoError)
|
||||
return errorCode;
|
||||
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(px::PublicKeyPath), realityPublicKey);
|
||||
if (errorCode != ErrorCode::NoError)
|
||||
return errorCode;
|
||||
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(px::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);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Xray writeServerConfigForSetup: upload failed, error=" << static_cast<int>(errorCode);
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
XrayProtocolConfig updated =
|
||||
buildClientProtocolConfig(credentials, container, srv, clientId, errorCode, realityPublicKey, realityShortId);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Xray writeServerConfigForSetup: buildClientProtocolConfig failed, error="
|
||||
<< static_cast<int>(errorCode);
|
||||
return errorCode;
|
||||
}
|
||||
containerConfig.protocolConfig = updated;
|
||||
logger.info() << "Xray writeServerConfigForSetup: done, clientId=" << clientId;
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentials, DockerContainer container,
|
||||
const ContainerConfig &containerConfig,
|
||||
const DnsSettings &dnsSettings,
|
||||
@@ -389,7 +531,9 @@ XrayProtocolConfig XrayConfigurator::buildClientProtocolConfig(const ServerCrede
|
||||
QString xrayPublicKey = prefetchedRealityPublicKey;
|
||||
QString xrayShortId = prefetchedRealityShortId;
|
||||
|
||||
if (srv.security == QLatin1String("reality")) {
|
||||
const QString securityEff = effectiveSecurity(srv);
|
||||
|
||||
if (securityEff == QLatin1String("reality")) {
|
||||
if (xrayPublicKey.isEmpty() || xrayShortId.isEmpty()) {
|
||||
errorCode = readRealityKeyFiles(container, credentials, xrayPublicKey, xrayShortId);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
@@ -401,8 +545,9 @@ XrayProtocolConfig XrayConfigurator::buildClientProtocolConfig(const ServerCrede
|
||||
QJsonObject userObj;
|
||||
userObj[amnezia::protocols::xray::id] = clientId;
|
||||
userObj[amnezia::protocols::xray::encryption] = QStringLiteral("none");
|
||||
if (!srv.flow.isEmpty()) {
|
||||
userObj[amnezia::protocols::xray::flow] = srv.flow;
|
||||
const QString flowValue = effectiveClientFlow(srv);
|
||||
if (!flowValue.isEmpty()) {
|
||||
userObj[amnezia::protocols::xray::flow] = flowValue;
|
||||
}
|
||||
|
||||
QJsonObject vnextEntry;
|
||||
@@ -419,7 +564,7 @@ XrayProtocolConfig XrayConfigurator::buildClientProtocolConfig(const ServerCrede
|
||||
outbound[amnezia::protocols::xray::settings] = outboundSettings;
|
||||
|
||||
QJsonObject streamObj = buildStreamSettings(srv, clientId);
|
||||
if (srv.security == QLatin1String("reality")) {
|
||||
if (securityEff == QLatin1String("reality")) {
|
||||
QJsonObject rs = streamObj[amnezia::protocols::xray::realitySettings].toObject();
|
||||
rs[amnezia::protocols::xray::publicKey] = xrayPublicKey;
|
||||
rs[amnezia::protocols::xray::shortId] = xrayShortId;
|
||||
@@ -468,18 +613,24 @@ QJsonObject XrayConfigurator::buildStreamSettings(const XrayServerConfig &srv, c
|
||||
networkValue = QStringLiteral("kcp");
|
||||
streamSettings[px::network] = networkValue;
|
||||
|
||||
streamSettings[px::security] = srv.security;
|
||||
const QString securityEff = effectiveSecurity(srv);
|
||||
streamSettings[px::security] = securityEff;
|
||||
|
||||
if (srv.security == QLatin1String("tls")) {
|
||||
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(','))) {
|
||||
const QString t = a.trimmed();
|
||||
if (!t.isEmpty())
|
||||
alpnArray.append(t);
|
||||
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;
|
||||
@@ -488,7 +639,7 @@ QJsonObject XrayConfigurator::buildStreamSettings(const XrayServerConfig &srv, c
|
||||
streamSettings[QStringLiteral("tlsSettings")] = tlsSettings;
|
||||
}
|
||||
|
||||
if (srv.security == QLatin1String("reality")) {
|
||||
if (securityEff == QLatin1String("reality")) {
|
||||
QJsonObject realSettings;
|
||||
const QString fpEff = srv.fingerprint.isEmpty() ? QString::fromLatin1(px::defaultFingerprint) : srv.fingerprint;
|
||||
realSettings[px::fingerprint] = fpEff;
|
||||
@@ -504,13 +655,15 @@ QJsonObject XrayConfigurator::buildStreamSettings(const XrayServerConfig &srv, c
|
||||
xo[QStringLiteral("host")] = hostEff;
|
||||
if (!xhttp.path.isEmpty())
|
||||
xo[QStringLiteral("path")] = xhttp.path;
|
||||
xo[QStringLiteral("mode")] = normalizeXhttpMode(xhttp.mode);
|
||||
|
||||
if (xhttp.headersTemplate.compare(QLatin1String("HTTP"), Qt::CaseInsensitive) == 0) {
|
||||
QJsonObject headers;
|
||||
headers[QStringLiteral("Host")] = hostEff;
|
||||
xo[QStringLiteral("headers")] = headers;
|
||||
QString modeEff = normalizeXhttpMode(xhttp.mode);
|
||||
// xhttp+reality: auto/packet-up hang silently; force stream mode (xray #5635).
|
||||
if (srv.security == QLatin1String("reality")
|
||||
&& (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;
|
||||
@@ -521,27 +674,27 @@ QJsonObject XrayConfigurator::buildStreamSettings(const XrayServerConfig &srv, c
|
||||
|
||||
const QString sessPl = normalizeSessionSeqPlacement(xhttp.sessionPlacement);
|
||||
if (!sessPl.isEmpty())
|
||||
xo[QStringLiteral("sessionPlacement")] = sessPl;
|
||||
xo[QStringLiteral("sessionIDPlacement")] = sessPl;
|
||||
const QString seqPl = normalizeSessionSeqPlacement(xhttp.seqPlacement);
|
||||
if (!seqPl.isEmpty())
|
||||
xo[QStringLiteral("seqPlacement")] = seqPl;
|
||||
if (!xhttp.sessionKey.isEmpty())
|
||||
xo[QStringLiteral("sessionKey")] = xhttp.sessionKey;
|
||||
xo[QStringLiteral("sessionIDKey")] = xhttp.sessionKey;
|
||||
if (!xhttp.seqKey.isEmpty())
|
||||
xo[QStringLiteral("seqKey")] = xhttp.seqKey;
|
||||
|
||||
xo[QStringLiteral("uplinkDataPlacement")] = normalizeUplinkDataPlacement(xhttp.uplinkDataPlacement);
|
||||
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")) {
|
||||
const int v = ucs.toInt();
|
||||
QJsonObject chunkR;
|
||||
chunkR[QStringLiteral("from")] = v;
|
||||
chunkR[QStringLiteral("to")] = v;
|
||||
xo[QStringLiteral("uplinkChunkSize")] = chunkR;
|
||||
xo[QStringLiteral("uplinkChunkSize")] = ucs.toInt();
|
||||
}
|
||||
|
||||
if (!xhttp.scMaxBufferedPosts.isEmpty())
|
||||
@@ -558,17 +711,20 @@ QJsonObject XrayConfigurator::buildStreamSettings(const XrayServerConfig &srv, c
|
||||
xo[QStringLiteral("xPaddingObfsMode")] = pad.obfsMode;
|
||||
if (pad.obfsMode) {
|
||||
if (!pad.bytesMin.isEmpty() || !pad.bytesMax.isEmpty()) {
|
||||
QJsonObject br;
|
||||
const int fromV = pad.bytesMin.isEmpty() ? 1 : pad.bytesMin.toInt();
|
||||
int toV = pad.bytesMax.isEmpty() ? 256 : pad.bytesMax.toInt();
|
||||
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;
|
||||
br[QStringLiteral("from")] = fromV;
|
||||
br[QStringLiteral("to")] = toV;
|
||||
xo[QStringLiteral("xPaddingBytes")] = br;
|
||||
xo[QStringLiteral("xPaddingBytes")] = makeRangeString(QString::number(fromV), QString::number(toV));
|
||||
}
|
||||
xo[QStringLiteral("xPaddingKey")] = pad.key.isEmpty() ? QStringLiteral("x_padding") : pad.key;
|
||||
xo[QStringLiteral("xPaddingHeader")] = pad.header.isEmpty() ? QStringLiteral("X-Padding") : pad.header;
|
||||
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(
|
||||
@@ -579,12 +735,14 @@ QJsonObject XrayConfigurator::buildStreamSettings(const XrayServerConfig &srv, c
|
||||
if (xhttp.xmux.enabled) {
|
||||
QJsonObject mux;
|
||||
auto addMuxRange = [&](const char *key, const QString &a, const QString &b) {
|
||||
if (a.isEmpty() && b.isEmpty())
|
||||
// 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;
|
||||
QJsonObject r;
|
||||
r[QStringLiteral("from")] = a.isEmpty() ? 0 : a.toInt();
|
||||
r[QStringLiteral("to")] = b.isEmpty() ? 0 : b.toInt();
|
||||
mux[QString::fromUtf8(key)] = r;
|
||||
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);
|
||||
|
||||
@@ -30,7 +30,16 @@ public:
|
||||
bool appendNewClient,
|
||||
QString *outClientId = nullptr);
|
||||
|
||||
amnezia::ErrorCode writeServerConfigForSetup(const amnezia::ServerCredentials &credentials,
|
||||
amnezia::DockerContainer container,
|
||||
amnezia::ContainerConfig &containerConfig,
|
||||
const amnezia::DnsSettings &dnsSettings);
|
||||
|
||||
private:
|
||||
amnezia::ErrorCode readContainerKeyFile(amnezia::DockerContainer container,
|
||||
const amnezia::ServerCredentials &credentials,
|
||||
const QString &path, QString &out) const;
|
||||
|
||||
QString prepareServerConfig(const amnezia::ServerCredentials &credentials, amnezia::DockerContainer container, const amnezia::ContainerConfig &containerConfig,
|
||||
const amnezia::DnsSettings &dnsSettings,
|
||||
amnezia::ErrorCode &errorCode);
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <QtConcurrent>
|
||||
|
||||
#include "core/configurators/configuratorBase.h"
|
||||
#include "core/configurators/xrayConfigurator.h"
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
#include "core/utils/protocolEnum.h"
|
||||
@@ -152,6 +153,15 @@ ErrorCode InstallController::setupContainer(const ServerCredentials &credentials
|
||||
return e;
|
||||
qDebug().noquote() << "InstallController::setupContainer configureContainerWorker finished";
|
||||
|
||||
if (container == DockerContainer::Xray || container == DockerContainer::SSXray) {
|
||||
DnsSettings dnsSettings = { m_appSettingsRepository->primaryDns(), m_appSettingsRepository->secondaryDns() };
|
||||
XrayConfigurator xrayConfigurator(&sshSession);
|
||||
e = xrayConfigurator.writeServerConfigForSetup(credentials, container, config, dnsSettings);
|
||||
if (e)
|
||||
return e;
|
||||
qDebug().noquote() << "InstallController::setupContainer xray writeServerConfigForSetup finished";
|
||||
}
|
||||
|
||||
setupServerFirewall(credentials, sshSession);
|
||||
qDebug().noquote() << "InstallController::setupContainer setupServerFirewall finished";
|
||||
|
||||
@@ -191,6 +201,9 @@ ErrorCode InstallController::updateServerConfig(const QString &serverId, DockerC
|
||||
SshSession sshSession;
|
||||
|
||||
bool reinstallRequired = isReinstallContainerRequired(container, oldConfig, newConfig);
|
||||
if (container == DockerContainer::Xray || container == DockerContainer::SSXray) {
|
||||
reinstallRequired = true;
|
||||
}
|
||||
qDebug() << "InstallController::updateServerConfig for container" << container << "reinstall required is" << reinstallRequired;
|
||||
|
||||
ErrorCode errorCode = ErrorCode::NoError;
|
||||
@@ -397,6 +410,11 @@ ErrorCode InstallController::prepareContainerConfig(DockerContainer container, c
|
||||
}
|
||||
|
||||
if (ContainerUtils::containerService(container) != ServiceType::Other) {
|
||||
if ((container == DockerContainer::Xray || container == DockerContainer::SSXray)
|
||||
&& containerConfig.protocolConfig.hasClientConfig()) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
Proto protocol = ContainerUtils::defaultProtocol(container);
|
||||
|
||||
DnsSettings dnsSettings = {
|
||||
@@ -484,6 +502,13 @@ ErrorCode InstallController::buildContainerWorker(const ServerCredentials &crede
|
||||
if (stdOut.contains("have reached") && stdOut.contains("pull rate limit"))
|
||||
return ErrorCode::DockerPullRateLimit;
|
||||
|
||||
if (stdOut.contains("returned a non-zero code")
|
||||
|| stdOut.contains("failed to solve")
|
||||
|| stdOut.contains("Unable to find image")
|
||||
|| stdOut.contains("Couldn't connect to server")
|
||||
|| (stdOut.contains("curl:") && stdOut.contains("(")))
|
||||
return ErrorCode::ServerDockerFailedError;
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
@@ -508,6 +533,27 @@ ErrorCode InstallController::runContainerWorker(const ServerCredentials &credent
|
||||
return ErrorCode::ServerPortAlreadyAllocatedError;
|
||||
if (stdOut.contains("invalid publish"))
|
||||
return ErrorCode::ServerDockerFailedError;
|
||||
if (stdOut.contains("Unable to find image") || stdOut.contains("No such image"))
|
||||
return ErrorCode::ServerDockerFailedError;
|
||||
|
||||
if (e != ErrorCode::NoError)
|
||||
return e;
|
||||
|
||||
const QString containerName = ContainerUtils::containerToString(container);
|
||||
QString stateOut;
|
||||
auto cbState = [&stateOut](const QString &data, libssh::Client &) {
|
||||
stateOut += data;
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
sshSession.runScript(credentials,
|
||||
QStringLiteral("sudo docker inspect --format '{{.State.Running}}' %1 2>/dev/null || echo notfound")
|
||||
.arg(containerName),
|
||||
cbState);
|
||||
if (!stateOut.contains("true")) {
|
||||
qWarning().noquote() << "runContainerWorker: container" << containerName
|
||||
<< "is not running after start:" << stateOut.trimmed();
|
||||
return ErrorCode::ServerDockerFailedError;
|
||||
}
|
||||
|
||||
return e;
|
||||
}
|
||||
|
||||
@@ -76,8 +76,16 @@ ContainerConfig InstallerBase::createBaseConfig(DockerContainer container, int p
|
||||
case Proto::Xray:
|
||||
case Proto::SSXray: {
|
||||
XrayProtocolConfig xrayConfig;
|
||||
xrayConfig.serverConfig.port = portStr;
|
||||
xrayConfig.serverConfig.transportProto = transportProtoStr;
|
||||
XrayServerConfig &srv = xrayConfig.serverConfig;
|
||||
srv.port = portStr;
|
||||
srv.transportProto = transportProtoStr;
|
||||
srv.transport = protocols::xray::defaultTransport;
|
||||
srv.security = protocols::xray::defaultSecurity;
|
||||
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;
|
||||
config.protocolConfig = xrayConfig;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
#include "core/utils/protocolEnum.h"
|
||||
#include "core/utils/selfhosted/sshSession.h"
|
||||
#include "core/models/containerConfig.h"
|
||||
@@ -20,6 +21,8 @@ namespace {
|
||||
constexpr QLatin1String kMtProxyClientJsonPath("/data/amnezia-mtproxy-client.json");
|
||||
constexpr QLatin1String kMtProxyClientJsonUploadPath("data/amnezia-mtproxy-client.json");
|
||||
constexpr QLatin1String kMtProxySecretPath("/data/secret");
|
||||
constexpr QLatin1String kMtProxyMetaPath("/data/mtproxy-meta");
|
||||
constexpr QLatin1String kMtProxyStartScriptPath("/opt/amnezia/start.sh");
|
||||
}
|
||||
|
||||
MtProxyInstaller::MtProxyInstaller(QObject *parent)
|
||||
@@ -64,6 +67,52 @@ ErrorCode MtProxyInstaller::extractConfigFromContainer(DockerContainer container
|
||||
}
|
||||
}
|
||||
|
||||
// Transport mode + FakeTLS domain are otherwise unrecoverable from the server (only the raw secret is
|
||||
// stored). Restore from /data/mtproxy-meta (new installs); fall back to the deployed start.sh for older ones.
|
||||
bool modeRestored = false;
|
||||
ErrorCode metaErr = ErrorCode::NoError;
|
||||
const QByteArray metaRaw =
|
||||
sshSession->getTextFileFromContainer(container, credentials, QString(kMtProxyMetaPath), metaErr);
|
||||
if (metaErr == ErrorCode::NoError && !metaRaw.trimmed().isEmpty()) {
|
||||
QString mode, domain;
|
||||
const QList<QByteArray> lines = metaRaw.split('\n');
|
||||
for (const QByteArray &rawLine : lines) {
|
||||
const QString line = QString::fromUtf8(rawLine).trimmed();
|
||||
if (line.startsWith(QLatin1String("mode="))) {
|
||||
mode = line.mid(5).trimmed();
|
||||
} else if (line.startsWith(QLatin1String("domain="))) {
|
||||
domain = line.mid(7).trimmed();
|
||||
}
|
||||
}
|
||||
if (!mode.isEmpty()) {
|
||||
mt->transportMode = mode;
|
||||
if (mode == QLatin1String(protocols::mtProxy::transportModeFakeTLS) && !domain.isEmpty()) {
|
||||
mt->tlsDomain = domain;
|
||||
}
|
||||
modeRestored = true;
|
||||
}
|
||||
}
|
||||
if (!modeRestored) {
|
||||
ErrorCode startErr = ErrorCode::NoError;
|
||||
const QByteArray startRaw =
|
||||
sshSession->getTextFileFromContainer(container, credentials, QString(kMtProxyStartScriptPath), startErr);
|
||||
if (startErr == ErrorCode::NoError && !startRaw.trimmed().isEmpty()) {
|
||||
const QString start = QString::fromUtf8(startRaw);
|
||||
static const QRegularExpression modeRe(QStringLiteral("\\[ \"(standard|faketls)\" = \"faketls\" \\]"));
|
||||
const QRegularExpressionMatch m = modeRe.match(start);
|
||||
if (m.hasMatch()) {
|
||||
mt->transportMode = m.captured(1);
|
||||
if (m.captured(1) == QLatin1String(protocols::mtProxy::transportModeFakeTLS)) {
|
||||
static const QRegularExpression domRe(QStringLiteral("--domain ([A-Za-z0-9.\\-]+)"));
|
||||
const QRegularExpressionMatch dm = domRe.match(start);
|
||||
if (dm.hasMatch()) {
|
||||
mt->tlsDomain = dm.captured(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
#include "core/utils/selfhosted/sshSession.h"
|
||||
#include "core/models/containerConfig.h"
|
||||
#include "core/models/protocols/telemtProtocolConfig.h"
|
||||
@@ -19,6 +20,7 @@ namespace {
|
||||
constexpr QLatin1String kTelemtClientJsonPath("/data/amnezia-telemt-client.json");
|
||||
constexpr QLatin1String kTelemtClientJsonUploadPath("data/amnezia-telemt-client.json");
|
||||
constexpr QLatin1String kTelemtSecretPath("/data/secret");
|
||||
constexpr QLatin1String kTelemtConfigTomlPath("/data/config.toml");
|
||||
}
|
||||
|
||||
TelemtInstaller::TelemtInstaller(QObject *parent) : InstallerBase(parent) {}
|
||||
@@ -54,10 +56,73 @@ ErrorCode TelemtInstaller::extractConfigFromContainer(DockerContainer container,
|
||||
const QByteArray secretRaw =
|
||||
sshSession->getTextFileFromContainer(container, credentials, QString(kTelemtSecretPath), secretErr);
|
||||
const QString sec = QString::fromUtf8(secretRaw).trimmed();
|
||||
if (sec.length() == 32) {
|
||||
static const QRegularExpression hex32(QStringLiteral("^[0-9a-fA-F]{32}$"));
|
||||
if (hex32.match(sec).hasMatch()) {
|
||||
tc->secret = sec;
|
||||
static const QRegularExpression hex32(QStringLiteral("^[0-9a-fA-F]{32}$"));
|
||||
if (sec.length() == 32 && hex32.match(sec).hasMatch()) {
|
||||
tc->secret = sec;
|
||||
}
|
||||
|
||||
// Authoritative recovery: /data/config.toml holds transport mode + FakeTLS domain even when the
|
||||
// client snapshot is absent (server re-added). It wins for mode/domain; other fields fill if empty.
|
||||
ErrorCode tomlErr = ErrorCode::NoError;
|
||||
const QByteArray tomlRaw =
|
||||
sshSession->getTextFileFromContainer(container, credentials, QString(kTelemtConfigTomlPath), tomlErr);
|
||||
if (tomlErr == ErrorCode::NoError && !tomlRaw.trimmed().isEmpty()) {
|
||||
QString section;
|
||||
bool userNameSet = false;
|
||||
const QList<QByteArray> lines = tomlRaw.split('\n');
|
||||
for (const QByteArray &rawLine : lines) {
|
||||
const QString line = QString::fromUtf8(rawLine).trimmed();
|
||||
if (line.isEmpty() || line.startsWith('#')) {
|
||||
continue;
|
||||
}
|
||||
if (line.startsWith('[')) {
|
||||
section = line;
|
||||
continue;
|
||||
}
|
||||
const int eq = line.indexOf('=');
|
||||
if (eq < 0) {
|
||||
continue;
|
||||
}
|
||||
const QString key = line.left(eq).trimmed();
|
||||
QString val = line.mid(eq + 1).trimmed();
|
||||
if (val.length() >= 2 && val.startsWith('"') && val.endsWith('"')) {
|
||||
val = val.mid(1, val.length() - 2);
|
||||
}
|
||||
|
||||
if (section == QLatin1String("[access.users]")) {
|
||||
if (key.startsWith(QLatin1String("extra"))) {
|
||||
if (hex32.match(val).hasMatch() && !tc->additionalSecrets.contains(val)) {
|
||||
tc->additionalSecrets.append(val);
|
||||
}
|
||||
} else if (!userNameSet) {
|
||||
tc->userName = key;
|
||||
userNameSet = true;
|
||||
if (tc->secret.isEmpty() && hex32.match(val).hasMatch()) {
|
||||
tc->secret = val;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (key == QLatin1String("tls")) {
|
||||
tc->transportMode = (val == QLatin1String("true"))
|
||||
? QString::fromUtf8(protocols::telemt::transportModeFakeTLS)
|
||||
: QString::fromUtf8(protocols::telemt::transportModeStandard);
|
||||
} else if (key == QLatin1String("tls_domain")) {
|
||||
tc->tlsDomain = val;
|
||||
} else if (key == QLatin1String("mask")) {
|
||||
tc->maskEnabled = (val == QLatin1String("true"));
|
||||
} else if (key == QLatin1String("tls_emulation")) {
|
||||
tc->tlsEmulation = (val == QLatin1String("true"));
|
||||
} else if (key == QLatin1String("use_middle_proxy")) {
|
||||
tc->useMiddleProxy = (val == QLatin1String("true"));
|
||||
} else if (key == QLatin1String("ad_tag") && tc->tag.isEmpty()) {
|
||||
tc->tag = val;
|
||||
} else if (key == QLatin1String("public_host") && tc->publicHost.isEmpty()) {
|
||||
tc->publicHost = val;
|
||||
} else if (key == QLatin1String("port") && section == QLatin1String("[server]") && tc->port.isEmpty()) {
|
||||
tc->port = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,31 @@ namespace
|
||||
}
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
using namespace amnezia;
|
||||
@@ -125,7 +150,13 @@ ErrorCode XrayInstaller::extractConfigFromContainer(DockerContainer container, c
|
||||
QJsonArray alpnArr = tls.value("alpn").toArray();
|
||||
QStringList alpnList;
|
||||
for (const QJsonValue &v : alpnArr) {
|
||||
alpnList << v.toString();
|
||||
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(",");
|
||||
}
|
||||
@@ -184,7 +215,9 @@ ErrorCode XrayInstaller::extractConfigFromContainer(DockerContainer container, c
|
||||
return QStringLiteral("Query");
|
||||
return core;
|
||||
};
|
||||
QString sess = xhttpObj.value("sessionPlacement").toString();
|
||||
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);
|
||||
@@ -210,14 +243,17 @@ ErrorCode XrayInstaller::extractConfigFromContainer(DockerContainer container, c
|
||||
udata = xhttpObj.value("scUplinkDataPlacement").toString();
|
||||
srv.xhttp.uplinkDataPlacement = uplinkDataUi(udata);
|
||||
|
||||
srv.xhttp.sessionKey = xhttpObj.value("sessionKey").toString();
|
||||
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"))) {
|
||||
QJsonObject uc = xhttpObj.value("uplinkChunkSize").toObject();
|
||||
if (!uc.isEmpty())
|
||||
srv.xhttp.uplinkChunkSize = QString::number(uc.value("from").toInt());
|
||||
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());
|
||||
}
|
||||
@@ -226,11 +262,7 @@ ErrorCode XrayInstaller::extractConfigFromContainer(DockerContainer container, c
|
||||
}
|
||||
|
||||
auto readRange = [&](const char *key, QString &minOut, QString &maxOut) {
|
||||
QJsonObject r = xhttpObj.value(QLatin1String(key)).toObject();
|
||||
if (!r.isEmpty()) {
|
||||
minOut = QString::number(r.value("from").toInt());
|
||||
maxOut = QString::number(r.value("to").toInt());
|
||||
}
|
||||
parseIntRange(xhttpObj.value(QLatin1String(key)), minOut, maxOut);
|
||||
};
|
||||
readRange("scMaxEachPostBytes", srv.xhttp.scMaxEachPostBytesMin, srv.xhttp.scMaxEachPostBytesMax);
|
||||
readRange("scMinPostsIntervalMs", srv.xhttp.scMinPostsIntervalMsMin, srv.xhttp.scMinPostsIntervalMsMax);
|
||||
@@ -243,10 +275,11 @@ ErrorCode XrayInstaller::extractConfigFromContainer(DockerContainer container, c
|
||||
srv.xhttp.xPadding.header = pad.value("xPaddingHeader").toString();
|
||||
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 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"))
|
||||
@@ -264,7 +297,7 @@ ErrorCode XrayInstaller::extractConfigFromContainer(DockerContainer container, c
|
||||
srv.xhttp.xPadding.method = QStringLiteral("Tokenish");
|
||||
};
|
||||
if (xhttpObj.contains(QLatin1String("xPaddingObfsMode")) || xhttpObj.contains(QLatin1String("xPaddingKey"))
|
||||
|| !xhttpObj.value("xPaddingBytes").toObject().isEmpty()) {
|
||||
|| xhttpObj.contains(QLatin1String("xPaddingBytes"))) {
|
||||
loadPaddingFromObject(xhttpObj);
|
||||
} else if (xhttpObj.contains(QLatin1String("xPadding")) && xhttpObj.value("xPadding").isObject()) {
|
||||
const QJsonObject nested = xhttpObj.value("xPadding").toObject();
|
||||
@@ -280,11 +313,7 @@ ErrorCode XrayInstaller::extractConfigFromContainer(DockerContainer container, c
|
||||
srv.xhttp.xmux.enabled = true;
|
||||
|
||||
auto readMuxRange = [&](const char *key, QString &minOut, QString &maxOut) {
|
||||
QJsonObject r = mux.value(QLatin1String(key)).toObject();
|
||||
if (!r.isEmpty()) {
|
||||
minOut = QString::number(r.value("from").toInt());
|
||||
maxOut = QString::number(r.value("to").toInt());
|
||||
}
|
||||
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);
|
||||
|
||||
@@ -99,9 +99,6 @@ bool TelemtProtocolConfig::equalsDockerDeploymentSettings(const TelemtProtocolCo
|
||||
const auto normTransport = [](const QString &t) {
|
||||
return t.isEmpty() ? QString(protocols::telemt::transportModeStandard) : t;
|
||||
};
|
||||
const auto normWorkersMode = [](const QString &m) {
|
||||
return m.isEmpty() ? QString(protocols::telemt::workersModeAuto) : m;
|
||||
};
|
||||
|
||||
if (normPort(port) != normPort(other.port)) {
|
||||
return false;
|
||||
@@ -133,18 +130,9 @@ bool TelemtProtocolConfig::equalsDockerDeploymentSettings(const TelemtProtocolCo
|
||||
if (userName != other.userName) {
|
||||
return false;
|
||||
}
|
||||
if (normWorkersMode(workersMode) != normWorkersMode(other.workersMode)) {
|
||||
return false;
|
||||
}
|
||||
if (workers != other.workers) {
|
||||
return false;
|
||||
}
|
||||
if (natEnabled != other.natEnabled) {
|
||||
return false;
|
||||
}
|
||||
if (natInternalIp != other.natInternalIp) {
|
||||
return false;
|
||||
}
|
||||
if (natExternalIp != other.natExternalIp) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -65,13 +65,13 @@ namespace amnezia
|
||||
constexpr char defaultTransport[] = "raw";
|
||||
constexpr char defaultFingerprint[] = "chrome";
|
||||
constexpr char defaultSni[] = "www.googletagmanager.com";
|
||||
constexpr char defaultAlpn[] = "HTTP/2";
|
||||
constexpr char defaultAlpn[] = "h2";
|
||||
|
||||
constexpr char defaultXhttpMode[] = "Auto";
|
||||
constexpr char defaultXhttpHeadersTemplate[] = "HTTP";
|
||||
constexpr char defaultXhttpUplinkMethod[] = "POST";
|
||||
constexpr char defaultXhttpSessionPlacement[] = "Path";
|
||||
constexpr char defaultXhttpSessionKey[] = "Path";
|
||||
constexpr char defaultXhttpSessionKey[] = "";
|
||||
constexpr char defaultXhttpSeqPlacement[] = "Path";
|
||||
constexpr char defaultXhttpUplinkDataPlacement[] = "Body";
|
||||
|
||||
@@ -86,6 +86,10 @@ namespace amnezia
|
||||
|
||||
constexpr char defaultXPaddingPlacement[] = "Cookie";
|
||||
constexpr char defaultXPaddingMethod[] = "Repeat-x";
|
||||
constexpr char defaultXPaddingKey[] = "x_padding";
|
||||
constexpr char defaultXPaddingHeader[] = "X-Padding";
|
||||
constexpr char defaultXPaddingBytesMin[] = "1";
|
||||
constexpr char defaultXPaddingBytesMax[] = "256";
|
||||
|
||||
constexpr char defaultMkcpTti[] = "50";
|
||||
constexpr char defaultMkcpUplinkCapacity[] = "5";
|
||||
@@ -235,7 +239,8 @@ namespace amnezia
|
||||
|
||||
constexpr char defaultPort[] = "443";
|
||||
constexpr char defaultWorkers[] = "2";
|
||||
constexpr int maxWorkers = 32;
|
||||
// mtproto-proxy loses connectivity with -M >= 20; keep the cap at the highest known-good value.
|
||||
constexpr int maxWorkers = 19;
|
||||
constexpr int botTagHexLength = 32;
|
||||
constexpr char defaultTlsDomain[] = "googletagmanager.com";
|
||||
}
|
||||
@@ -254,7 +259,6 @@ namespace amnezia
|
||||
constexpr char tlsEmulationKey[] = "telemt_tls_emulation";
|
||||
constexpr char useMiddleProxyKey[] = "telemt_use_middle_proxy";
|
||||
constexpr char userNameKey[] = "telemt_user_name";
|
||||
// Stored for UI only (Telemt server ignores these; same controls as MTProxy page)
|
||||
constexpr char additionalSecretsKey[] = "telemt_additional_secrets";
|
||||
constexpr char workersKey[] = "telemt_workers";
|
||||
constexpr char workersModeKey[] = "telemt_workers_mode";
|
||||
|
||||
@@ -24,6 +24,13 @@ QList<QString> qrCodeUtils::generateQrCodeImageSeries(const QByteArray &data)
|
||||
return chunks;
|
||||
}
|
||||
|
||||
QString qrCodeUtils::generatePlainQrCodeImage(const QByteArray &data)
|
||||
{
|
||||
qrcodegen::QrCode qr = qrcodegen::QrCode::encodeText(data, qrcodegen::QrCode::Ecc::LOW);
|
||||
QString svg = QString::fromStdString(toSvgString(qr, 1));
|
||||
return svgToBase64(svg);
|
||||
}
|
||||
|
||||
QString qrCodeUtils::svgToBase64(const QString &image)
|
||||
{
|
||||
return "data:image/svg;base64," + QString::fromLatin1(image.toUtf8().toBase64().data());
|
||||
|
||||
@@ -10,6 +10,7 @@ namespace qrCodeUtils
|
||||
constexpr const qint16 qrMagicCode = 1984;
|
||||
|
||||
QList<QString> generateQrCodeImageSeries(const QByteArray &data);
|
||||
QString generatePlainQrCodeImage(const QByteArray &data);
|
||||
qrcodegen::QrCode generateQrCode(const QByteArray &data);
|
||||
QString svgToBase64(const QString &image);
|
||||
};
|
||||
|
||||
@@ -375,6 +375,12 @@ amnezia::ScriptVars amnezia::genTelemtVars(const ContainerConfig &containerConfi
|
||||
}
|
||||
}
|
||||
vars.append({ { "$TELEMT_ADDITIONAL_SECRETS", additionalList.join(QLatin1Char(',')) } });
|
||||
|
||||
QString middleProxyNatIp;
|
||||
if (c.natEnabled && !c.natExternalIp.isEmpty()) {
|
||||
middleProxyNatIp = c.natExternalIp;
|
||||
}
|
||||
vars.append({ { "$TELEMT_MIDDLE_PROXY_NAT_IP", middleProxyNatIp } });
|
||||
}
|
||||
|
||||
return vars;
|
||||
|
||||
@@ -176,7 +176,8 @@ QByteArray SshSession::getTextFileFromContainer(DockerContainer container, const
|
||||
|
||||
errorCode = ErrorCode::NoError;
|
||||
|
||||
QString script = QStringLiteral("sudo docker exec -i %1 sh -c \"xxd -p '%2'\"").arg(ContainerUtils::containerToString(container), path);
|
||||
QString script = QStringLiteral("sudo docker exec -i %1 sh -c \"xxd -p '%2' 2>/dev/null || od -An -v -tx1 '%2'\"")
|
||||
.arg(ContainerUtils::containerToString(container), path);
|
||||
|
||||
QString stdOut;
|
||||
auto cbReadStdOut = [&](const QString &data, libssh::Client &) {
|
||||
|
||||
@@ -47,6 +47,9 @@ else
|
||||
FAKETLS_SECRET=""
|
||||
fi
|
||||
|
||||
# Persist mode + domain so a re-added server can restore FakeTLS on scan (secret alone is not enough).
|
||||
printf 'mode=%s\ndomain=%s\n' "$TRANSPORT_MODE" "$MTPROXY_TLS_DOMAIN" > /data/mtproxy-meta
|
||||
|
||||
# Active link secret depends on transport mode
|
||||
if [ "$TRANSPORT_MODE" = "faketls" ] && [ -n "$FAKETLS_SECRET" ]; then
|
||||
LINK_SECRET="$FAKETLS_SECRET"
|
||||
|
||||
@@ -28,6 +28,9 @@ rm -f /data/config.toml
|
||||
if [ -n "$TELEMT_TAG" ]; then
|
||||
echo "ad_tag = \"$TELEMT_TAG\""
|
||||
fi
|
||||
if [ -n "$TELEMT_MIDDLE_PROXY_NAT_IP" ]; then
|
||||
echo "middle_proxy_nat_ip = \"$TELEMT_MIDDLE_PROXY_NAT_IP\""
|
||||
fi
|
||||
echo ""
|
||||
echo "[general.modes]"
|
||||
echo "classic = false"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
FROM alpine:3.15
|
||||
LABEL maintainer="AmneziaVPN"
|
||||
|
||||
ARG XRAY_RELEASE="v25.8.3"
|
||||
ARG XRAY_RELEASE="v26.7.28"
|
||||
|
||||
RUN apk add --no-cache curl unzip bash openssl netcat-openbsd dumb-init rng-tools xz
|
||||
RUN apk --update upgrade --no-cache
|
||||
|
||||
@@ -2,17 +2,11 @@ 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
|
||||
|
||||
# Parse x25519 keypair by label (v26.7 output has an extra Hash32 line; line-index parsing breaks).
|
||||
KEYPAIR=$(xray x25519)
|
||||
LINE_NUM=1
|
||||
while IFS= read -r line; do
|
||||
if [[ $LINE_NUM -gt 1 ]]
|
||||
then
|
||||
IFS=":" read FIST XRAY_PUBLIC_KEY <<< "$line"
|
||||
else
|
||||
LINE_NUM=$((LINE_NUM + 1))
|
||||
IFS=":" read FIST XRAY_PRIVATE_KEY <<< "$line"
|
||||
fi
|
||||
done <<< "$KEYPAIR"
|
||||
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)
|
||||
[ -z "$XRAY_PUBLIC_KEY" ] && XRAY_PUBLIC_KEY=$(printf '%s\n' "$KEYPAIR" | sed -n 's/.*[Pp]ublic[ ]*[Kk]ey:[[:space:]]*//p' | head -1)
|
||||
|
||||
XRAY_PRIVATE_KEY=$(echo $XRAY_PRIVATE_KEY | tr -d ' ')
|
||||
XRAY_PUBLIC_KEY=$(echo $XRAY_PUBLIC_KEY | tr -d ' ')
|
||||
@@ -21,47 +15,4 @@ 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
|
||||
|
||||
|
||||
cat > /opt/amnezia/xray/server.json <<EOF
|
||||
{
|
||||
"log": {
|
||||
"loglevel": "error"
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"port": $XRAY_SERVER_PORT,
|
||||
"protocol": "vless",
|
||||
"settings": {
|
||||
"clients": [
|
||||
{
|
||||
"id": "$XRAY_CLIENT_ID",
|
||||
"flow": "xtls-rprx-vision"
|
||||
}
|
||||
],
|
||||
"decryption": "none"
|
||||
},
|
||||
"streamSettings": {
|
||||
"network": "tcp",
|
||||
"security": "reality",
|
||||
"realitySettings": {
|
||||
"dest": "$XRAY_SITE_NAME:443",
|
||||
"serverNames": [
|
||||
"$XRAY_SITE_NAME"
|
||||
],
|
||||
"privateKey": "$XRAY_PRIVATE_KEY",
|
||||
"shortIds": [
|
||||
"$XRAY_SHORT_ID"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"protocol": "freedom"
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
|
||||
|
||||
# server.json is written by the client (writeServerConfigForSetup); this script only makes keys.
|
||||
|
||||
@@ -5,6 +5,7 @@ sudo docker run -d \
|
||||
--restart always \
|
||||
--cap-add=NET_ADMIN \
|
||||
-p $XRAY_SERVER_PORT:$XRAY_SERVER_PORT/tcp \
|
||||
-p $XRAY_SERVER_PORT:$XRAY_SERVER_PORT/udp \
|
||||
--name $CONTAINER_NAME $CONTAINER_NAME
|
||||
|
||||
sudo docker network connect amnezia-dns-net $CONTAINER_NAME
|
||||
|
||||
@@ -69,6 +69,14 @@ void ExportUiController::generateQrFromString(const QString &text)
|
||||
emit exportConfigChanged();
|
||||
}
|
||||
|
||||
void ExportUiController::generateQrFromStringRaw(const QString &text)
|
||||
{
|
||||
clearPreviousConfig();
|
||||
m_config = text;
|
||||
m_qrCodes = { qrCodeUtils::generatePlainQrCodeImage(text.toUtf8()) };
|
||||
emit exportConfigChanged();
|
||||
}
|
||||
|
||||
QString ExportUiController::getConfig()
|
||||
{
|
||||
return m_config;
|
||||
|
||||
@@ -26,6 +26,7 @@ public slots:
|
||||
void generateAwgConfig(const QString &serverId, int containerIndex, const QString &clientName);
|
||||
void generateXrayConfig(const QString &serverId, const QString &clientName);
|
||||
void generateQrFromString(const QString &text);
|
||||
void generateQrFromStringRaw(const QString &text);
|
||||
|
||||
QString getConfig();
|
||||
QString getNativeConfigString();
|
||||
|
||||
@@ -481,7 +481,6 @@ void XrayConfigModel::applyServerConfig(const amnezia::XrayServerConfig &serverC
|
||||
m_protocolConfig.serverConfig = serverConfig;
|
||||
// Clear client config since server settings changed
|
||||
m_protocolConfig.clearClientConfig();
|
||||
m_originalProtocolConfig = m_protocolConfig;
|
||||
endResetModel();
|
||||
|
||||
if (wasUnsavedChanges != hasUnsavedChanges()) {
|
||||
@@ -515,7 +514,7 @@ QStringList XrayConfigModel::fingerprintOptions()
|
||||
|
||||
QStringList XrayConfigModel::alpnOptions()
|
||||
{
|
||||
return { "HTTP/2", "HTTP/1.1", "HTTP/2,HTTP/1.1" };
|
||||
return { "h2", "http/1.1", "h2,http/1.1" };
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::xhttpModeOptions()
|
||||
@@ -535,17 +534,12 @@ QStringList XrayConfigModel::xhttpUplinkMethodOptions()
|
||||
|
||||
QStringList XrayConfigModel::xhttpSessionPlacementOptions()
|
||||
{
|
||||
return { "Path", "Header", "Cookie", "None" };
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::xhttpSessionKeyOptions()
|
||||
{
|
||||
return { "Path", "Header", "None" };
|
||||
return { "Path", "Header", "Cookie", "Query", "None" };
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::xhttpSeqPlacementOptions()
|
||||
{
|
||||
return { "Path", "Header", "Cookie", "None" };
|
||||
return { "Path", "Header", "Cookie", "Query", "None" };
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::xhttpUplinkDataPlacementOptions()
|
||||
@@ -590,6 +584,76 @@ QString XrayConfigModel::mkcpDefaultWriteBufferSize()
|
||||
return QString::fromLatin1(protocols::xray::defaultMkcpWriteBufferSize);
|
||||
}
|
||||
|
||||
QString XrayConfigModel::portDefault()
|
||||
{
|
||||
return QString::fromLatin1(protocols::xray::defaultPort);
|
||||
}
|
||||
|
||||
QString XrayConfigModel::sniDefault()
|
||||
{
|
||||
return QString::fromLatin1(protocols::xray::defaultSni);
|
||||
}
|
||||
|
||||
QString XrayConfigModel::xhttpHostDefault()
|
||||
{
|
||||
return QString::fromLatin1(protocols::xray::defaultXhttpHost);
|
||||
}
|
||||
|
||||
QString XrayConfigModel::xhttpUplinkChunkSizeDefault()
|
||||
{
|
||||
return QString::fromLatin1(protocols::xray::defaultXhttpUplinkChunkSize);
|
||||
}
|
||||
|
||||
QString XrayConfigModel::scMaxEachPostBytesMinDefault()
|
||||
{
|
||||
return QString::fromLatin1(protocols::xray::defaultXhttpScMaxEachPostBytesMin);
|
||||
}
|
||||
|
||||
QString XrayConfigModel::scMaxEachPostBytesMaxDefault()
|
||||
{
|
||||
return QString::fromLatin1(protocols::xray::defaultXhttpScMaxEachPostBytesMax);
|
||||
}
|
||||
|
||||
QString XrayConfigModel::scMinPostsIntervalMsMinDefault()
|
||||
{
|
||||
return QString::fromLatin1(protocols::xray::defaultXhttpScMinPostsIntervalMsMin);
|
||||
}
|
||||
|
||||
QString XrayConfigModel::scMinPostsIntervalMsMaxDefault()
|
||||
{
|
||||
return QString::fromLatin1(protocols::xray::defaultXhttpScMinPostsIntervalMsMax);
|
||||
}
|
||||
|
||||
QString XrayConfigModel::scStreamUpServerSecsMinDefault()
|
||||
{
|
||||
return QString::fromLatin1(protocols::xray::defaultXhttpScStreamUpServerSecsMin);
|
||||
}
|
||||
|
||||
QString XrayConfigModel::scStreamUpServerSecsMaxDefault()
|
||||
{
|
||||
return QString::fromLatin1(protocols::xray::defaultXhttpScStreamUpServerSecsMax);
|
||||
}
|
||||
|
||||
QString XrayConfigModel::xPaddingKeyDefault()
|
||||
{
|
||||
return QString::fromLatin1(protocols::xray::defaultXPaddingKey);
|
||||
}
|
||||
|
||||
QString XrayConfigModel::xPaddingHeaderDefault()
|
||||
{
|
||||
return QString::fromLatin1(protocols::xray::defaultXPaddingHeader);
|
||||
}
|
||||
|
||||
QString XrayConfigModel::xPaddingBytesMinDefault()
|
||||
{
|
||||
return QString::fromLatin1(protocols::xray::defaultXPaddingBytesMin);
|
||||
}
|
||||
|
||||
QString XrayConfigModel::xPaddingBytesMaxDefault()
|
||||
{
|
||||
return QString::fromLatin1(protocols::xray::defaultXPaddingBytesMax);
|
||||
}
|
||||
|
||||
namespace {
|
||||
bool isValidSingleHost(const QString &t)
|
||||
{
|
||||
|
||||
@@ -105,7 +105,6 @@ public:
|
||||
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();
|
||||
@@ -118,6 +117,21 @@ public:
|
||||
Q_INVOKABLE static QString mkcpDefaultReadBufferSize();
|
||||
Q_INVOKABLE static QString mkcpDefaultWriteBufferSize();
|
||||
|
||||
Q_INVOKABLE static QString portDefault();
|
||||
Q_INVOKABLE static QString sniDefault();
|
||||
Q_INVOKABLE static QString xhttpHostDefault();
|
||||
Q_INVOKABLE static QString xhttpUplinkChunkSizeDefault();
|
||||
Q_INVOKABLE static QString scMaxEachPostBytesMinDefault();
|
||||
Q_INVOKABLE static QString scMaxEachPostBytesMaxDefault();
|
||||
Q_INVOKABLE static QString scMinPostsIntervalMsMinDefault();
|
||||
Q_INVOKABLE static QString scMinPostsIntervalMsMaxDefault();
|
||||
Q_INVOKABLE static QString scStreamUpServerSecsMinDefault();
|
||||
Q_INVOKABLE static QString scStreamUpServerSecsMaxDefault();
|
||||
Q_INVOKABLE static QString xPaddingKeyDefault();
|
||||
Q_INVOKABLE static QString xPaddingHeaderDefault();
|
||||
Q_INVOKABLE static QString xPaddingBytesMinDefault();
|
||||
Q_INVOKABLE static QString xPaddingBytesMaxDefault();
|
||||
|
||||
Q_INVOKABLE static bool isValidHost(const QString &host);
|
||||
Q_INVOKABLE static bool isValidSni(const QString &sni);
|
||||
Q_INVOKABLE static bool isValidPath(const QString &path);
|
||||
|
||||
@@ -232,6 +232,7 @@ Popup {
|
||||
textField.placeholderTextColor: AmneziaStyle.color.mutedGray
|
||||
textField.inputMethodHints: Qt.ImhDigitsOnly | Qt.ImhNoPredictiveText
|
||||
textField.maximumLength: 6
|
||||
textField.validator: RegularExpressionValidator { regularExpression: /^[0-9]{0,6}$/ }
|
||||
textField.font.letterSpacing: 2
|
||||
|
||||
textField.onAccepted: {
|
||||
|
||||
@@ -22,6 +22,8 @@ ListViewType {
|
||||
|
||||
property int selectedIndex: 0
|
||||
|
||||
property string currentValue: ""
|
||||
|
||||
width: rootWidth
|
||||
height: root.contentItem.height
|
||||
|
||||
@@ -131,7 +133,7 @@ ListViewType {
|
||||
}
|
||||
|
||||
ButtonGroup.group: buttonGroup
|
||||
checked: root.selectedIndex === index
|
||||
checked: root.currentValue !== "" ? (name === root.currentValue) : (root.selectedIndex === index)
|
||||
|
||||
onClicked: {
|
||||
root.selectedIndex = index
|
||||
|
||||
@@ -24,6 +24,9 @@ Item {
|
||||
property int minLimit: 0
|
||||
property int maxLimit: 2147483647
|
||||
|
||||
property string minPlaceholder: ""
|
||||
property string maxPlaceholder: ""
|
||||
|
||||
property string hintText: root.minLimit > 0
|
||||
? (root.minLimit + "–" + root.maxLimit)
|
||||
: ("≤ " + root.maxLimit)
|
||||
@@ -73,6 +76,7 @@ Item {
|
||||
property string lastValid: ""
|
||||
Layout.fillWidth: true
|
||||
headerText: qsTr("Min")
|
||||
placeholderText: root.minPlaceholder
|
||||
textField.maximumLength: 10
|
||||
textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ }
|
||||
textField.onActiveFocusChanged: {
|
||||
@@ -87,10 +91,9 @@ Item {
|
||||
if (!isNaN(mx) && parseInt(v, 10) > mx)
|
||||
root.maxChanged(v)
|
||||
}
|
||||
if (v !== root.minValue)
|
||||
root.minChanged(v)
|
||||
else if (minField.textField.text !== v)
|
||||
if (minField.textField.text !== v)
|
||||
minField.textField.text = v
|
||||
root.minChanged(v)
|
||||
}
|
||||
|
||||
Binding {
|
||||
@@ -108,6 +111,7 @@ Item {
|
||||
property string lastValid: ""
|
||||
Layout.fillWidth: true
|
||||
headerText: qsTr("Max")
|
||||
placeholderText: root.maxPlaceholder
|
||||
textField.maximumLength: 10
|
||||
textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ }
|
||||
textField.onActiveFocusChanged: {
|
||||
@@ -122,10 +126,9 @@ Item {
|
||||
if (!isNaN(mn) && parseInt(v, 10) < mn)
|
||||
v = String(mn)
|
||||
}
|
||||
if (v !== root.maxValue)
|
||||
root.maxChanged(v)
|
||||
else if (maxField.textField.text !== v)
|
||||
if (maxField.textField.text !== v)
|
||||
maxField.textField.text = v
|
||||
root.maxChanged(v)
|
||||
}
|
||||
|
||||
Binding {
|
||||
|
||||
@@ -11,6 +11,7 @@ Item {
|
||||
|
||||
property string headerText
|
||||
property string subtitleText // optional line under header (e.g. default value hint)
|
||||
property string hintText // optional (i) info tooltip next to the header (e.g. range / max / default)
|
||||
property string headerTextDisabledColor: AmneziaStyle.color.charcoalGray
|
||||
property string headerTextColor: AmneziaStyle.color.mutedGray
|
||||
|
||||
@@ -23,6 +24,7 @@ Item {
|
||||
property var clickedFunc
|
||||
|
||||
property alias textField: textField
|
||||
property alias placeholderText: textField.placeholderText
|
||||
property string textFieldTextColor: AmneziaStyle.color.paleGray
|
||||
property string textFieldTextDisabledColor: AmneziaStyle.color.mutedGray
|
||||
|
||||
@@ -215,6 +217,52 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
// (i) hint: tap to reveal a tooltip with hintText
|
||||
ImageButtonType {
|
||||
id: hintButton
|
||||
visible: root.hintText !== ""
|
||||
focusPolicy: Qt.NoFocus
|
||||
hoverEnabled: true
|
||||
|
||||
image: "qrc:/images/controls/info.svg"
|
||||
imageColor: hintTooltip.opened ? AmneziaStyle.color.paleGray : AmneziaStyle.color.mutedGray
|
||||
|
||||
anchors.top: content.top
|
||||
anchors.right: content.right
|
||||
anchors.topMargin: 10
|
||||
anchors.rightMargin: 12
|
||||
|
||||
implicitWidth: 28
|
||||
implicitHeight: 28
|
||||
|
||||
onClicked: hintTooltip.opened ? hintTooltip.close() : hintTooltip.open()
|
||||
|
||||
ToolTip {
|
||||
id: hintTooltip
|
||||
parent: hintButton
|
||||
x: hintButton.width - width
|
||||
y: -height - 6
|
||||
width: Math.min(280, root.width - 24)
|
||||
delay: 0
|
||||
timeout: 8000
|
||||
closePolicy: Popup.CloseOnPressOutside | Popup.CloseOnEscape
|
||||
|
||||
contentItem: Text {
|
||||
text: root.hintText
|
||||
color: AmneziaStyle.color.paleGray
|
||||
wrapMode: Text.WordWrap
|
||||
font.pixelSize: 14
|
||||
font.family: "PT Root UI VF"
|
||||
}
|
||||
background: Rectangle {
|
||||
color: AmneziaStyle.color.slateGray
|
||||
radius: 12
|
||||
border.color: AmneziaStyle.color.charcoalGray
|
||||
border.width: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getBackgroundBorderColor(noneFocusedColor) {
|
||||
return textField.focus ? root.borderFocusedColor : noneFocusedColor
|
||||
}
|
||||
|
||||
@@ -37,6 +37,9 @@ PageType {
|
||||
width: listView.width
|
||||
spacing: 0
|
||||
|
||||
// xtls-rprx-vision requires the RAW (TCP) transport; it is invalid with XHTTP / mKCP.
|
||||
readonly property bool visionAllowed: transport === "" || transport === "raw"
|
||||
|
||||
BaseHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
@@ -63,6 +66,7 @@ PageType {
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: "xtls-rprx-vision"
|
||||
enabled: visionAllowed
|
||||
checked: flow === "xtls-rprx-vision"
|
||||
onClicked: flow = "xtls-rprx-vision"
|
||||
}
|
||||
@@ -75,6 +79,7 @@ PageType {
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: "xtls-rprx-vision-udp443"
|
||||
enabled: visionAllowed
|
||||
checked: flow === "xtls-rprx-vision-udp443"
|
||||
onClicked: flow = "xtls-rprx-vision-udp443"
|
||||
}
|
||||
@@ -82,6 +87,16 @@ PageType {
|
||||
DividerType {
|
||||
}
|
||||
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
visible: !visionAllowed
|
||||
color: AmneziaStyle.color.goldenApricot
|
||||
text: qsTr("xtls-rprx-vision is available only with the RAW (TCP) transport.")
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.preferredHeight: 16
|
||||
}
|
||||
|
||||
@@ -39,6 +39,9 @@ PageType {
|
||||
width: listView.width
|
||||
spacing: 0
|
||||
|
||||
// REALITY is not supported with the mKCP transport (xray refuses reality+mkcp).
|
||||
readonly property bool realityAllowed: transport !== "mkcp"
|
||||
|
||||
BaseHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
@@ -77,10 +80,21 @@ PageType {
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: qsTr("Reality")
|
||||
enabled: realityAllowed
|
||||
checked: security === "reality"
|
||||
onClicked: security = "reality"
|
||||
}
|
||||
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
visible: !realityAllowed
|
||||
color: AmneziaStyle.color.goldenApricot
|
||||
text: qsTr("REALITY is not supported with the mKCP transport. Use None or TLS.")
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
@@ -103,6 +117,7 @@ PageType {
|
||||
drawerParent: root
|
||||
listView: ListViewWithRadioButtonType {
|
||||
rootWidth: root.width
|
||||
currentValue: alpn
|
||||
model: ListModel {
|
||||
Component.onCompleted: {
|
||||
var opts = XrayConfigModel.alpnOptions()
|
||||
@@ -155,6 +170,7 @@ PageType {
|
||||
}
|
||||
}
|
||||
}
|
||||
currentValue: fingerprint
|
||||
clickedFunction: function () {
|
||||
fingerprint = selectedText
|
||||
tlsFingerprintDropDown.text = selectedText
|
||||
@@ -185,6 +201,7 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("Server Name (SNI)")
|
||||
placeholderText: XrayConfigModel.sniDefault()
|
||||
textField.text: sni
|
||||
textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9.*_-]*$/ }
|
||||
textField.onTextEdited: root.editDirty = (textField.text !== sni)
|
||||
@@ -225,6 +242,7 @@ PageType {
|
||||
}
|
||||
}
|
||||
}
|
||||
currentValue: fingerprint
|
||||
clickedFunction: function () {
|
||||
fingerprint = selectedText
|
||||
realityFingerprintDropDown.text = selectedText
|
||||
@@ -255,6 +273,7 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("Server Name (SNI)")
|
||||
placeholderText: XrayConfigModel.sniDefault()
|
||||
textField.text: sni
|
||||
textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9.*_-]*$/ }
|
||||
textField.onTextEdited: root.editDirty = (textField.text !== sni)
|
||||
|
||||
@@ -109,7 +109,8 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
enabled: listView.enabled
|
||||
headerText: qsTr("Port")
|
||||
subtitleText: qsTr("1–65535")
|
||||
hintText: qsTr("Valid range: 1–65535.")
|
||||
placeholderText: XrayConfigModel.portDefault()
|
||||
|
||||
Binding {
|
||||
target: textFieldWithHeaderType.textField
|
||||
@@ -247,9 +248,9 @@ PageType {
|
||||
visible: listView.enabled
|
||||
clickedFunction: function() {
|
||||
var yesButtonFunction = function() {
|
||||
XrayConfigModel.resetToDefaults()
|
||||
PageController.showNotificationMessage(
|
||||
qsTr("Settings were reset to defaults. Tap Save to apply them on the server."))
|
||||
XrayConfigModel.resetToDefaults()
|
||||
}
|
||||
showQuestionDrawer(qsTr("Reset settings?"), qsTr("All XRay settings will be restored to defaults."),
|
||||
qsTr("Reset"), qsTr("Cancel"), yesButtonFunction, function() {
|
||||
|
||||
@@ -123,7 +123,8 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("TTI")
|
||||
subtitleText: qsTr("Range 10–100, default %1 ms", "mKCP TTI").arg(XrayConfigModel.mkcpDefaultTti())
|
||||
hintText: qsTr("Transmission time interval (ms). Valid range: 10–100.")
|
||||
placeholderText: XrayConfigModel.mkcpDefaultTti()
|
||||
textField.text: mkcpTti
|
||||
textField.maximumLength: 3
|
||||
textField.validator: RegularExpressionValidator { regularExpression: /^(|\d{1,2}|100)$/ }
|
||||
@@ -142,7 +143,8 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("uplinkCapacity")
|
||||
subtitleText: qsTr("≥ 0, default %1 MB/s", "mKCP uplink").arg(XrayConfigModel.mkcpDefaultUplinkCapacity())
|
||||
hintText: qsTr("Uplink capacity (MB/s). Maximum: 2147483647.")
|
||||
placeholderText: XrayConfigModel.mkcpDefaultUplinkCapacity()
|
||||
textField.text: mkcpUplinkCapacity
|
||||
textField.maximumLength: 10
|
||||
textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ }
|
||||
@@ -161,7 +163,8 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("downlinkCapacity")
|
||||
subtitleText: qsTr("≥ 0, default %1 MB/s", "mKCP downlink").arg(XrayConfigModel.mkcpDefaultDownlinkCapacity())
|
||||
hintText: qsTr("Downlink capacity (MB/s). Maximum: 2147483647.")
|
||||
placeholderText: XrayConfigModel.mkcpDefaultDownlinkCapacity()
|
||||
textField.text: mkcpDownlinkCapacity
|
||||
textField.maximumLength: 10
|
||||
textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ }
|
||||
@@ -180,7 +183,8 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("readBufferSize")
|
||||
subtitleText: qsTr("≥ 1, default %1 MB").arg(XrayConfigModel.mkcpDefaultReadBufferSize())
|
||||
hintText: qsTr("Read buffer size (MB). Range: 1–2147483647.")
|
||||
placeholderText: XrayConfigModel.mkcpDefaultReadBufferSize()
|
||||
textField.text: mkcpReadBufferSize
|
||||
textField.maximumLength: 10
|
||||
textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ }
|
||||
@@ -199,7 +203,8 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("writeBufferSize")
|
||||
subtitleText: qsTr("≥ 1, default %1 MB").arg(XrayConfigModel.mkcpDefaultWriteBufferSize())
|
||||
hintText: qsTr("Write buffer size (MB). Range: 1–2147483647.")
|
||||
placeholderText: XrayConfigModel.mkcpDefaultWriteBufferSize()
|
||||
textField.text: mkcpWriteBufferSize
|
||||
textField.maximumLength: 10
|
||||
textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ }
|
||||
@@ -243,6 +248,7 @@ PageType {
|
||||
drawerParent: root
|
||||
listView: ListViewWithRadioButtonType {
|
||||
rootWidth: root.width
|
||||
currentValue: xhttpMode
|
||||
model: ListModel {
|
||||
Component.onCompleted: {
|
||||
var opts = XrayConfigModel.xhttpModeOptions()
|
||||
@@ -291,6 +297,7 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("Host")
|
||||
placeholderText: XrayConfigModel.xhttpHostDefault()
|
||||
textField.text: xhttpHost
|
||||
textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9._:,-]*$/ }
|
||||
textField.onTextEdited: root.editDirty = (textField.text !== xhttpHost)
|
||||
@@ -335,6 +342,7 @@ PageType {
|
||||
drawerParent: root
|
||||
listView: ListViewWithRadioButtonType {
|
||||
rootWidth: root.width
|
||||
currentValue: xhttpHeadersTemplate
|
||||
model: ListModel {
|
||||
Component.onCompleted: {
|
||||
var opts = XrayConfigModel.xhttpHeadersTemplateOptions()
|
||||
@@ -379,6 +387,7 @@ PageType {
|
||||
drawerParent: root
|
||||
listView: ListViewWithRadioButtonType {
|
||||
rootWidth: root.width
|
||||
currentValue: xhttpUplinkMethod
|
||||
model: ListModel {
|
||||
Component.onCompleted: {
|
||||
var opts = XrayConfigModel.xhttpUplinkMethodOptions()
|
||||
@@ -459,6 +468,7 @@ PageType {
|
||||
drawerParent: root
|
||||
listView: ListViewWithRadioButtonType {
|
||||
rootWidth: root.width
|
||||
currentValue: xhttpSessionPlacement
|
||||
model: ListModel {
|
||||
Component.onCompleted: {
|
||||
var opts = XrayConfigModel.xhttpSessionPlacementOptions()
|
||||
@@ -490,47 +500,20 @@ PageType {
|
||||
}
|
||||
}
|
||||
|
||||
DropDownType {
|
||||
id: sessionKeyDropDown
|
||||
fitContent: true
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: xhttpSessionKey
|
||||
descriptionText: qsTr("SessionKey")
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("SessionKey")
|
||||
drawerParent: root
|
||||
listView: ListViewWithRadioButtonType {
|
||||
rootWidth: root.width
|
||||
model: ListModel {
|
||||
Component.onCompleted: {
|
||||
var opts = XrayConfigModel.xhttpSessionKeyOptions()
|
||||
for (var i = 0; i < opts.length; i++) {
|
||||
append({name: opts[i]})
|
||||
}
|
||||
}
|
||||
}
|
||||
clickedFunction: function () {
|
||||
xhttpSessionKey = selectedText
|
||||
sessionKeyDropDown.text = selectedText
|
||||
sessionKeyDropDown.closeTriggered()
|
||||
}
|
||||
Component.onCompleted: {
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
if (model.get(i).name === xhttpSessionKey) {
|
||||
selectedIndex = i;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: XrayConfigModel
|
||||
|
||||
function onDataChanged() {
|
||||
sessionKeyDropDown.text = xhttpSessionKey
|
||||
}
|
||||
textField.text: xhttpSessionKey
|
||||
textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9_-]*$/ }
|
||||
textField.onTextEdited: root.editDirty = (textField.text !== xhttpSessionKey)
|
||||
textField.onEditingFinished: {
|
||||
var v = textField.text.trim()
|
||||
if (v !== xhttpSessionKey) xhttpSessionKey = v
|
||||
else if (textField.text !== v) textField.text = v
|
||||
root.editDirty = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -547,6 +530,7 @@ PageType {
|
||||
drawerParent: root
|
||||
listView: ListViewWithRadioButtonType {
|
||||
rootWidth: root.width
|
||||
currentValue: xhttpSeqPlacement
|
||||
model: ListModel {
|
||||
Component.onCompleted: {
|
||||
var opts = XrayConfigModel.xhttpSeqPlacementOptions()
|
||||
@@ -603,11 +587,12 @@ PageType {
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: xhttpUplinkDataPlacement
|
||||
descriptionText: qsTr("UplinkDataPlacement")
|
||||
descriptionText: qsTr("Header/Cookie apply only in Packet-up mode")
|
||||
headerText: qsTr("UplinkDataPlacement")
|
||||
drawerParent: root
|
||||
listView: ListViewWithRadioButtonType {
|
||||
rootWidth: root.width
|
||||
currentValue: xhttpUplinkDataPlacement
|
||||
model: ListModel {
|
||||
Component.onCompleted: {
|
||||
var opts = XrayConfigModel.xhttpUplinkDataPlacementOptions()
|
||||
@@ -673,7 +658,8 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("UplinkChunkSize")
|
||||
subtitleText: qsTr("≥ 0 (0 = off)")
|
||||
hintText: qsTr("Uplink chunk size in bytes. Maximum: 2147483647. 0 = off.")
|
||||
placeholderText: XrayConfigModel.xhttpUplinkChunkSizeDefault()
|
||||
textField.text: xhttpUplinkChunkSize
|
||||
textField.maximumLength: 10
|
||||
textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ }
|
||||
@@ -692,7 +678,7 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("scMaxBufferedPosts")
|
||||
subtitleText: qsTr("≥ 0")
|
||||
hintText: qsTr("Max buffered POSTs. Range: 0–2147483647.")
|
||||
textField.text: xhttpScMaxBufferedPosts
|
||||
textField.maximumLength: 10
|
||||
textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ }
|
||||
@@ -720,6 +706,8 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
minValue: xhttpScMaxEachPostBytesMin
|
||||
maxValue: xhttpScMaxEachPostBytesMax
|
||||
minPlaceholder: XrayConfigModel.scMaxEachPostBytesMinDefault()
|
||||
maxPlaceholder: XrayConfigModel.scMaxEachPostBytesMaxDefault()
|
||||
onMinChanged: function(val) { xhttpScMaxEachPostBytesMin = val; root.editDirty = false }
|
||||
onMaxChanged: function(val) { xhttpScMaxEachPostBytesMax = val; root.editDirty = false }
|
||||
onEdited: root.editDirty = true
|
||||
@@ -740,6 +728,8 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
minValue: xhttpScStreamUpServerSecsMin
|
||||
maxValue: xhttpScStreamUpServerSecsMax
|
||||
minPlaceholder: XrayConfigModel.scStreamUpServerSecsMinDefault()
|
||||
maxPlaceholder: XrayConfigModel.scStreamUpServerSecsMaxDefault()
|
||||
onMinChanged: function(val) { xhttpScStreamUpServerSecsMin = val; root.editDirty = false }
|
||||
onMaxChanged: function(val) { xhttpScStreamUpServerSecsMax = val; root.editDirty = false }
|
||||
onEdited: root.editDirty = true
|
||||
@@ -760,6 +750,8 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
minValue: xhttpScMinPostsIntervalMsMin
|
||||
maxValue: xhttpScMinPostsIntervalMsMax
|
||||
minPlaceholder: XrayConfigModel.scMinPostsIntervalMsMinDefault()
|
||||
maxPlaceholder: XrayConfigModel.scMinPostsIntervalMsMaxDefault()
|
||||
onMinChanged: function(val) { xhttpScMinPostsIntervalMsMin = val; root.editDirty = false }
|
||||
onMaxChanged: function(val) { xhttpScMinPostsIntervalMsMax = val; root.editDirty = false }
|
||||
onEdited: root.editDirty = true
|
||||
|
||||
@@ -63,6 +63,8 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
minValue: xPaddingBytesMin
|
||||
maxValue: xPaddingBytesMax
|
||||
minPlaceholder: XrayConfigModel.xPaddingBytesMinDefault()
|
||||
maxPlaceholder: XrayConfigModel.xPaddingBytesMaxDefault()
|
||||
onMinChanged: function(val) { xPaddingBytesMin = val; root.editDirty = false }
|
||||
onMaxChanged: function(val) { xPaddingBytesMax = val; root.editDirty = false }
|
||||
onEdited: root.editDirty = true
|
||||
|
||||
@@ -79,6 +79,7 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 16
|
||||
headerText: qsTr("xPaddingKey")
|
||||
placeholderText: XrayConfigModel.xPaddingKeyDefault()
|
||||
textField.text: xPaddingKey
|
||||
textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9_-]*$/ }
|
||||
textField.onTextEdited: root.editDirty = (textField.text !== xPaddingKey)
|
||||
@@ -96,6 +97,7 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("xPaddingHeader")
|
||||
placeholderText: XrayConfigModel.xPaddingHeaderDefault()
|
||||
textField.text: xPaddingHeader
|
||||
textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9_-]*$/ }
|
||||
textField.onTextEdited: root.editDirty = (textField.text !== xPaddingHeader)
|
||||
@@ -120,6 +122,7 @@ PageType {
|
||||
drawerParent: root
|
||||
listView: ListViewWithRadioButtonType {
|
||||
rootWidth: root.width
|
||||
currentValue: xPaddingPlacement
|
||||
model: ListModel {
|
||||
Component.onCompleted: {
|
||||
var opts = XrayConfigModel.xPaddingPlacementOptions()
|
||||
@@ -164,6 +167,7 @@ PageType {
|
||||
drawerParent: root
|
||||
listView: ListViewWithRadioButtonType {
|
||||
rootWidth: root.width
|
||||
currentValue: xPaddingMethod
|
||||
model: ListModel {
|
||||
Component.onCompleted: {
|
||||
var opts = XrayConfigModel.xPaddingMethodOptions()
|
||||
|
||||
@@ -93,6 +93,8 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
minValue: xmuxMaxConcurrencyMin
|
||||
maxValue: xmuxMaxConcurrencyMax
|
||||
minPlaceholder: "0"
|
||||
maxPlaceholder: "0"
|
||||
onMinChanged: function(val) { xmuxMaxConcurrencyMin = val; root.editDirty = false }
|
||||
onMaxChanged: function(val) { xmuxMaxConcurrencyMax = val; root.editDirty = false }
|
||||
onEdited: root.editDirty = true
|
||||
@@ -114,6 +116,8 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
minValue: xmuxMaxConnectionsMin
|
||||
maxValue: xmuxMaxConnectionsMax
|
||||
minPlaceholder: "0"
|
||||
maxPlaceholder: "0"
|
||||
onMinChanged: function(val) { xmuxMaxConnectionsMin = val; root.editDirty = false }
|
||||
onMaxChanged: function(val) { xmuxMaxConnectionsMax = val; root.editDirty = false }
|
||||
onEdited: root.editDirty = true
|
||||
@@ -135,6 +139,8 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
minValue: xmuxCMaxReuseTimesMin
|
||||
maxValue: xmuxCMaxReuseTimesMax
|
||||
minPlaceholder: "0"
|
||||
maxPlaceholder: "0"
|
||||
onMinChanged: function(val) { xmuxCMaxReuseTimesMin = val; root.editDirty = false }
|
||||
onMaxChanged: function(val) { xmuxCMaxReuseTimesMax = val; root.editDirty = false }
|
||||
onEdited: root.editDirty = true
|
||||
@@ -156,6 +162,8 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
minValue: xmuxHMaxRequestTimesMin
|
||||
maxValue: xmuxHMaxRequestTimesMax
|
||||
minPlaceholder: "0"
|
||||
maxPlaceholder: "0"
|
||||
onMinChanged: function(val) { xmuxHMaxRequestTimesMin = val; root.editDirty = false }
|
||||
onMaxChanged: function(val) { xmuxHMaxRequestTimesMax = val; root.editDirty = false }
|
||||
onEdited: root.editDirty = true
|
||||
@@ -177,6 +185,8 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
minValue: xmuxHMaxReusableSecsMin
|
||||
maxValue: xmuxHMaxReusableSecsMax
|
||||
minPlaceholder: "0"
|
||||
maxPlaceholder: "0"
|
||||
onMinChanged: function(val) { xmuxHMaxReusableSecsMin = val; root.editDirty = false }
|
||||
onMaxChanged: function(val) { xmuxHMaxReusableSecsMax = val; root.editDirty = false }
|
||||
onEdited: root.editDirty = true
|
||||
@@ -188,7 +198,7 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 16
|
||||
headerText: qsTr("hKeepAlivePeriod")
|
||||
subtitleText: qsTr("Integer, may be negative")
|
||||
hintText: qsTr("HTTP keep-alive period. Integer, may be negative.")
|
||||
textField.text: xmuxHKeepAlivePeriod
|
||||
textField.maximumLength: 11
|
||||
textField.validator: RegularExpressionValidator { regularExpression: /^-?\d*$/ }
|
||||
|
||||
@@ -562,22 +562,6 @@ PageType {
|
||||
font.pixelSize: 13
|
||||
}
|
||||
|
||||
ImageButtonType {
|
||||
implicitWidth: 36
|
||||
implicitHeight: 36
|
||||
hoverEnabled: true
|
||||
image: "qrc:/images/controls/qr-code.svg"
|
||||
imageColor: AmneziaStyle.color.paleGray
|
||||
visible: secret !== ""
|
||||
onClicked: {
|
||||
ExportController.generateQrFromString(tmeLink())
|
||||
PageController.goToShareConnectionPage(
|
||||
qsTr("Telegram connection link"),
|
||||
qsTr("MTProxy connection link"),
|
||||
"", "", "")
|
||||
}
|
||||
}
|
||||
|
||||
ImageButtonType {
|
||||
implicitWidth: 36
|
||||
implicitHeight: 36
|
||||
@@ -630,7 +614,7 @@ PageType {
|
||||
image: "qrc:/images/controls/qr-code.svg"
|
||||
imageColor: AmneziaStyle.color.paleGray
|
||||
onClicked: {
|
||||
ExportController.generateQrFromString(tgLink())
|
||||
ExportController.generateQrFromStringRaw(tgLink())
|
||||
PageController.goToShareConnectionPage(
|
||||
qsTr("Telegram connection link"),
|
||||
qsTr("MTProxy connection link"),
|
||||
@@ -877,7 +861,7 @@ PageType {
|
||||
}
|
||||
|
||||
function mtProxyShareQr(link) {
|
||||
ExportController.generateQrFromString(link)
|
||||
ExportController.generateQrFromStringRaw(link)
|
||||
PageController.goToShareConnectionPage(qsTr("Telegram connection link"),
|
||||
qsTr("MTProxy connection link"), "", "", "")
|
||||
}
|
||||
@@ -1462,15 +1446,6 @@ PageType {
|
||||
font.pixelSize: 13
|
||||
}
|
||||
|
||||
ImageButtonType {
|
||||
implicitWidth: 36
|
||||
implicitHeight: 36
|
||||
hoverEnabled: true
|
||||
image: "qrc:/images/controls/qr-code.svg"
|
||||
imageColor: AmneziaStyle.color.paleGray
|
||||
onClicked: settingsRoot.mtProxyShareQr(settingsRoot.mtProxyTmeLinkForAdditional(modelData))
|
||||
}
|
||||
|
||||
ImageButtonType {
|
||||
implicitWidth: 36
|
||||
implicitHeight: 36
|
||||
|
||||
@@ -562,22 +562,6 @@ PageType {
|
||||
font.pixelSize: 13
|
||||
}
|
||||
|
||||
ImageButtonType {
|
||||
implicitWidth: 36
|
||||
implicitHeight: 36
|
||||
hoverEnabled: true
|
||||
image: "qrc:/images/controls/qr-code.svg"
|
||||
imageColor: AmneziaStyle.color.paleGray
|
||||
visible: secret !== ""
|
||||
onClicked: {
|
||||
ExportController.generateQrFromString(tmeLink())
|
||||
PageController.goToShareConnectionPage(
|
||||
qsTr("Telegram connection link"),
|
||||
qsTr("Telemt connection link"),
|
||||
"", "", "")
|
||||
}
|
||||
}
|
||||
|
||||
ImageButtonType {
|
||||
implicitWidth: 36
|
||||
implicitHeight: 36
|
||||
@@ -630,7 +614,7 @@ PageType {
|
||||
image: "qrc:/images/controls/qr-code.svg"
|
||||
imageColor: AmneziaStyle.color.paleGray
|
||||
onClicked: {
|
||||
ExportController.generateQrFromString(tgLink())
|
||||
ExportController.generateQrFromStringRaw(tgLink())
|
||||
PageController.goToShareConnectionPage(
|
||||
qsTr("Telegram connection link"),
|
||||
qsTr("Telemt connection link"),
|
||||
@@ -877,7 +861,7 @@ PageType {
|
||||
}
|
||||
|
||||
function telemtShareQr(link) {
|
||||
ExportController.generateQrFromString(link)
|
||||
ExportController.generateQrFromStringRaw(link)
|
||||
PageController.goToShareConnectionPage(qsTr("Telegram connection link"),
|
||||
qsTr("Telemt connection link"), "", "", "")
|
||||
}
|
||||
@@ -1462,15 +1446,6 @@ PageType {
|
||||
font.pixelSize: 13
|
||||
}
|
||||
|
||||
ImageButtonType {
|
||||
implicitWidth: 36
|
||||
implicitHeight: 36
|
||||
hoverEnabled: true
|
||||
image: "qrc:/images/controls/qr-code.svg"
|
||||
imageColor: AmneziaStyle.color.paleGray
|
||||
onClicked: settingsRoot.telemtShareQr(settingsRoot.telemtTmeLinkForAdditional(modelData))
|
||||
}
|
||||
|
||||
ImageButtonType {
|
||||
implicitWidth: 36
|
||||
implicitHeight: 36
|
||||
@@ -1549,121 +1524,13 @@ PageType {
|
||||
Layout.bottomMargin: 8
|
||||
}
|
||||
|
||||
LabelTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.bottomMargin: 4
|
||||
text: qsTr("Worker mode")
|
||||
}
|
||||
|
||||
ButtonGroup {
|
||||
id: workerModeGroup
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.bottomMargin: 4
|
||||
spacing: 0
|
||||
visible: transportMode !== "faketls"
|
||||
|
||||
HorizontalRadioButton {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Auto")
|
||||
ButtonGroup.group: workerModeGroup
|
||||
checked: workersMode === "auto"
|
||||
onClicked: { workersMode = "auto"; TelemtConfigModel.setWorkersMode("auto") }
|
||||
}
|
||||
HorizontalRadioButton {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Manual")
|
||||
ButtonGroup.group: workerModeGroup
|
||||
checked: workersMode === "manual"
|
||||
onClicked: { workersMode = "manual"; TelemtConfigModel.setWorkersMode("manual") }
|
||||
}
|
||||
}
|
||||
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.bottomMargin: 8
|
||||
visible: transportMode === "faketls"
|
||||
text: qsTr("Workers are set to 0 automatically for FakeTLS mode.")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
font.pixelSize: 12
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
id: workersTextField
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.bottomMargin: 16
|
||||
visible: workersMode === "manual" && transportMode !== "faketls"
|
||||
headerText: qsTr("Workers count")
|
||||
textField.placeholderText: "2"
|
||||
textField.text: workers
|
||||
textField.maximumLength: 2
|
||||
textField.inputMethodHints: Qt.ImhDigitsOnly
|
||||
textField.validator: IntValidator {
|
||||
bottom: 0
|
||||
top: TelemtConfigModel.maxWorkers()
|
||||
}
|
||||
textField.onTextChanged: {
|
||||
var cur = workersTextField.textField.text
|
||||
if (cur === "") {
|
||||
return
|
||||
}
|
||||
var n = parseInt(cur, 10)
|
||||
var maxW = TelemtConfigModel.maxWorkers()
|
||||
if (isNaN(n) || n < 0) {
|
||||
n = 0
|
||||
}
|
||||
if (n > maxW) {
|
||||
n = maxW
|
||||
}
|
||||
var clamped = String(n)
|
||||
if (clamped !== cur) {
|
||||
textField.text = clamped
|
||||
textField.cursorPosition = clamped.length
|
||||
}
|
||||
}
|
||||
textField.onEditingFinished: {
|
||||
var v = workersTextField.textField.text
|
||||
if (v !== "") {
|
||||
var m = parseInt(v, 10)
|
||||
var maxW2 = TelemtConfigModel.maxWorkers()
|
||||
if (isNaN(m) || m < 0) {
|
||||
m = 0
|
||||
}
|
||||
if (m > maxW2) {
|
||||
m = maxW2
|
||||
}
|
||||
v = String(m)
|
||||
textField.text = v
|
||||
}
|
||||
if (v !== workers) {
|
||||
workers = v
|
||||
TelemtConfigModel.setWorkers(workers)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
Layout.fillWidth: true
|
||||
Layout.bottomMargin: 8
|
||||
}
|
||||
|
||||
SwitcherType {
|
||||
Layout.fillWidth: true
|
||||
Layout.rightMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.bottomMargin: 4
|
||||
text: qsTr("Server is behind NAT / Docker bridge")
|
||||
descriptionText: qsTr("Enable if your server is not directly accessible from the internet, e.g. Docker or private network")
|
||||
text: qsTr("Set public IP manually")
|
||||
descriptionText: qsTr("By default the proxy auto-detects its public IP. Enable to override it manually, e.g. when the server is behind NAT / Docker bridge")
|
||||
checked: natEnabled
|
||||
onToggled: function () {
|
||||
if (checked !== natEnabled) {
|
||||
@@ -1673,41 +1540,6 @@ PageType {
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
id: natInternalIpTextField
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.bottomMargin: 16
|
||||
visible: natEnabled
|
||||
headerText: qsTr("Internal IP")
|
||||
textField.placeholderText: "172.17.0.2"
|
||||
textField.text: natInternalIp
|
||||
textField.maximumLength: 15
|
||||
textField.validator: RegularExpressionValidator {
|
||||
regularExpression: root.natIpv4InputFormat
|
||||
}
|
||||
textField.onTextChanged: {
|
||||
if (root.natIpv4FieldShowInvalidError(textField.text)) {
|
||||
natInternalIpTextField.errorText = qsTr("Enter a valid IPv4 address")
|
||||
} else {
|
||||
natInternalIpTextField.errorText = ""
|
||||
}
|
||||
}
|
||||
textField.onEditingFinished: {
|
||||
textField.text = textField.text.replace(/^\s+|\s+$/g, '')
|
||||
if (!TelemtConfigModel.isValidOptionalIpv4(textField.text)) {
|
||||
natInternalIpTextField.errorText = qsTr("Enter a valid IPv4 address")
|
||||
return
|
||||
}
|
||||
natInternalIpTextField.errorText = ""
|
||||
if (textField.text !== natInternalIp) {
|
||||
natInternalIp = textField.text
|
||||
TelemtConfigModel.setNatInternalIp(natInternalIp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
id: natExternalIpTextField
|
||||
Layout.fillWidth: true
|
||||
@@ -1715,7 +1547,7 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
Layout.bottomMargin: 16
|
||||
visible: natEnabled
|
||||
headerText: qsTr("External IP")
|
||||
headerText: qsTr("Public IP")
|
||||
textField.placeholderText: "1.2.3.4"
|
||||
textField.text: natExternalIp
|
||||
textField.maximumLength: 15
|
||||
@@ -1912,7 +1744,6 @@ PageType {
|
||||
publicHostTextField.errorText = ""
|
||||
tagTextField.errorText = ""
|
||||
tlsDomainTextField.errorText = ""
|
||||
natInternalIpTextField.errorText = ""
|
||||
natExternalIpTextField.errorText = ""
|
||||
portTextField.errorText = ""
|
||||
|
||||
@@ -1948,13 +1779,9 @@ PageType {
|
||||
errorLines.push(bullet + tlsErr)
|
||||
}
|
||||
var natIpErr = qsTr("Enter a valid IPv4 address")
|
||||
if (!TelemtConfigModel.isValidOptionalIpv4(natInternalIpTextField.textField.text)) {
|
||||
natInternalIpTextField.errorText = natIpErr
|
||||
errorLines.push(bullet + qsTr("NAT internal IP: enter a valid IPv4 address"))
|
||||
}
|
||||
if (!TelemtConfigModel.isValidOptionalIpv4(natExternalIpTextField.textField.text)) {
|
||||
natExternalIpTextField.errorText = natIpErr
|
||||
errorLines.push(bullet + qsTr("NAT external IP: enter a valid IPv4 address"))
|
||||
errorLines.push(bullet + qsTr("Public IP: enter a valid IPv4 address"))
|
||||
}
|
||||
if (errorLines.length > 0) {
|
||||
PageController.showErrorMessage(errorLines.join("\n"))
|
||||
@@ -1969,15 +1796,7 @@ PageType {
|
||||
: tlsDomainTextField.textField.text
|
||||
TelemtConfigModel.setTlsDomain(domainValue)
|
||||
|
||||
if (transportMode === "faketls") {
|
||||
workers = "0"
|
||||
TelemtConfigModel.setWorkers("0")
|
||||
} else {
|
||||
TelemtConfigModel.setWorkersMode(workersMode)
|
||||
TelemtConfigModel.setWorkers(workers)
|
||||
}
|
||||
TelemtConfigModel.setNatEnabled(natEnabled)
|
||||
TelemtConfigModel.setNatInternalIp(natInternalIpTextField.textField.text)
|
||||
TelemtConfigModel.setNatExternalIp(natExternalIpTextField.textField.text)
|
||||
|
||||
previousPort = port
|
||||
|
||||
@@ -319,6 +319,9 @@ PageType {
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
visible: isQrCodeVisible
|
||||
&& !(pageShareConnection.isSelfHostedConfig
|
||||
&& (ExportController.config.startsWith("tg://")
|
||||
|| ExportController.config.startsWith("https://t.me")))
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: qsTr("To read the QR code in the Amnezia app, tap + in the main menu → 'QR code'")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user