fix: rework xray settings - split client/server data, unify saving, fix tls (#3006)

* Feat: split xray client and server settings

* remove test logs

* in-place save and TLS pin

* fixed xhttp/mkcp + tls

* remove check version xray in start connect

* address review findings for the settings split

* reset namespace

* reset namespace

* remove template xray

* fix: stop xray defaults from overwriting entered settings

* revert dropdown selection refactor from the defaults fix

* move save 'updateServerConfig'

* remove comment
This commit is contained in:
yp authored and GitHub committed 2026-10-08 23:58:25 +08:00
1 parent c5ecf6281f
commit 9017984b1d
35 files changed
+2695 -1286

No files matched your search

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