fixed vless

This commit is contained in:
dranik
2026-05-20 19:38:20 +03:00
parent 4890dd1d74
commit ba2382924d
8 changed files with 492 additions and 163 deletions
+233 -154
View File
@@ -137,117 +137,284 @@ amnezia::ProtocolConfig XrayConfigurator::processConfigWithLocalSettings(const a
return protocolConfig;
}
QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentials, DockerContainer container,
const ContainerConfig &containerConfig,
const DnsSettings &dnsSettings,
ErrorCode &errorCode)
ErrorCode XrayConfigurator::uploadServerConfigJson(const ServerCredentials &credentials, DockerContainer container,
const DnsSettings &dnsSettings, const QJsonObject &serverConfig) const
{
// Generate new UUID for client
QString clientId = QUuid::createUuid().toString(QUuid::WithoutBraces);
// Get flow value from settings (default xtls-rprx-vision)
QString flowValue = "xtls-rprx-vision";
if (const auto *xrayCfg = containerConfig.protocolConfig.as<XrayProtocolConfig>()) {
if (!xrayCfg->serverConfig.flow.isEmpty()) {
flowValue = xrayCfg->serverConfig.flow;
}
}
// Get current server config
QString currentConfig = m_sshSession->getTextFileFromContainer(
container, credentials, amnezia::protocols::xray::serverConfigPath, errorCode);
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 get server config file";
return "";
logger.error() << "Failed to upload updated config";
return errorCode;
}
// Parse current config as JSON
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;
}
QJsonObject XrayConfigurator::mergeStreamSettingsForServerInbound(const XrayServerConfig &srv,
const QJsonObject &existingStreamSettings) const
{
QJsonObject streamSettings = buildStreamSettings(srv, QString());
if (srv.security != QLatin1String("reality")) {
return streamSettings;
}
const QJsonObject newRs = streamSettings[amnezia::protocols::xray::realitySettings].toObject();
QJsonObject oldRs = existingStreamSettings[amnezia::protocols::xray::realitySettings].toObject();
QJsonObject merged = oldRs.isEmpty() ? newRs : oldRs;
const QString siteEff = srv.site.isEmpty() ? QString::fromLatin1(amnezia::protocols::xray::defaultSite) : srv.site;
const QString sniEff = srv.sni.isEmpty() ? siteEff : srv.sni;
if (newRs.contains(amnezia::protocols::xray::fingerprint)) {
merged[amnezia::protocols::xray::fingerprint] = newRs[amnezia::protocols::xray::fingerprint];
}
merged[amnezia::protocols::xray::serverNames] = QJsonArray { sniEff };
if (!merged.contains(QStringLiteral("dest"))) {
merged[QStringLiteral("dest")] = siteEff + QStringLiteral(":443");
}
streamSettings[amnezia::protocols::xray::realitySettings] = merged;
return streamSettings;
}
ErrorCode XrayConfigurator::applyServerSettingsToRemote(const ServerCredentials &credentials, DockerContainer container,
ContainerConfig &containerConfig, const DnsSettings &dnsSettings,
bool appendNewClient, QString *outClientId)
{
ErrorCode errorCode = ErrorCode::NoError;
const auto *xrayCfg = containerConfig.protocolConfig.as<XrayProtocolConfig>();
if (!xrayCfg) {
logger.error() << "Xray applyServerSettings: missing XrayProtocolConfig";
return ErrorCode::InternalError;
}
const XrayServerConfig &srv = xrayCfg->serverConfig;
logger.info() << "Xray applyServerSettings: start"
<< "container=" << static_cast<int>(container) << "host=" << credentials.hostName
<< "transport=" << srv.transport << "security=" << srv.security << "port=" << srv.port
<< "appendClient=" << appendNewClient;
QString flowValue = srv.flow;
if (flowValue.isEmpty() && srv.security == QLatin1String("reality")) {
flowValue = QStringLiteral("xtls-rprx-vision");
}
QString currentConfig = m_sshSession->getTextFileFromContainer(
container, credentials, amnezia::protocols::xray::serverConfigPath, errorCode);
if (errorCode != ErrorCode::NoError) {
logger.error() << "Xray applyServerSettings: getTextFileFromContainer failed, error="
<< static_cast<int>(errorCode) << "path=" << amnezia::protocols::xray::serverConfigPath;
return errorCode;
}
logger.info() << "Xray applyServerSettings: read server config, bytes=" << currentConfig.size();
QJsonDocument doc = QJsonDocument::fromJson(currentConfig.toUtf8());
if (doc.isNull() || !doc.isObject()) {
logger.error() << "Failed to parse server config JSON";
errorCode = ErrorCode::InternalError;
return "";
return ErrorCode::InternalError;
}
QJsonObject serverConfig = doc.object();
// Validate server config structure
if (!serverConfig.contains(amnezia::protocols::xray::inbounds)) {
logger.error() << "Server config missing 'inbounds' field";
errorCode = ErrorCode::InternalError;
return "";
return ErrorCode::InternalError;
}
QJsonArray inbounds = serverConfig[amnezia::protocols::xray::inbounds].toArray();
if (inbounds.isEmpty()) {
logger.error() << "Server config has empty 'inbounds' array";
errorCode = ErrorCode::InternalError;
return "";
return ErrorCode::InternalError;
}
QJsonObject inbound = inbounds[0].toObject();
if (!inbound.contains(amnezia::protocols::xray::settings)) {
logger.error() << "Inbound missing 'settings' field";
errorCode = ErrorCode::InternalError;
return "";
return ErrorCode::InternalError;
}
const QJsonObject existingStream = inbound[amnezia::protocols::xray::streamSettings].toObject();
inbound[amnezia::protocols::xray::streamSettings] = mergeStreamSettingsForServerInbound(srv, existingStream);
if (!srv.port.isEmpty()) {
inbound[amnezia::protocols::xray::port] = srv.port.toInt();
}
QJsonObject settings = inbound[amnezia::protocols::xray::settings].toObject();
if (!settings.contains(amnezia::protocols::xray::clients)) {
logger.error() << "Settings missing 'clients' field";
errorCode = ErrorCode::InternalError;
return "";
settings[amnezia::protocols::xray::clients] = QJsonArray {};
}
QJsonArray clients = settings[amnezia::protocols::xray::clients].toArray();
QString clientId;
// Create configuration for new client
QJsonObject clientConfig {
{amnezia::protocols::xray::id, clientId},
};
clientConfig[amnezia::protocols::xray::id] = clientId;
if (!flowValue.isEmpty()) {
clientConfig[amnezia::protocols::xray::flow] = flowValue;
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;
}
clients.append(clientEntry);
} else {
if (clients.isEmpty()) {
logger.error() << "Server config has no VLESS clients";
return ErrorCode::InternalError;
}
clientId = clients[0].toObject()[amnezia::protocols::xray::id].toString();
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.append(clientConfig);
// Update config
settings[amnezia::protocols::xray::clients] = clients;
inbound[amnezia::protocols::xray::settings] = settings;
inbounds[0] = inbound;
serverConfig[amnezia::protocols::xray::inbounds] = inbounds;
// Save updated config to server
QString updatedConfig = QJsonDocument(serverConfig).toJson();
errorCode = m_sshSession->uploadTextFileToContainer(
container,
credentials,
updatedConfig,
amnezia::protocols::xray::serverConfigPath,
libssh::ScpOverwriteMode::ScpOverwriteExisting
);
errorCode = uploadServerConfigJson(credentials, container, dnsSettings, serverConfig);
if (errorCode != ErrorCode::NoError) {
logger.error() << "Failed to upload updated config";
return "";
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) {
*outClientId = clientId;
}
// Restart container
QString restartScript = QString("sudo docker restart $CONTAINER_NAME");
errorCode = m_sshSession->runScript(
credentials,
m_sshSession->replaceVars(restartScript, amnezia::genBaseVars(credentials, container, dnsSettings.primaryDns, dnsSettings.secondaryDns))
);
XrayProtocolConfig updated = buildClientProtocolConfig(credentials, container, srv, clientId, errorCode);
if (errorCode != ErrorCode::NoError) {
logger.error() << "Failed to restart container";
return "";
logger.error() << "Xray applyServerSettings: buildClientProtocolConfig failed, error="
<< static_cast<int>(errorCode);
return errorCode;
}
containerConfig.protocolConfig = updated;
logger.info() << "Xray applyServerSettings: done, clientId=" << clientId;
return ErrorCode::NoError;
}
QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentials, DockerContainer container,
const ContainerConfig &containerConfig,
const DnsSettings &dnsSettings,
ErrorCode &errorCode)
{
ContainerConfig mutableConfig = containerConfig;
QString clientId;
const ErrorCode applyError =
applyServerSettingsToRemote(credentials, container, mutableConfig, dnsSettings, true, &clientId);
errorCode = applyError;
if (applyError != ErrorCode::NoError || clientId.isEmpty()) {
return QString();
}
return clientId;
}
XrayProtocolConfig XrayConfigurator::buildClientProtocolConfig(const ServerCredentials &credentials,
DockerContainer container,
const XrayServerConfig &srv, const QString &clientId,
ErrorCode &errorCode) const
{
QString xrayPublicKey;
QString xrayShortId;
if (srv.security == QLatin1String("reality")) {
xrayPublicKey = m_sshSession->getTextFileFromContainer(container, credentials,
amnezia::protocols::xray::PublicKeyPath, errorCode);
if (errorCode != ErrorCode::NoError || xrayPublicKey.isEmpty()) {
logger.error() << "Failed to get public key";
if (errorCode == ErrorCode::NoError) {
errorCode = ErrorCode::InternalError;
}
return {};
}
xrayPublicKey.replace("\n", "");
xrayShortId = m_sshSession->getTextFileFromContainer(container, credentials,
amnezia::protocols::xray::shortidPath, errorCode);
if (errorCode != ErrorCode::NoError || xrayShortId.isEmpty()) {
logger.error() << "Failed to get short ID";
if (errorCode == ErrorCode::NoError) {
errorCode = ErrorCode::InternalError;
}
return {};
}
xrayShortId.replace("\n", "");
}
QJsonObject userObj;
userObj[amnezia::protocols::xray::id] = clientId;
userObj[amnezia::protocols::xray::encryption] = QStringLiteral("none");
if (!srv.flow.isEmpty()) {
userObj[amnezia::protocols::xray::flow] = srv.flow;
}
QJsonObject vnextEntry;
vnextEntry[amnezia::protocols::xray::address] = credentials.hostName;
vnextEntry[amnezia::protocols::xray::port] = srv.port.isEmpty() ? amnezia::protocols::xray::defaultPort : 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 (srv.security == 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;
XrayClientConfig clientConfig;
clientConfig.nativeConfig = config;
clientConfig.localPort = QString(amnezia::protocols::xray::defaultLocalProxyPort);
clientConfig.id = clientId;
protocolConfig.setClientConfig(clientConfig);
return protocolConfig;
}
QJsonObject XrayConfigurator::buildStreamSettings(const XrayServerConfig &srv, const QString &clientId) const
{
QJsonObject streamSettings;
@@ -441,93 +608,5 @@ ProtocolConfig XrayConfigurator::createConfig(const ServerCredentials &credentia
return XrayProtocolConfig{};
}
// Fetch server keys (Reality only)
QString xrayPublicKey;
QString xrayShortId;
if (srv.security == "reality") {
xrayPublicKey = m_sshSession->getTextFileFromContainer(container, credentials,
amnezia::protocols::xray::PublicKeyPath, errorCode);
if (errorCode != ErrorCode::NoError || xrayPublicKey.isEmpty()) {
logger.error() << "Failed to get public key";
if (errorCode == ErrorCode::NoError) {
errorCode = ErrorCode::InternalError;
}
return XrayProtocolConfig{};
}
xrayPublicKey.replace("\n", "");
xrayShortId = m_sshSession->getTextFileFromContainer(container, credentials,
amnezia::protocols::xray::shortidPath, errorCode);
if (errorCode != ErrorCode::NoError || xrayShortId.isEmpty()) {
logger.error() << "Failed to get short ID";
if (errorCode == ErrorCode::NoError) {
errorCode = ErrorCode::InternalError;
}
return XrayProtocolConfig{};
}
xrayShortId.replace("\n", "");
}
// Build outbound
QJsonObject userObj;
userObj[amnezia::protocols::xray::id] = xrayClientId;
userObj[amnezia::protocols::xray::encryption] = "none";
if (!srv.flow.isEmpty()) {
userObj[amnezia::protocols::xray::flow] = srv.flow;
}
QJsonObject vnextEntry;
vnextEntry[amnezia::protocols::xray::address] = credentials.hostName;
vnextEntry[amnezia::protocols::xray::port] = srv.port.toInt();
vnextEntry[amnezia::protocols::xray::users] = QJsonArray { userObj };
QJsonObject outboundSettings;
outboundSettings[amnezia::protocols::xray::vnext] = QJsonArray { vnextEntry };
QJsonObject outbound;
outbound["protocol"] = "vless";
outbound[amnezia::protocols::xray::settings] = outboundSettings;
// Build streamSettings
QJsonObject streamObj = buildStreamSettings(srv, xrayClientId);
// Inject Reality keys
if (srv.security == "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] = "";
streamObj[amnezia::protocols::xray::realitySettings] = rs;
}
outbound[amnezia::protocols::xray::streamSettings] = streamObj;
// Build full client config
QJsonObject inboundObj;
inboundObj["listen"] = amnezia::protocols::xray::defaultLocalListenAddr;
inboundObj[amnezia::protocols::xray::port] = amnezia::protocols::xray::defaultLocalProxyPort;
inboundObj["protocol"] = "socks";
inboundObj[amnezia::protocols::xray::settings] = QJsonObject { { "udp", true } };
QJsonObject clientJson;
clientJson["log"] = QJsonObject { { "loglevel", "error" } };
clientJson[amnezia::protocols::xray::inbounds] = QJsonArray { inboundObj };
clientJson[amnezia::protocols::xray::outbounds] = QJsonArray { outbound };
QString config = QString::fromUtf8(QJsonDocument(clientJson).toJson(QJsonDocument::Compact));
// Return
XrayProtocolConfig protocolConfig;
protocolConfig.serverConfig = srv;
XrayClientConfig clientConfig;
clientConfig.nativeConfig = config;
qDebug() << "config:" << config;
clientConfig.localPort = QString(amnezia::protocols::xray::defaultLocalProxyPort);
clientConfig.id = xrayClientId;
protocolConfig.setClientConfig(clientConfig);
return protocolConfig;
return buildClientProtocolConfig(credentials, container, srv, xrayClientId, errorCode);
}
@@ -23,11 +23,32 @@ public:
amnezia::ProtocolConfig processConfigWithLocalSettings(const amnezia::ConnectionSettings &settings,
amnezia::ProtocolConfig protocolConfig) override;
/// Uploads server.json inbound (port, streamSettings, client flows). Optionally appends a new VLESS client.
/// When appendNewClient is false, rebuilds admin client config in containerConfig from the first server client.
amnezia::ErrorCode applyServerSettingsToRemote(const amnezia::ServerCredentials &credentials,
amnezia::DockerContainer container,
amnezia::ContainerConfig &containerConfig,
const amnezia::DnsSettings &dnsSettings,
bool appendNewClient,
QString *outClientId = nullptr);
private:
QString prepareServerConfig(const amnezia::ServerCredentials &credentials, amnezia::DockerContainer container, const amnezia::ContainerConfig &containerConfig,
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;
QJsonObject mergeStreamSettingsForServerInbound(const amnezia::XrayServerConfig &srv,
const QJsonObject &existingStreamSettings) const;
// Builds the native xray "streamSettings" JSON object from XrayServerConfig
QJsonObject buildStreamSettings(const amnezia::XrayServerConfig &srv,
const QString &clientId) const;
@@ -20,6 +20,7 @@
#include "core/installers/sftpInstaller.h"
#include "core/installers/socks5Installer.h"
#include "core/installers/mtProxyInstaller.h"
#include "core/configurators/xrayConfigurator.h"
#include "core/installers/telemtInstaller.h"
#include "core/installers/torInstaller.h"
#include "core/installers/wireguardInstaller.h"
@@ -181,6 +182,16 @@ ErrorCode InstallController::updateContainer(const QString &serverId, DockerCont
bool reinstallRequired = isReinstallContainerRequired(container, oldConfig, newConfig);
qDebug() << "InstallController::updateContainer for container" << container << "reinstall required is" << reinstallRequired;
bool xrayServerSettingsChanged = false;
if (container == DockerContainer::Xray || container == DockerContainer::SSXray) {
const auto *oldXrayConfig = oldConfig.getXrayProtocolConfig();
const auto *newXrayConfig = newConfig.getXrayProtocolConfig();
if (oldXrayConfig && newXrayConfig) {
xrayServerSettingsChanged =
!oldXrayConfig->serverConfig.hasEqualServerSettings(newXrayConfig->serverConfig);
}
}
ErrorCode errorCode = ErrorCode::NoError;
if (reinstallRequired) {
errorCode = setupContainer(credentials, container, newConfig, true);
@@ -191,6 +202,18 @@ ErrorCode InstallController::updateContainer(const QString &serverId, DockerCont
}
}
if (errorCode == ErrorCode::NoError && xrayServerSettingsChanged) {
DnsSettings dnsSettings = { m_appSettingsRepository->primaryDns(), m_appSettingsRepository->secondaryDns() };
XrayConfigurator xrayConfigurator(&sshSession);
qDebug() << "InstallController::updateContainer applying Xray server inbound sync, reinstall="
<< reinstallRequired;
errorCode = xrayConfigurator.applyServerSettingsToRemote(credentials, container, newConfig, dnsSettings, false);
if (errorCode != ErrorCode::NoError) {
qDebug() << "InstallController::updateContainer Xray inbound sync failed, error="
<< static_cast<int>(errorCode);
}
}
if (errorCode == ErrorCode::NoError) {
if (container == DockerContainer::MtProxy) {
MtProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
@@ -638,12 +661,13 @@ bool InstallController::isReinstallContainerRequired(DockerContainer container,
}
if (container == DockerContainer::Xray || container == DockerContainer::SSXray) {
const auto* oldXrayConfig = oldConfig.getXrayProtocolConfig();
const auto* newXrayConfig = newConfig.getXrayProtocolConfig();
const auto *oldXrayConfig = oldConfig.getXrayProtocolConfig();
const auto *newXrayConfig = newConfig.getXrayProtocolConfig();
if (oldXrayConfig && newXrayConfig) {
if (oldXrayConfig->serverConfig.port != newXrayConfig->serverConfig.port)
if (!oldXrayConfig->serverConfig.hasEqualServerSettings(newXrayConfig->serverConfig)) {
return true;
}
}
}
@@ -251,7 +251,10 @@ bool XrayServerConfig::hasEqualServerSettings(const XrayServerConfig &other) con
&& flow == other.flow
&& transport == other.transport
&& fingerprint == other.fingerprint
&& sni == other.sni;
&& sni == other.sni
&& alpn == other.alpn
&& xhttp.toJson() == other.xhttp.toJson()
&& mkcp.toJson() == other.mkcp.toJson();
}
QJsonObject XrayClientConfig::toJson() const
@@ -351,9 +354,150 @@ XrayProtocolConfig XrayProtocolConfig::fromJson(const QJsonObject &json)
}
}
c.needsClientHydration = !json.contains(configKey::xrayTransport) && c.hasClientConfig();
if (c.needsClientHydration) {
c.hydrateServerConfigFromClientNative();
}
return c;
}
bool XrayProtocolConfig::hydrateServerConfigFromClientNative()
{
if (!clientConfig.has_value() || clientConfig->nativeConfig.isEmpty()) {
return false;
}
QJsonDocument doc = QJsonDocument::fromJson(clientConfig->nativeConfig.toUtf8());
if (doc.isNull() || !doc.isObject()) {
return false;
}
const QJsonObject root = doc.object();
const QJsonArray outbounds = root.value(protocols::xray::outbounds).toArray();
if (outbounds.isEmpty()) {
return false;
}
const QJsonObject outbound = outbounds[0].toObject();
const QJsonObject streamSettings = outbound.value(protocols::xray::streamSettings).toObject();
if (streamSettings.isEmpty()) {
return false;
}
XrayServerConfig &srv = serverConfig;
const QJsonObject settings = outbound.value(protocols::xray::settings).toObject();
const QJsonArray vnext = settings.value(protocols::xray::vnext).toArray();
if (!vnext.isEmpty()) {
const QJsonObject vnextEntry = vnext[0].toObject();
if (vnextEntry.contains(protocols::xray::port)) {
srv.port = QString::number(vnextEntry.value(protocols::xray::port).toInt());
}
const QJsonArray users = vnextEntry.value(protocols::xray::users).toArray();
if (!users.isEmpty()) {
srv.flow = users[0].toObject().value(protocols::xray::flow).toString();
}
}
const QString networkVal = streamSettings.value(protocols::xray::network).toString(QStringLiteral("tcp"));
if (networkVal == QLatin1String("xhttp")) {
srv.transport = QStringLiteral("xhttp");
} else if (networkVal == QLatin1String("kcp")) {
srv.transport = QStringLiteral("mkcp");
} else {
srv.transport = QStringLiteral("raw");
}
srv.security = streamSettings.value(protocols::xray::security).toString(protocols::xray::defaultSecurity);
if (srv.security == QLatin1String("reality")) {
const QJsonObject rs = streamSettings.value(protocols::xray::realitySettings).toObject();
srv.sni = rs.value(protocols::xray::serverName).toString();
srv.site = srv.sni.isEmpty() ? srv.site : srv.sni;
const QString fp = rs.value(protocols::xray::fingerprint).toString();
if (!fp.isEmpty()) {
srv.fingerprint = fp.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)
? QString::fromLatin1(protocols::xray::defaultFingerprint)
: fp;
}
}
if (srv.security == QLatin1String("tls")) {
const QJsonObject tls = streamSettings.value(QStringLiteral("tlsSettings")).toObject();
srv.sni = tls.value(protocols::xray::serverName).toString();
const QString fp = tls.value(protocols::xray::fingerprint).toString();
if (!fp.isEmpty()) {
srv.fingerprint = fp;
}
QStringList alpnList;
for (const QJsonValue &v : tls.value(QStringLiteral("alpn")).toArray()) {
alpnList << v.toString();
}
if (!alpnList.isEmpty()) {
srv.alpn = alpnList.join(QLatin1Char(','));
}
}
if (srv.transport == QLatin1String("xhttp")) {
const QJsonObject xhttpObj = streamSettings.value(QStringLiteral("xhttpSettings")).toObject();
QJsonObject xhttpJson;
const QString mode = xhttpObj.value(QStringLiteral("mode")).toString();
if (!mode.isEmpty()) {
if (mode == QLatin1String("auto")) {
xhttpJson[configKey::xhttpMode] = QStringLiteral("Auto");
} else if (mode == QLatin1String("packet-up")) {
xhttpJson[configKey::xhttpMode] = QStringLiteral("Packet-up");
} else if (mode == QLatin1String("stream-up")) {
xhttpJson[configKey::xhttpMode] = QStringLiteral("Stream-up");
} else if (mode == QLatin1String("stream-one")) {
xhttpJson[configKey::xhttpMode] = QStringLiteral("Stream-one");
} else {
xhttpJson[configKey::xhttpMode] = mode;
}
}
if (xhttpObj.contains(QStringLiteral("host"))) {
xhttpJson[configKey::xhttpHost] = xhttpObj.value(QStringLiteral("host")).toString();
}
if (xhttpObj.contains(QStringLiteral("path"))) {
xhttpJson[configKey::xhttpPath] = xhttpObj.value(QStringLiteral("path")).toString();
}
if (xhttpObj.contains(QStringLiteral("uplinkHTTPMethod"))) {
xhttpJson[configKey::xhttpUplinkMethod] = xhttpObj.value(QStringLiteral("uplinkHTTPMethod")).toString();
}
xhttpJson[configKey::xhttpDisableGrpc] = xhttpObj.value(QStringLiteral("noGRPCHeader")).toBool(true);
xhttpJson[configKey::xhttpDisableSse] = xhttpObj.value(QStringLiteral("noSSEHeader")).toBool(true);
srv.xhttp = XrayXhttpConfig::fromJson(xhttpJson);
}
if (srv.transport == QLatin1String("mkcp")) {
const QJsonObject kcpObj = streamSettings.value(QStringLiteral("kcpSettings")).toObject();
XrayMkcpConfig mk;
if (kcpObj.contains(QStringLiteral("tti"))) {
mk.tti = QString::number(kcpObj.value(QStringLiteral("tti")).toInt());
}
if (kcpObj.contains(QStringLiteral("uplinkCapacity"))) {
mk.uplinkCapacity = QString::number(kcpObj.value(QStringLiteral("uplinkCapacity")).toInt());
}
if (kcpObj.contains(QStringLiteral("downlinkCapacity"))) {
mk.downlinkCapacity = QString::number(kcpObj.value(QStringLiteral("downlinkCapacity")).toInt());
}
if (kcpObj.contains(QStringLiteral("readBufferSize"))) {
mk.readBufferSize = QString::number(kcpObj.value(QStringLiteral("readBufferSize")).toInt());
}
if (kcpObj.contains(QStringLiteral("writeBufferSize"))) {
mk.writeBufferSize = QString::number(kcpObj.value(QStringLiteral("writeBufferSize")).toInt());
}
if (kcpObj.contains(QStringLiteral("congestion"))) {
mk.congestion = kcpObj.value(QStringLiteral("congestion")).toBool(true);
}
srv.mkcp = mk;
}
needsClientHydration = false;
return true;
}
bool XrayProtocolConfig::hasClientConfig() const
{
return clientConfig.has_value();
@@ -139,6 +139,12 @@ struct XrayProtocolConfig {
bool hasClientConfig() const;
void setClientConfig(const XrayClientConfig &config);
void clearClientConfig();
/// Set in fromJson when persisted server fields are missing (typical shared profile).
bool needsClientHydration = false;
/// Fills serverConfig from client nativeConfig (outbound streamSettings). For read-only UI.
bool hydrateServerConfigFromClientNative();
};
} // namespace amnezia
@@ -263,11 +263,15 @@ void InstallUiController::updateContainer(const QString &serverId, int container
}
ContainerConfig oldContainerConfig = m_serversController->getContainerConfig(serverId, container);
if (container == DockerContainer::MtProxy || container == DockerContainer::Telemt) {
const bool asyncUpdate = container == DockerContainer::MtProxy || container == DockerContainer::Telemt
|| container == DockerContainer::Xray || container == DockerContainer::SSXray;
if (asyncUpdate) {
emit serverIsBusy(true);
auto *watcher = new QFutureWatcher<ErrorCode>(this);
const Proto protocolTypeCopy = protocolType;
QObject::connect(watcher, &QFutureWatcher<ErrorCode>::finished, this,
[this, watcher, serverId, container, closePage]() {
[this, watcher, serverId, container, closePage, protocolTypeCopy]() {
const ErrorCode errorCode = watcher->result();
watcher->deleteLater();
emit serverIsBusy(false);
@@ -276,6 +280,7 @@ void InstallUiController::updateContainer(const QString &serverId, int container
const ContainerConfig updatedConfig =
m_serversController->getContainerConfig(serverId, container);
m_protocolModel->updateModel(updatedConfig);
updateProtocolConfigModel(serverId, static_cast<int>(container), static_cast<int>(protocolTypeCopy));
const auto defaultContainer =
m_serversController->getDefaultContainer(serverId);
@@ -295,7 +300,7 @@ void InstallUiController::updateContainer(const QString &serverId, int container
InstallController *installController = m_installController;
QFuture<ErrorCode> future =
QtConcurrent::run([installController, serverId, container, oldConfigCopy,
newConfigCopy]() mutable -> ErrorCode {
newConfigCopy]() mutable -> ErrorCode {
return installController->updateContainer(serverId, container, oldConfigCopy, newConfigCopy);
});
watcher->setFuture(future);
@@ -307,6 +312,7 @@ void InstallUiController::updateContainer(const QString &serverId, int container
if (errorCode == ErrorCode::NoError) {
ContainerConfig updatedConfig = m_serversController->getContainerConfig(serverId, container);
m_protocolModel->updateModel(updatedConfig);
updateProtocolConfigModel(serverId, static_cast<int>(container), static_cast<int>(protocolType));
const auto defaultContainer = m_serversController->getDefaultContainer(serverId);
if ((serverId == m_serversController->getDefaultServerId()) && (container == defaultContainer)) {
@@ -427,6 +433,34 @@ void InstallUiController::removeContainer(const QString &serverId, int container
DockerContainer container = static_cast<DockerContainer>(containerIndex);
QString containerName = ContainerUtils::containerHumanNames().value(container);
const bool asyncRemove = container == DockerContainer::Xray || container == DockerContainer::SSXray;
if (asyncRemove) {
emit serverIsBusy(true);
auto *watcher = new QFutureWatcher<ErrorCode>(this);
QObject::connect(watcher, &QFutureWatcher<ErrorCode>::finished, this,
[this, watcher, serverId, container, containerName, serverName]() {
const ErrorCode errorCode = watcher->result();
watcher->deleteLater();
emit serverIsBusy(false);
if (errorCode == ErrorCode::NoError) {
emit removeContainerFinished(
tr("%1 has been removed from the server '%2'").arg(containerName, serverName));
} else {
emit installationErrorOccurred(errorCode);
}
});
InstallController *installController = m_installController;
QFuture<ErrorCode> future = QtConcurrent::run(
[installController, serverId, container]() -> ErrorCode {
return installController->removeContainer(serverId, container);
});
watcher->setFuture(future);
return;
}
ErrorCode errorCode = m_installController->removeContainer(serverId, container);
if (errorCode == ErrorCode::NoError) {
@@ -267,6 +267,9 @@ void XrayConfigModel::updateModel(amnezia::DockerContainer container, const amne
m_container = container;
m_protocolConfig = protocolConfig;
if (m_protocolConfig.needsClientHydration) {
m_protocolConfig.hydrateServerConfigFromClientNative();
}
applyDefaultsToServerConfig(m_protocolConfig.serverConfig);
@@ -64,6 +64,18 @@ PageType {
spacing: 0
Text {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 8
visible: !listView.enabled
wrapMode: Text.WordWrap
color: AmneziaStyle.color.paleGray
font.pixelSize: 14
text: qsTr("You have read-only access to this server. XRay settings cannot be edited.")
}
RowLayout {
Layout.fillWidth: true
Layout.leftMargin: 16
@@ -174,7 +186,7 @@ PageType {
Layout.leftMargin: 16
Layout.rightMargin: 16
// Show Save immediately while user edits port, even before focus loss.
visible: listView.enabled && (XrayConfigModel.hasUnsavedChanges || textFieldWithHeaderType.textField.text !== port)
visible: listView.enabled && XrayConfigModel.hasUnsavedChanges
enabled: visible && textFieldWithHeaderType.errorText === ""
text: qsTr("Save")
onClicked: function() {
@@ -189,6 +201,10 @@ PageType {
return
}
if (textFieldWithHeaderType.textField.text !== port) {
port = textFieldWithHeaderType.textField.text
}
PageController.goToPage(PageEnum.PageSetupWizardInstalling);
InstallController.updateContainer(ServersUiController.getServerId(ServersUiController.processedServerIndex), ServersUiController.processedContainerIndex, ProtocolEnum.Xray)
}
@@ -209,6 +225,8 @@ PageType {
clickedFunction: function() {
var yesButtonFunction = function() {
XrayConfigModel.resetToDefaults()
PageController.showNotificationMessage(
qsTr("Settings were reset to defaults. Tap Save to apply them on the server."))
}
showQuestionDrawer(qsTr("Reset settings?"), qsTr("All XRay settings will be restored to defaults."),
qsTr("Reset"), qsTr("Cancel"), yesButtonFunction, function() {