mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-06-19 02:00:45 +07:00
fix: extended VLESS configuration (#2643)
* fixed vless * fixed default var * fixed save button * remove comment * fix: fixed header link in xray settings page --------- Co-authored-by: vkamn <vk@amnezia.org>
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QThread>
|
||||
#include <QUuid>
|
||||
#include "logger.h"
|
||||
|
||||
@@ -137,117 +138,326 @@ 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);
|
||||
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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
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
|
||||
{
|
||||
outPublicKey.clear();
|
||||
outShortId.clear();
|
||||
|
||||
auto readKeyFile = [&](const QString &path, QString &out) -> ErrorCode {
|
||||
for (int attempt = 0; attempt < 3; ++attempt) {
|
||||
ErrorCode fileError = ErrorCode::NoError;
|
||||
out = QString::fromUtf8(m_sshSession->getTextFileFromContainer(container, credentials, path, fileError));
|
||||
out.replace(QLatin1Char('\n'), QString());
|
||||
out.replace(QLatin1Char('\r'), QString());
|
||||
if (fileError == ErrorCode::NoError && !out.isEmpty()) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
if (attempt < 2) {
|
||||
QThread::msleep(500);
|
||||
}
|
||||
}
|
||||
logger.error() << "Xray readRealityKeyFiles: failed path=" << path;
|
||||
return ErrorCode::XrayRealityKeysReadFailed;
|
||||
};
|
||||
|
||||
ErrorCode errorCode = readKeyFile(QString::fromLatin1(amnezia::protocols::xray::PublicKeyPath), outPublicKey);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
return readKeyFile(QString::fromLatin1(amnezia::protocols::xray::shortidPath), outShortId);
|
||||
}
|
||||
|
||||
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;
|
||||
if (srv.isThirdPartyConfig) {
|
||||
logger.info() << "Xray applyServerSettings: skipped (third-party/native profile)";
|
||||
if (outClientId && xrayCfg->hasClientConfig()) {
|
||||
*outClientId = xrayCfg->clientConfig->id;
|
||||
}
|
||||
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;
|
||||
QString flowValue = srv.flow;
|
||||
if (flowValue.isEmpty() && srv.security == QLatin1String("reality")) {
|
||||
flowValue = QStringLiteral("xtls-rprx-vision");
|
||||
}
|
||||
|
||||
QString realityPublicKey;
|
||||
QString realityShortId;
|
||||
if (srv.security == QLatin1String("reality")) {
|
||||
errorCode = readRealityKeyFiles(container, credentials, realityPublicKey, realityShortId);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Xray applyServerSettings: readRealityKeyFiles failed, error="
|
||||
<< static_cast<int>(errorCode);
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
|
||||
// Get current server config
|
||||
QString currentConfig = m_sshSession->getTextFileFromContainer(
|
||||
container, credentials, amnezia::protocols::xray::serverConfigPath, errorCode);
|
||||
|
||||
container, credentials, amnezia::protocols::xray::serverConfigPath, errorCode);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Failed to get server config file";
|
||||
return "";
|
||||
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();
|
||||
|
||||
// Parse current config as JSON
|
||||
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::XrayServerConfigInvalid;
|
||||
}
|
||||
|
||||
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::XrayServerConfigInvalid;
|
||||
}
|
||||
|
||||
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::XrayServerConfigInvalid;
|
||||
}
|
||||
|
||||
QJsonObject inbound = inbounds[0].toObject();
|
||||
if (!inbound.contains(amnezia::protocols::xray::settings)) {
|
||||
logger.error() << "Inbound missing 'settings' field";
|
||||
errorCode = ErrorCode::InternalError;
|
||||
return "";
|
||||
return ErrorCode::XrayServerConfigInvalid;
|
||||
}
|
||||
|
||||
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::XrayServerNoVlessClients;
|
||||
}
|
||||
clientId = clients[0].toObject()[amnezia::protocols::xray::id].toString();
|
||||
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.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, realityPublicKey, realityShortId);
|
||||
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 &prefetchedRealityPublicKey,
|
||||
const QString &prefetchedRealityShortId) const
|
||||
{
|
||||
QString xrayPublicKey = prefetchedRealityPublicKey;
|
||||
QString xrayShortId = prefetchedRealityShortId;
|
||||
|
||||
if (srv.security == QLatin1String("reality")) {
|
||||
if (xrayPublicKey.isEmpty() || xrayShortId.isEmpty()) {
|
||||
errorCode = readRealityKeyFiles(container, credentials, xrayPublicKey, xrayShortId);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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() ? 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 (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;
|
||||
@@ -419,6 +629,13 @@ ProtocolConfig XrayConfigurator::createConfig(const ServerCredentials &credentia
|
||||
const DnsSettings &dnsSettings,
|
||||
ErrorCode &errorCode)
|
||||
{
|
||||
if (const auto *xrayCfg = containerConfig.protocolConfig.as<XrayProtocolConfig>()) {
|
||||
if (xrayCfg->serverConfig.isThirdPartyConfig && xrayCfg->hasClientConfig()) {
|
||||
logger.info() << "Xray createConfig: returning existing third-party client config without server SSH";
|
||||
return *xrayCfg;
|
||||
}
|
||||
}
|
||||
|
||||
const XrayServerConfig *serverConfig = nullptr;
|
||||
if (const auto *xrayCfg = containerConfig.protocolConfig.as<XrayProtocolConfig>()) {
|
||||
serverConfig = &xrayCfg->serverConfig;
|
||||
@@ -441,93 +658,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,12 +23,37 @@ public:
|
||||
amnezia::ProtocolConfig processConfigWithLocalSettings(const amnezia::ConnectionSettings &settings,
|
||||
amnezia::ProtocolConfig protocolConfig) override;
|
||||
|
||||
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);
|
||||
|
||||
// Builds the native xray "streamSettings" JSON object from XrayServerConfig
|
||||
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;
|
||||
|
||||
amnezia::ErrorCode readRealityKeyFiles(amnezia::DockerContainer container,
|
||||
const amnezia::ServerCredentials &credentials,
|
||||
QString &outPublicKey,
|
||||
QString &outShortId) const;
|
||||
|
||||
QJsonObject mergeStreamSettingsForServerInbound(const amnezia::XrayServerConfig &srv,
|
||||
const QJsonObject &existingStreamSettings) const;
|
||||
|
||||
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"
|
||||
@@ -186,6 +187,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);
|
||||
@@ -196,6 +207,21 @@ ErrorCode InstallController::updateContainer(const QString &serverId, DockerCont
|
||||
}
|
||||
}
|
||||
|
||||
const bool skipXrayInboundSync =
|
||||
newConfig.getXrayProtocolConfig() && newConfig.getXrayProtocolConfig()->serverConfig.isThirdPartyConfig;
|
||||
|
||||
if (errorCode == ErrorCode::NoError && xrayServerSettingsChanged && !skipXrayInboundSync) {
|
||||
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);
|
||||
@@ -643,12 +669,19 @@ 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)
|
||||
const QString oldPort = oldXrayConfig->serverConfig.port.isEmpty()
|
||||
? QString(protocols::xray::defaultPort)
|
||||
: oldXrayConfig->serverConfig.port;
|
||||
const QString newPort = newXrayConfig->serverConfig.port.isEmpty()
|
||||
? QString(protocols::xray::defaultPort)
|
||||
: newXrayConfig->serverConfig.port;
|
||||
if (oldPort != newPort) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -108,35 +108,114 @@ QJsonObject XrayXhttpConfig::toJson() const
|
||||
return obj;
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
XrayXhttpConfig clearedXhttpConfig()
|
||||
{
|
||||
XrayXhttpConfig c;
|
||||
c.mode = QString();
|
||||
c.host = QString();
|
||||
c.path = QString();
|
||||
c.headersTemplate = QString();
|
||||
c.uplinkMethod = QString();
|
||||
c.disableGrpc = false;
|
||||
c.disableSse = false;
|
||||
c.sessionPlacement = QString();
|
||||
c.sessionKey = QString();
|
||||
c.seqPlacement = QString();
|
||||
c.seqKey = QString();
|
||||
c.uplinkDataPlacement = QString();
|
||||
c.uplinkDataKey = QString();
|
||||
c.uplinkChunkSize = QString();
|
||||
c.scMaxBufferedPosts = QString();
|
||||
c.scMaxEachPostBytesMin = QString();
|
||||
c.scMaxEachPostBytesMax = QString();
|
||||
c.scMinPostsIntervalMsMin = QString();
|
||||
c.scMinPostsIntervalMsMax = QString();
|
||||
c.scStreamUpServerSecsMin = QString();
|
||||
c.scStreamUpServerSecsMax = QString();
|
||||
return c;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
XrayXhttpConfig XrayXhttpConfig::fromJson(const QJsonObject &json)
|
||||
{
|
||||
XrayXhttpConfig c;
|
||||
c.mode = json.value(configKey::xhttpMode).toString(protocols::xray::defaultXhttpMode);
|
||||
c.host = json.value(configKey::xhttpHost).toString(protocols::xray::defaultSite);
|
||||
c.path = json.value(configKey::xhttpPath).toString();
|
||||
c.headersTemplate = json.value(configKey::xhttpHeadersTemplate).toString(protocols::xray::defaultXhttpHeadersTemplate);
|
||||
c.uplinkMethod = json.value(configKey::xhttpUplinkMethod).toString(protocols::xray::defaultXhttpUplinkMethod);
|
||||
c.disableGrpc = json.value(configKey::xhttpDisableGrpc).toBool(true);
|
||||
c.disableSse = json.value(configKey::xhttpDisableSse).toBool(true);
|
||||
if (json.isEmpty()) {
|
||||
return clearedXhttpConfig();
|
||||
}
|
||||
|
||||
c.sessionPlacement = json.value(configKey::xhttpSessionPlacement).toString(protocols::xray::defaultXhttpSessionPlacement);
|
||||
c.sessionKey = json.value(configKey::xhttpSessionKey).toString();
|
||||
c.seqPlacement = json.value(configKey::xhttpSeqPlacement).toString(protocols::xray::defaultXhttpSessionPlacement);
|
||||
c.seqKey = json.value(configKey::xhttpSeqKey).toString();
|
||||
c.uplinkDataPlacement = json.value(configKey::xhttpUplinkDataPlacement).toString(protocols::xray::defaultXhttpUplinkDataPlacement);
|
||||
c.uplinkDataKey = json.value(configKey::xhttpUplinkDataKey).toString();
|
||||
XrayXhttpConfig c = clearedXhttpConfig();
|
||||
|
||||
c.uplinkChunkSize = json.value(configKey::xhttpUplinkChunkSize).toString("0");
|
||||
c.scMaxBufferedPosts = json.value(configKey::xhttpScMaxBufferedPosts).toString();
|
||||
c.scMaxEachPostBytesMin = json.value(configKey::xhttpScMaxEachPostBytesMin).toString("1");
|
||||
c.scMaxEachPostBytesMax = json.value(configKey::xhttpScMaxEachPostBytesMax).toString("100");
|
||||
c.scMinPostsIntervalMsMin = json.value(configKey::xhttpScMinPostsIntervalMsMin).toString("100");
|
||||
c.scMinPostsIntervalMsMax = json.value(configKey::xhttpScMinPostsIntervalMsMax).toString("800");
|
||||
c.scStreamUpServerSecsMin = json.value(configKey::xhttpScStreamUpServerSecsMin).toString("1");
|
||||
c.scStreamUpServerSecsMax = json.value(configKey::xhttpScStreamUpServerSecsMax).toString("100");
|
||||
if (json.contains(configKey::xhttpMode)) {
|
||||
c.mode = json.value(configKey::xhttpMode).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpHost)) {
|
||||
c.host = json.value(configKey::xhttpHost).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpPath)) {
|
||||
c.path = json.value(configKey::xhttpPath).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpHeadersTemplate)) {
|
||||
c.headersTemplate = json.value(configKey::xhttpHeadersTemplate).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpUplinkMethod)) {
|
||||
c.uplinkMethod = json.value(configKey::xhttpUplinkMethod).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpDisableGrpc)) {
|
||||
c.disableGrpc = json.value(configKey::xhttpDisableGrpc).toBool();
|
||||
}
|
||||
if (json.contains(configKey::xhttpDisableSse)) {
|
||||
c.disableSse = json.value(configKey::xhttpDisableSse).toBool();
|
||||
}
|
||||
if (json.contains(configKey::xhttpSessionPlacement)) {
|
||||
c.sessionPlacement = json.value(configKey::xhttpSessionPlacement).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpSessionKey)) {
|
||||
c.sessionKey = json.value(configKey::xhttpSessionKey).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpSeqPlacement)) {
|
||||
c.seqPlacement = json.value(configKey::xhttpSeqPlacement).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpSeqKey)) {
|
||||
c.seqKey = json.value(configKey::xhttpSeqKey).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpUplinkDataPlacement)) {
|
||||
c.uplinkDataPlacement = json.value(configKey::xhttpUplinkDataPlacement).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpUplinkDataKey)) {
|
||||
c.uplinkDataKey = json.value(configKey::xhttpUplinkDataKey).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpUplinkChunkSize)) {
|
||||
c.uplinkChunkSize = json.value(configKey::xhttpUplinkChunkSize).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpScMaxBufferedPosts)) {
|
||||
c.scMaxBufferedPosts = json.value(configKey::xhttpScMaxBufferedPosts).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpScMaxEachPostBytesMin)) {
|
||||
c.scMaxEachPostBytesMin = json.value(configKey::xhttpScMaxEachPostBytesMin).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpScMaxEachPostBytesMax)) {
|
||||
c.scMaxEachPostBytesMax = json.value(configKey::xhttpScMaxEachPostBytesMax).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpScMinPostsIntervalMsMin)) {
|
||||
c.scMinPostsIntervalMsMin = json.value(configKey::xhttpScMinPostsIntervalMsMin).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpScMinPostsIntervalMsMax)) {
|
||||
c.scMinPostsIntervalMsMax = json.value(configKey::xhttpScMinPostsIntervalMsMax).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpScStreamUpServerSecsMin)) {
|
||||
c.scStreamUpServerSecsMin = json.value(configKey::xhttpScStreamUpServerSecsMin).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpScStreamUpServerSecsMax)) {
|
||||
c.scStreamUpServerSecsMax = json.value(configKey::xhttpScStreamUpServerSecsMax).toString();
|
||||
}
|
||||
|
||||
c.xPadding = XrayXPaddingConfig::fromJson(json.value("xPadding").toObject());
|
||||
c.xmux = XrayXmuxConfig::fromJson(json.value("xmux").toObject());
|
||||
if (json.contains(QLatin1String("xPadding"))) {
|
||||
c.xPadding = XrayXPaddingConfig::fromJson(json.value(QLatin1String("xPadding")).toObject());
|
||||
}
|
||||
if (json.contains(QLatin1String("xmux"))) {
|
||||
c.xmux = XrayXmuxConfig::fromJson(json.value(QLatin1String("xmux")).toObject());
|
||||
}
|
||||
|
||||
return c;
|
||||
}
|
||||
@@ -156,12 +235,27 @@ QJsonObject XrayMkcpConfig::toJson() const
|
||||
XrayMkcpConfig XrayMkcpConfig::fromJson(const QJsonObject &json)
|
||||
{
|
||||
XrayMkcpConfig c;
|
||||
c.tti = json.value(configKey::mkcpTti).toString();
|
||||
c.uplinkCapacity = json.value(configKey::mkcpUplinkCapacity).toString();
|
||||
c.downlinkCapacity = json.value(configKey::mkcpDownlinkCapacity).toString();
|
||||
c.readBufferSize = json.value(configKey::mkcpReadBufferSize).toString();
|
||||
c.writeBufferSize = json.value(configKey::mkcpWriteBufferSize).toString();
|
||||
c.congestion = json.value(configKey::mkcpCongestion).toBool(true);
|
||||
if (json.isEmpty()) {
|
||||
return c;
|
||||
}
|
||||
if (json.contains(configKey::mkcpTti)) {
|
||||
c.tti = json.value(configKey::mkcpTti).toString();
|
||||
}
|
||||
if (json.contains(configKey::mkcpUplinkCapacity)) {
|
||||
c.uplinkCapacity = json.value(configKey::mkcpUplinkCapacity).toString();
|
||||
}
|
||||
if (json.contains(configKey::mkcpDownlinkCapacity)) {
|
||||
c.downlinkCapacity = json.value(configKey::mkcpDownlinkCapacity).toString();
|
||||
}
|
||||
if (json.contains(configKey::mkcpReadBufferSize)) {
|
||||
c.readBufferSize = json.value(configKey::mkcpReadBufferSize).toString();
|
||||
}
|
||||
if (json.contains(configKey::mkcpWriteBufferSize)) {
|
||||
c.writeBufferSize = json.value(configKey::mkcpWriteBufferSize).toString();
|
||||
}
|
||||
if (json.contains(configKey::mkcpCongestion)) {
|
||||
c.congestion = json.value(configKey::mkcpCongestion).toBool();
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
@@ -208,8 +302,14 @@ QJsonObject XrayServerConfig::toJson() const
|
||||
if (!transport.isEmpty()) {
|
||||
obj[configKey::xrayTransport] = transport;
|
||||
}
|
||||
obj["xhttp"] = xhttp.toJson();
|
||||
obj["mkcp"] = mkcp.toJson();
|
||||
const QJsonObject xhttpObj = xhttp.toJson();
|
||||
if (!xhttpObj.isEmpty()) {
|
||||
obj[QStringLiteral("xhttp")] = xhttpObj;
|
||||
}
|
||||
const QJsonObject mkcpObj = mkcp.toJson();
|
||||
if (!mkcpObj.isEmpty()) {
|
||||
obj[QStringLiteral("mkcp")] = mkcpObj;
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
@@ -225,20 +325,39 @@ XrayServerConfig XrayServerConfig::fromJson(const QJsonObject &json)
|
||||
c.site = json.value(configKey::site).toString();
|
||||
c.isThirdPartyConfig = json.value(configKey::isThirdPartyConfig).toBool(false);
|
||||
|
||||
// New: Security
|
||||
c.security = json.value(configKey::xraySecurity).toString(protocols::xray::defaultSecurity);
|
||||
c.flow = json.value(configKey::xrayFlow).toString(protocols::xray::defaultFlow);
|
||||
c.fingerprint = json.value(configKey::xrayFingerprint).toString(protocols::xray::defaultFingerprint);
|
||||
if (c.fingerprint.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)) {
|
||||
c.fingerprint = QString::fromLatin1(protocols::xray::defaultFingerprint);
|
||||
if (json.contains(configKey::xraySecurity)) {
|
||||
c.security = json.value(configKey::xraySecurity).toString();
|
||||
}
|
||||
if (json.contains(configKey::xrayFlow)) {
|
||||
c.flow = json.value(configKey::xrayFlow).toString();
|
||||
}
|
||||
if (json.contains(configKey::xrayFingerprint)) {
|
||||
c.fingerprint = json.value(configKey::xrayFingerprint).toString();
|
||||
if (c.fingerprint.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)) {
|
||||
c.fingerprint = QString::fromLatin1(protocols::xray::defaultFingerprint);
|
||||
}
|
||||
}
|
||||
if (json.contains(configKey::xraySni)) {
|
||||
c.sni = json.value(configKey::xraySni).toString();
|
||||
}
|
||||
if (json.contains(configKey::xrayAlpn)) {
|
||||
c.alpn = json.value(configKey::xrayAlpn).toString();
|
||||
}
|
||||
if (json.contains(configKey::xrayTransport)) {
|
||||
c.transport = json.value(configKey::xrayTransport).toString();
|
||||
}
|
||||
if (json.contains(QLatin1String("xhttp"))) {
|
||||
const QJsonObject xhttpJson = json.value(QLatin1String("xhttp")).toObject();
|
||||
if (!xhttpJson.isEmpty()) {
|
||||
c.xhttp = XrayXhttpConfig::fromJson(xhttpJson);
|
||||
}
|
||||
}
|
||||
if (json.contains(QLatin1String("mkcp"))) {
|
||||
const QJsonObject mkcpJson = json.value(QLatin1String("mkcp")).toObject();
|
||||
if (!mkcpJson.isEmpty()) {
|
||||
c.mkcp = XrayMkcpConfig::fromJson(mkcpJson);
|
||||
}
|
||||
}
|
||||
c.sni = json.value(configKey::xraySni).toString(protocols::xray::defaultSni);
|
||||
c.alpn = json.value(configKey::xrayAlpn).toString(protocols::xray::defaultAlpn);
|
||||
|
||||
// New: Transport
|
||||
c.transport = json.value(configKey::xrayTransport).toString(protocols::xray::defaultTransport);
|
||||
c.xhttp = XrayXhttpConfig::fromJson(json.value("xhttp").toObject());
|
||||
c.mkcp = XrayMkcpConfig::fromJson(json.value("mkcp").toObject());
|
||||
|
||||
return c;
|
||||
}
|
||||
@@ -251,7 +370,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 +473,154 @@ XrayProtocolConfig XrayProtocolConfig::fromJson(const QJsonObject &json)
|
||||
}
|
||||
}
|
||||
|
||||
c.needsClientHydration =
|
||||
c.hasClientConfig()
|
||||
&& (!json.contains(configKey::xrayTransport) || c.serverConfig.isThirdPartyConfig);
|
||||
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");
|
||||
}
|
||||
|
||||
if (streamSettings.contains(protocols::xray::security)) {
|
||||
srv.security = streamSettings.value(protocols::xray::security).toString();
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
@@ -75,6 +75,7 @@ struct XrayXhttpConfig {
|
||||
XrayXmuxConfig xmux;
|
||||
|
||||
QJsonObject toJson() const;
|
||||
/// Reads only keys present in JSON (no Amnezia UI defaults). Use XrayConfigModel::applyDefaultsToServerConfig for UI.
|
||||
static XrayXhttpConfig fromJson(const QJsonObject &json);
|
||||
};
|
||||
|
||||
@@ -99,15 +100,13 @@ struct XrayServerConfig {
|
||||
QString site;
|
||||
bool isThirdPartyConfig = false;
|
||||
|
||||
// New: Security
|
||||
QString security = protocols::xray::defaultSecurity;
|
||||
QString flow = protocols::xray::defaultFlow;
|
||||
QString fingerprint = protocols::xray::defaultFingerprint;
|
||||
QString sni = protocols::xray::defaultSni;
|
||||
QString alpn = protocols::xray::defaultAlpn;
|
||||
QString security;
|
||||
QString flow;
|
||||
QString fingerprint;
|
||||
QString sni;
|
||||
QString alpn;
|
||||
|
||||
// New: Transport
|
||||
QString transport = protocols::xray::defaultTransport;
|
||||
QString transport;
|
||||
XrayXhttpConfig xhttp;
|
||||
XrayMkcpConfig mkcp;
|
||||
|
||||
@@ -139,6 +138,10 @@ struct XrayProtocolConfig {
|
||||
bool hasClientConfig() const;
|
||||
void setClientConfig(const XrayClientConfig &config);
|
||||
void clearClientConfig();
|
||||
|
||||
bool needsClientHydration = false;
|
||||
|
||||
bool hydrateServerConfigFromClientNative();
|
||||
};
|
||||
|
||||
} // namespace amnezia
|
||||
|
||||
@@ -35,6 +35,9 @@ namespace amnezia
|
||||
ServerCgroupMountpoint = 212,
|
||||
DockerPullRateLimit = 213,
|
||||
ServerLinuxKernelTooOld = 214,
|
||||
XrayServerConfigInvalid = 215,
|
||||
XrayServerNoVlessClients = 216,
|
||||
XrayRealityKeysReadFailed = 217,
|
||||
|
||||
// Ssh connection errors
|
||||
SshRequestDeniedError = 300,
|
||||
|
||||
@@ -30,6 +30,15 @@ QString errorString(ErrorCode code) {
|
||||
case(ErrorCode::ServerCgroupMountpoint): errorMessage = QObject::tr("Server error: cgroup mountpoint does not exist"); break;
|
||||
case(ErrorCode::DockerPullRateLimit): errorMessage = QObject::tr("Docker error: The pull rate limit has been reached"); break;
|
||||
case(ErrorCode::ServerLinuxKernelTooOld): errorMessage = QObject::tr("Server error: Linux kernel is too old"); break;
|
||||
case(ErrorCode::XrayServerConfigInvalid):
|
||||
errorMessage = QObject::tr("Server error: invalid or unreadable XRay server configuration");
|
||||
break;
|
||||
case(ErrorCode::XrayServerNoVlessClients):
|
||||
errorMessage = QObject::tr("Server error: XRay server has no VLESS clients");
|
||||
break;
|
||||
case(ErrorCode::XrayRealityKeysReadFailed):
|
||||
errorMessage = QObject::tr("Server error: failed to read XRay Reality keys from the server");
|
||||
break;
|
||||
|
||||
// Libssh errors
|
||||
case(ErrorCode::SshRequestDeniedError): errorMessage = QObject::tr("SSH request was denied"); break;
|
||||
|
||||
@@ -275,11 +275,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);
|
||||
@@ -288,6 +292,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));
|
||||
emit updateContainerFinished(tr("Settings updated successfully"), closePage);
|
||||
} else {
|
||||
emit installationErrorOccurred(errorCode);
|
||||
@@ -299,7 +304,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);
|
||||
@@ -311,6 +316,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));
|
||||
emit updateContainerFinished(tr("Settings updated successfully"), closePage);
|
||||
return;
|
||||
}
|
||||
@@ -425,6 +431,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,8 +267,13 @@ 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);
|
||||
if (!m_protocolConfig.serverConfig.isThirdPartyConfig) {
|
||||
applyDefaultsToServerConfig(m_protocolConfig.serverConfig);
|
||||
}
|
||||
|
||||
m_originalProtocolConfig = m_protocolConfig;
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -73,6 +85,8 @@ PageType {
|
||||
BaseHeaderType {
|
||||
Layout.fillWidth: true
|
||||
headerText: qsTr("XRay VLESS settings")
|
||||
descriptionLinkText: qsTr("More about settings")
|
||||
descriptionLinkUrl: "https://docs.amnezia.org"
|
||||
}
|
||||
|
||||
ImageButtonType {
|
||||
@@ -85,22 +99,6 @@ PageType {
|
||||
}
|
||||
}
|
||||
|
||||
LabelTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 4
|
||||
text: qsTr("More about settings")
|
||||
color: AmneziaStyle.color.goldenApricot
|
||||
font.pixelSize: 16
|
||||
lineHeight: 24 + LanguageUiController.getLineHeightAppend()
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: Qt.openUrlExternally("https://docs.amnezia.org")
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
id: textFieldWithHeaderType
|
||||
Layout.fillWidth: true
|
||||
@@ -173,8 +171,9 @@ PageType {
|
||||
Layout.bottomMargin: 8
|
||||
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
|
||||
|| textFieldWithHeaderType.textField.text !== port)
|
||||
enabled: visible && textFieldWithHeaderType.errorText === ""
|
||||
text: qsTr("Save")
|
||||
onClicked: function() {
|
||||
@@ -189,6 +188,10 @@ PageType {
|
||||
return
|
||||
}
|
||||
|
||||
if (textFieldWithHeaderType.textField.text !== port) {
|
||||
port = textFieldWithHeaderType.textField.text
|
||||
}
|
||||
|
||||
PageController.goToPage(PageEnum.PageSetupWizardInstalling);
|
||||
InstallController.updateContainer(ServersUiController.processedServerId, ServersUiController.processedContainerIndex, ProtocolEnum.Xray)
|
||||
}
|
||||
@@ -209,6 +212,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() {
|
||||
|
||||
Reference in New Issue
Block a user