Compare commits

...

5 Commits

Author SHA1 Message Date
yp
fb5666057b feat: add extended vless configuration (#2566)
* update UI XRay, add new page PageProtocolXrayTransportSettings.qml PageProtocolXrayXmuxSettings.qml PageProtocolXrayXPaddingSettings.qml

* add UI PageProtocolXrayConfigsSettings, PageProtocolXrayFlowSettings, PageProtocolXraySecuritySettings

* add Xray-specific keys

* add vars xray model

* add new qml padding, update model

* update model and export

* rename file & update name class & update list xray

* fixed ui

* add save file in temp

* remove debug macros

* fixed build windows

* fix path Windows

* remove save config

* fixed changes

* fixed conf

* fixed UI

* fixed size & button save

* fixed build iOS

* fix: fixed headers base control

---------

Co-authored-by: vkamn <vk@amnezia.org>
2026-05-18 22:35:01 +08:00
yp
a49892c7e7 feat: add telemt container (#2435)
* Feat: Add MtProxy (Telegram)

* add path files

* Feat: Add Telemt (MtProxy)

* fixed secret & enum

* remove old path

* refactor: move logic from ui to core

---------

Co-authored-by: vkamn <vk@amnezia.org>
2026-05-18 20:01:09 +08:00
yp
277b295fd8 feat: add mtproxy(#2370)
* Feat: Add MtProxy (Telegram)

* add path files

* refactor: move logic from ui to core

---------

Co-authored-by: vkamn <vk@amnezia.org>
2026-05-18 19:52:58 +08:00
lunardunno
8c33779fc3 chore: Install recommends for apt (#2596) 2026-05-18 13:56:57 +08:00
lunardunno
f0299ca9fe chore: authentication prompt in Ubuntu 26.04 (#2603)
Handling the password prompt in Ubuntu 26.04
2026-05-18 11:55:07 +08:00
93 changed files with 11142 additions and 267 deletions

View File

@@ -35,6 +35,8 @@ set(HEADERS ${HEADERS}
${CLIENT_ROOT_DIR}/core/installers/torInstaller.h
${CLIENT_ROOT_DIR}/core/installers/sftpInstaller.h
${CLIENT_ROOT_DIR}/core/installers/socks5Installer.h
${CLIENT_ROOT_DIR}/core/installers/mtProxyInstaller.h
${CLIENT_ROOT_DIR}/core/installers/telemtInstaller.h
${CLIENT_ROOT_DIR}/core/controllers/appSplitTunnelingController.h
${CLIENT_ROOT_DIR}/core/controllers/ipSplitTunnelingController.h
${CLIENT_ROOT_DIR}/core/controllers/allowedDnsController.h
@@ -110,6 +112,8 @@ set(SOURCES ${SOURCES}
${CLIENT_ROOT_DIR}/core/installers/torInstaller.cpp
${CLIENT_ROOT_DIR}/core/installers/sftpInstaller.cpp
${CLIENT_ROOT_DIR}/core/installers/socks5Installer.cpp
${CLIENT_ROOT_DIR}/core/installers/mtProxyInstaller.cpp
${CLIENT_ROOT_DIR}/core/installers/telemtInstaller.cpp
${CLIENT_ROOT_DIR}/core/controllers/appSplitTunnelingController.cpp
${CLIENT_ROOT_DIR}/core/controllers/ipSplitTunnelingController.cpp
${CLIENT_ROOT_DIR}/core/controllers/allowedDnsController.cpp
@@ -201,12 +205,14 @@ file(GLOB UI_MODELS_H CONFIGURE_DEPENDS
${CLIENT_ROOT_DIR}/ui/models/*.h
${CLIENT_ROOT_DIR}/ui/models/protocols/*.h
${CLIENT_ROOT_DIR}/ui/models/services/*.h
${CLIENT_ROOT_DIR}/ui/models/utils/*.h
${CLIENT_ROOT_DIR}/ui/models/api/*.h
)
file(GLOB UI_MODELS_CPP CONFIGURE_DEPENDS
${CLIENT_ROOT_DIR}/ui/models/*.cpp
${CLIENT_ROOT_DIR}/ui/models/protocols/*.cpp
${CLIENT_ROOT_DIR}/ui/models/services/*.cpp
${CLIENT_ROOT_DIR}/ui/models/utils/*.cpp
${CLIENT_ROOT_DIR}/ui/models/api/*.cpp
)

View File

@@ -20,14 +20,123 @@
#include "core/models/protocols/xrayProtocolConfig.h"
namespace {
Logger logger("XrayConfigurator");
}
Logger logger("XrayConfigurator");
QString normalizeXhttpMode(const QString &m) {
const QString t = m.trimmed();
if (t.isEmpty() || t.compare(QLatin1String("Auto"), Qt::CaseInsensitive) == 0) {
return QStringLiteral("auto");
}
if (t.compare(QLatin1String("Packet-up"), Qt::CaseInsensitive) == 0)
return QStringLiteral("packet-up");
if (t.compare(QLatin1String("Stream-up"), Qt::CaseInsensitive) == 0)
return QStringLiteral("stream-up");
if (t.compare(QLatin1String("Stream-one"), Qt::CaseInsensitive) == 0)
return QStringLiteral("stream-one");
return t.toLower();
}
// Xray-core: empty → path; "None" in UI → omit (core default path)
QString normalizeSessionSeqPlacement(const QString &p)
{
if (p.isEmpty() || p.compare(QLatin1String("None"), Qt::CaseInsensitive) == 0)
return {};
return p.toLower();
}
QString normalizeUplinkDataPlacement(const QString &p)
{
if (p.isEmpty() || p.compare(QLatin1String("Body"), Qt::CaseInsensitive) == 0)
return QStringLiteral("body");
if (p.compare(QLatin1String("Auto"), Qt::CaseInsensitive) == 0)
return QStringLiteral("auto");
if (p.compare(QLatin1String("Query"), Qt::CaseInsensitive) == 0)
// "Query" is not valid for uplink payload in splithttp; closest documented mode
return QStringLiteral("header");
return p.toLower();
}
// splithttp: cookie | header | query | queryInHeader (not "body")
QString normalizeXPaddingPlacement(const QString &p)
{
QString t = p.trimmed();
if (t.isEmpty())
return QString::fromLatin1(amnezia::protocols::xray::defaultXPaddingPlacement).toLower();
if (t.compare(QLatin1String("Body"), Qt::CaseInsensitive) == 0)
return QStringLiteral("queryInHeader");
if (t.contains(QLatin1String("queryInHeader"), Qt::CaseInsensitive)
|| t.compare(QLatin1String("Query in header"), Qt::CaseInsensitive) == 0)
return QStringLiteral("queryInHeader");
return t.toLower();
}
// splithttp: repeat-x | tokenish
QString normalizeXPaddingMethod(const QString &m)
{
QString t = m.trimmed();
if (t.isEmpty() || t.compare(QLatin1String("Repeat-x"), Qt::CaseInsensitive) == 0)
return QStringLiteral("repeat-x");
if (t.compare(QLatin1String("Tokenish"), Qt::CaseInsensitive) == 0)
return QStringLiteral("tokenish");
if (t.compare(QLatin1String("Random"), Qt::CaseInsensitive) == 0
|| t.compare(QLatin1String("Zero"), Qt::CaseInsensitive) == 0)
return QStringLiteral("repeat-x");
return t.toLower();
}
void putIntRangeIfAny(QJsonObject &obj, const char *key, QString minV, QString maxV, const char *fallbackMin,
const char *fallbackMax)
{
if (minV.isEmpty() && maxV.isEmpty())
return;
if (minV.isEmpty())
minV = QString::fromLatin1(fallbackMin);
if (maxV.isEmpty())
maxV = QString::fromLatin1(fallbackMax);
QJsonObject r;
r[QStringLiteral("from")] = minV.toInt();
r[QStringLiteral("to")] = maxV.toInt();
obj[QString::fromUtf8(key)] = r;
}
// Desktop applies this in XrayProtocol::start(); iOS/Android pass JSON straight to libxray — same fixes here.
void sanitizeXrayNativeConfig(amnezia::ProtocolConfig &pc)
{
QString c = pc.nativeConfig();
if (c.isEmpty()) {
return;
}
bool changed = false;
if (c.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)) {
c.replace(QLatin1String("Mozilla/5.0"), QString::fromLatin1(amnezia::protocols::xray::defaultFingerprint),
Qt::CaseInsensitive);
changed = true;
}
const QString legacyListen = QString::fromLatin1(amnezia::protocols::xray::defaultLocalAddr);
const QString listenOk = QString::fromLatin1(amnezia::protocols::xray::defaultLocalListenAddr);
if (c.contains(legacyListen)) {
c.replace(legacyListen, listenOk);
changed = true;
}
if (changed) {
pc.setNativeConfig(c);
}
}
} // namespace
XrayConfigurator::XrayConfigurator(SshSession* sshSession, QObject *parent)
: ConfiguratorBase(sshSession, parent)
{
}
amnezia::ProtocolConfig XrayConfigurator::processConfigWithLocalSettings(const amnezia::ConnectionSettings &settings,
amnezia::ProtocolConfig protocolConfig)
{
applyDnsToNativeConfig(settings.dns, protocolConfig);
sanitizeXrayNativeConfig(protocolConfig);
return protocolConfig;
}
QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentials, DockerContainer container,
const ContainerConfig &containerConfig,
const DnsSettings &dnsSettings,
@@ -35,11 +144,19 @@ QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentia
{
// 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);
if (errorCode != ErrorCode::NoError) {
logger.error() << "Failed to get server config file";
return "";
@@ -54,7 +171,7 @@ QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentia
}
QJsonObject serverConfig = doc.object();
// Validate server config structure
if (!serverConfig.contains(amnezia::protocols::xray::inbounds)) {
logger.error() << "Server config missing 'inbounds' field";
@@ -68,7 +185,7 @@ QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentia
errorCode = ErrorCode::InternalError;
return "";
}
QJsonObject inbound = inbounds[0].toObject();
if (!inbound.contains(amnezia::protocols::xray::settings)) {
logger.error() << "Inbound missing 'settings' field";
@@ -84,26 +201,29 @@ QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentia
}
QJsonArray clients = settings[amnezia::protocols::xray::clients].toArray();
// Create configuration for new client
QJsonObject clientConfig {
{amnezia::protocols::xray::id, clientId},
{amnezia::protocols::xray::flow, "xtls-rprx-vision"}
};
clientConfig[amnezia::protocols::xray::id] = clientId;
if (!flowValue.isEmpty()) {
clientConfig[amnezia::protocols::xray::flow] = flowValue;
}
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,
container,
credentials,
updatedConfig,
amnezia::protocols::xray::serverConfigPath,
libssh::ScpOverwriteMode::ScpOverwriteExisting
@@ -116,7 +236,7 @@ QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentia
// Restart container
QString restartScript = QString("sudo docker restart $CONTAINER_NAME");
errorCode = m_sshSession->runScript(
credentials,
credentials,
m_sshSession->replaceVars(restartScript, amnezia::genBaseVars(credentials, container, dnsSettings.primaryDns, dnsSettings.secondaryDns))
);
@@ -128,75 +248,286 @@ QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentia
return clientId;
}
ProtocolConfig XrayConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container,
const ContainerConfig &containerConfig,
const DnsSettings &dnsSettings,
ErrorCode &errorCode)
QJsonObject XrayConfigurator::buildStreamSettings(const XrayServerConfig &srv, const QString &clientId) const
{
const XrayServerConfig* serverConfig = nullptr;
if (auto* xrayConfig = containerConfig.protocolConfig.as<XrayProtocolConfig>()) {
serverConfig = &xrayConfig->serverConfig;
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;
streamSettings[px::security] = srv.security;
if (srv.security == QLatin1String("tls")) {
QJsonObject tlsSettings;
const QString sniEff = srv.sni.isEmpty() ? QString::fromLatin1(px::defaultSni) : srv.sni;
tlsSettings[px::serverName] = sniEff;
const QString alpnEff = srv.alpn.isEmpty() ? QString::fromLatin1(px::defaultAlpn) : srv.alpn;
QJsonArray alpnArray;
for (const QString &a : alpnEff.split(QLatin1Char(','))) {
const QString t = a.trimmed();
if (!t.isEmpty())
alpnArray.append(t);
}
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 (srv.security == 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;
xo[QStringLiteral("mode")] = normalizeXhttpMode(xhttp.mode);
if (xhttp.headersTemplate.compare(QLatin1String("HTTP"), Qt::CaseInsensitive) == 0) {
QJsonObject headers;
headers[QStringLiteral("Host")] = hostEff;
xo[QStringLiteral("headers")] = headers;
}
const QString methodEff =
xhttp.uplinkMethod.isEmpty() ? QString::fromLatin1(px::defaultXhttpUplinkMethod) : xhttp.uplinkMethod;
xo[QStringLiteral("uplinkHTTPMethod")] = methodEff.toUpper();
xo[QStringLiteral("noGRPCHeader")] = xhttp.disableGrpc;
xo[QStringLiteral("noSSEHeader")] = xhttp.disableSse;
const QString sessPl = normalizeSessionSeqPlacement(xhttp.sessionPlacement);
if (!sessPl.isEmpty())
xo[QStringLiteral("sessionPlacement")] = sessPl;
const QString seqPl = normalizeSessionSeqPlacement(xhttp.seqPlacement);
if (!seqPl.isEmpty())
xo[QStringLiteral("seqPlacement")] = seqPl;
if (!xhttp.sessionKey.isEmpty())
xo[QStringLiteral("sessionKey")] = xhttp.sessionKey;
if (!xhttp.seqKey.isEmpty())
xo[QStringLiteral("seqKey")] = xhttp.seqKey;
xo[QStringLiteral("uplinkDataPlacement")] = normalizeUplinkDataPlacement(xhttp.uplinkDataPlacement);
if (!xhttp.uplinkDataKey.isEmpty())
xo[QStringLiteral("uplinkDataKey")] = xhttp.uplinkDataKey;
const QString ucs = xhttp.uplinkChunkSize.isEmpty() ? QString::fromLatin1(px::defaultXhttpUplinkChunkSize)
: xhttp.uplinkChunkSize;
if (!ucs.isEmpty() && ucs != QLatin1String("0")) {
const int v = ucs.toInt();
QJsonObject chunkR;
chunkR[QStringLiteral("from")] = v;
chunkR[QStringLiteral("to")] = v;
xo[QStringLiteral("uplinkChunkSize")] = chunkR;
}
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()) {
QJsonObject br;
br[QStringLiteral("from")] = pad.bytesMin.isEmpty() ? 1 : pad.bytesMin.toInt();
br[QStringLiteral("to")] = pad.bytesMax.isEmpty() ? (pad.bytesMin.isEmpty() ? 256 : pad.bytesMin.toInt())
: pad.bytesMax.toInt();
xo[QStringLiteral("xPaddingBytes")] = br;
}
xo[QStringLiteral("xPaddingKey")] = pad.key.isEmpty() ? QStringLiteral("x_padding") : pad.key;
xo[QStringLiteral("xPaddingHeader")] = pad.header.isEmpty() ? QStringLiteral("X-Padding") : pad.header;
xo[QStringLiteral("xPaddingPlacement")] = normalizeXPaddingPlacement(
pad.placement.isEmpty() ? QString::fromLatin1(px::defaultXPaddingPlacement) : pad.placement);
xo[QStringLiteral("xPaddingMethod")] = normalizeXPaddingMethod(
pad.method.isEmpty() ? QString::fromLatin1(px::defaultXPaddingMethod) : pad.method);
}
// 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) {
if (a.isEmpty() && b.isEmpty())
return;
QJsonObject r;
r[QStringLiteral("from")] = a.isEmpty() ? 0 : a.toInt();
r[QStringLiteral("to")] = b.isEmpty() ? 0 : b.toInt();
mux[QString::fromUtf8(key)] = r;
};
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,
ErrorCode &errorCode)
{
const XrayServerConfig *serverConfig = nullptr;
if (const auto *xrayCfg = containerConfig.protocolConfig.as<XrayProtocolConfig>()) {
serverConfig = &xrayCfg->serverConfig;
}
if (!serverConfig) {
logger.error() << "No XrayProtocolConfig found";
errorCode = ErrorCode::InternalError;
return XrayProtocolConfig{};
}
const XrayServerConfig &srv = *serverConfig;
QString xrayClientId = prepareServerConfig(credentials, container, containerConfig, dnsSettings, errorCode);
if (errorCode != ErrorCode::NoError || xrayClientId.isEmpty()) {
logger.error() << "Failed to prepare server config";
errorCode = ErrorCode::InternalError;
if (errorCode == ErrorCode::NoError) {
errorCode = ErrorCode::InternalError;
}
return XrayProtocolConfig{};
}
amnezia::ScriptVars vars = amnezia::genBaseVars(credentials, container, dnsSettings.primaryDns, dnsSettings.secondaryDns);
vars.append(amnezia::genProtocolVarsForContainer(container, containerConfig));
QString config = m_sshSession->replaceVars(amnezia::scriptData(ProtocolScriptType::xray_template, container), vars);
if (config.isEmpty()) {
logger.error() << "Failed to get config template";
errorCode = ErrorCode::InternalError;
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", "");
}
QString xrayPublicKey =
m_sshSession->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::PublicKeyPath, errorCode);
if (errorCode != ErrorCode::NoError || xrayPublicKey.isEmpty()) {
logger.error() << "Failed to get public key";
errorCode = ErrorCode::InternalError;
return XrayProtocolConfig{};
}
xrayPublicKey.replace("\n", "");
QString xrayShortId =
m_sshSession->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::shortidPath, errorCode);
if (errorCode != ErrorCode::NoError || xrayShortId.isEmpty()) {
logger.error() << "Failed to get short ID";
errorCode = ErrorCode::InternalError;
return XrayProtocolConfig{};
}
xrayShortId.replace("\n", "");
if (!config.contains("$XRAY_CLIENT_ID") || !config.contains("$XRAY_PUBLIC_KEY") || !config.contains("$XRAY_SHORT_ID")) {
logger.error() << "Config template missing required variables:"
<< "XRAY_CLIENT_ID:" << !config.contains("$XRAY_CLIENT_ID")
<< "XRAY_PUBLIC_KEY:" << !config.contains("$XRAY_PUBLIC_KEY")
<< "XRAY_SHORT_ID:" << !config.contains("$XRAY_SHORT_ID");
errorCode = ErrorCode::InternalError;
return XrayProtocolConfig{};
// 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;
}
config.replace("$XRAY_CLIENT_ID", xrayClientId);
config.replace("$XRAY_PUBLIC_KEY", xrayPublicKey);
config.replace("$XRAY_SHORT_ID", xrayShortId);
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;
if (serverConfig) {
protocolConfig.serverConfig = *serverConfig;
}
protocolConfig.serverConfig = srv;
XrayClientConfig clientConfig;
clientConfig.nativeConfig = config;
clientConfig.localPort = "";
qDebug() << "config:" << config;
clientConfig.localPort = QString(amnezia::protocols::xray::defaultLocalProxyPort);
clientConfig.id = xrayClientId;
protocolConfig.setClientConfig(clientConfig);
return protocolConfig;
}
}

View File

@@ -2,11 +2,13 @@
#define XRAY_CONFIGURATOR_H
#include <QObject>
#include <QJsonObject>
#include "configuratorBase.h"
#include "core/utils/errorCodes.h"
#include "core/utils/routeModes.h"
#include "core/utils/commonStructs.h"
#include "core/models/protocols/xrayProtocolConfig.h"
class XrayConfigurator : public ConfiguratorBase
{
@@ -18,10 +20,17 @@ public:
const amnezia::DnsSettings &dnsSettings,
amnezia::ErrorCode &errorCode) override;
amnezia::ProtocolConfig processConfigWithLocalSettings(const amnezia::ConnectionSettings &settings,
amnezia::ProtocolConfig protocolConfig) override;
private:
QString prepareServerConfig(const amnezia::ServerCredentials &credentials, amnezia::DockerContainer container, const amnezia::ContainerConfig &containerConfig,
const amnezia::DnsSettings &dnsSettings,
amnezia::ErrorCode &errorCode);
// Builds the native xray "streamSettings" JSON object from XrayServerConfig
QJsonObject buildStreamSettings(const amnezia::XrayServerConfig &srv,
const QString &clientId) const;
};
#endif // XRAY_CONFIGURATOR_H

View File

@@ -86,6 +86,9 @@ void CoreController::initModels()
m_xrayConfigModel = new XrayConfigModel(this);
setQmlContextProperty("XrayConfigModel", m_xrayConfigModel);
m_xrayConfigSnapshotsModel = new XrayConfigSnapshotsModel(m_appSettingsRepository, m_xrayConfigModel, this);
setQmlContextProperty("XrayConfigSnapshotsModel", m_xrayConfigSnapshotsModel);
m_torConfigModel = new TorConfigModel(this);
setQmlContextProperty("TorConfigModel", m_torConfigModel);
@@ -100,6 +103,12 @@ void CoreController::initModels()
m_socks5ConfigModel = new Socks5ProxyConfigModel(this);
setQmlContextProperty("Socks5ProxyConfigModel", m_socks5ConfigModel);
m_mtProxyConfigModel = new MtProxyConfigModel(this);
setQmlContextProperty("MtProxyConfigModel", m_mtProxyConfigModel);
m_telemtConfigModel = new TelemtConfigModel(this);
setQmlContextProperty("TelemtConfigModel", m_telemtConfigModel);
m_clientManagementModel = new ClientManagementModel(this);
setQmlContextProperty("ClientManagementModel", m_clientManagementModel);
@@ -169,7 +178,7 @@ void CoreController::initControllers()
#ifdef Q_OS_WINDOWS
m_ikev2ConfigModel,
#endif
m_sftpConfigModel, m_socks5ConfigModel, this);
m_sftpConfigModel, m_socks5ConfigModel, m_mtProxyConfigModel, m_telemtConfigModel, this);
setQmlContextProperty("InstallController", m_installUiController);
m_importController = new ImportUiController(m_importCoreController, this);
@@ -202,6 +211,10 @@ void CoreController::initControllers()
m_systemController = new SystemController(this);
setQmlContextProperty("SystemController", m_systemController);
m_networkReachabilityController = new NetworkReachabilityController(this);
m_engine->rootContext()->setContextProperty("NetworkReachabilityController", m_networkReachabilityController);
m_engine->rootContext()->setContextProperty("NetworkReachability", m_networkReachabilityController);
m_servicesCatalogUiController = new ServicesCatalogUiController(m_servicesCatalogController, m_apiServicesModel, this);
setQmlContextProperty("ServicesCatalogUiController", m_servicesCatalogUiController);

View File

@@ -28,6 +28,7 @@
#include "ui/controllers/languageUiController.h"
#include "ui/controllers/updateUiController.h"
#include "ui/controllers/api/servicesCatalogUiController.h"
#include "ui/controllers/networkReachabilityController.h"
#include "core/controllers/serversController.h"
#include "core/controllers/selfhosted/usersController.h"
@@ -64,11 +65,15 @@
#include "ui/models/protocols/openvpnConfigModel.h"
#include "ui/models/protocols/wireguardConfigModel.h"
#include "ui/models/protocols/xrayConfigModel.h"
#include "ui/models/protocols/xrayConfigSnapshotsModel.h"
#include "ui/models/protocolsModel.h"
#include "ui/models/services/torConfigModel.h"
#include "ui/models/serversModel.h"
#include "ui/models/services/sftpConfigModel.h"
#include "ui/models/services/socks5ProxyConfigModel.h"
#include "ui/models/services/mtProxyConfigModel.h"
#include "ui/models/services/telemtConfigModel.h"
#include "ui/models/ipSplitTunnelingModel.h"
#include "ui/models/newsModel.h"
@@ -156,6 +161,7 @@ private:
ServersUiController* m_serversUiController;
IpSplitTunnelingUiController* m_ipSplitTunnelingUiController;
SystemController* m_systemController;
NetworkReachabilityController* m_networkReachabilityController;
AppSplitTunnelingUiController* m_appSplitTunnelingUiController;
AllowedDnsUiController* m_allowedDnsUiController;
LanguageUiController* m_languageUiController;
@@ -200,6 +206,7 @@ private:
OpenVpnConfigModel* m_openVpnConfigModel;
XrayConfigModel* m_xrayConfigModel;
XrayConfigSnapshotsModel* m_xrayConfigSnapshotsModel;
TorConfigModel* m_torConfigModel;
WireGuardConfigModel* m_wireGuardConfigModel;
AwgConfigModel* m_awgConfigModel;
@@ -208,6 +215,8 @@ private:
#endif
SftpConfigModel* m_sftpConfigModel;
Socks5ProxyConfigModel* m_socks5ConfigModel;
MtProxyConfigModel* m_mtProxyConfigModel;
TelemtConfigModel* m_telemtConfigModel;
CoreSignalHandlers* m_signalHandlers;
};

View File

@@ -323,6 +323,18 @@ ExportController::ExportResult ExportController::generateXrayConfig(const QStrin
vlessServer.shortId = realitySettings.value(amnezia::protocols::xray::shortId).toString();
vlessServer.fingerprint = realitySettings.value(amnezia::protocols::xray::fingerprint).toString("chrome");
vlessServer.spiderX = realitySettings.value(amnezia::protocols::xray::spiderX).toString("");
} else if (vlessServer.security == "tls") {
QJsonObject tlsSettings = streamSettings.value("tlsSettings").toObject();
vlessServer.serverName = tlsSettings.value(amnezia::protocols::xray::serverName).toString();
vlessServer.fingerprint = tlsSettings.value(amnezia::protocols::xray::fingerprint).toString();
// alpn: serialize array back to comma-separated for VLESS URI
QJsonArray alpnArr = tlsSettings.value("alpn").toArray();
QStringList alpnList;
for (const QJsonValue &v : alpnArr) {
alpnList << v.toString();
}
// alpn goes into vless URI query param — handled by Serialize via serverName/alpn fields
// VlessServerObject doesn't have alpn field, so we embed in serverName if needed
}
result.nativeConfigString = amnezia::serialization::vless::Serialize(vlessServer, "AmneziaVPN");

View File

@@ -19,6 +19,8 @@
#include "core/installers/openvpnInstaller.h"
#include "core/installers/sftpInstaller.h"
#include "core/installers/socks5Installer.h"
#include "core/installers/mtProxyInstaller.h"
#include "core/installers/telemtInstaller.h"
#include "core/installers/torInstaller.h"
#include "core/installers/wireguardInstaller.h"
#include "core/installers/xrayInstaller.h"
@@ -34,6 +36,7 @@
#include "core/utils/constants/configKeys.h"
#include "core/utils/constants/protocolConstants.h"
#include "core/models/containerConfig.h"
#include "core/models/protocols/mtProxyProtocolConfig.h"
#include "core/models/protocols/awgProtocolConfig.h"
#include "ui/models/protocols/wireguardConfigModel.h"
#include "core/utils/utilities.h"
@@ -53,6 +56,21 @@ using namespace ProtocolUtils;
namespace
{
Logger logger("InstallController");
bool dockerDaemonContainerMissing(const QString &out, const QString &containerDockerName)
{
if (!out.contains(QLatin1String("Error response from daemon"), Qt::CaseInsensitive)) {
return false;
}
if (out.contains(QLatin1String("No such container"), Qt::CaseInsensitive)
&& out.contains(containerDockerName, Qt::CaseInsensitive)) {
return true;
}
if (out.size() < 700 && out.contains(QLatin1String("is not running"), Qt::CaseInsensitive)) {
return true;
}
return false;
}
}
InstallController::InstallController(SecureServersRepository *serversRepository,
@@ -136,6 +154,15 @@ ErrorCode InstallController::updateContainer(const QString &serverId, DockerCont
if (!adminConfig.has_value()) {
return ErrorCode::InternalError;
}
if (container == DockerContainer::MtProxy) {
ServerCredentials credentials = adminConfig->credentials();
SshSession sshSession(this);
MtProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
} else if (container == DockerContainer::Telemt) {
ServerCredentials credentials = adminConfig->credentials();
SshSession sshSession(this);
TelemtInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
}
adminConfig->updateContainerConfig(container, newConfig);
m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin);
return ErrorCode::NoError;
@@ -165,6 +192,11 @@ ErrorCode InstallController::updateContainer(const QString &serverId, DockerCont
}
if (errorCode == ErrorCode::NoError) {
if (container == DockerContainer::MtProxy) {
MtProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
} else if (container == DockerContainer::Telemt) {
TelemtInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
}
clearCachedProfile(serverId, container);
adminConfig->updateContainerConfig(container, newConfig);
m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin);
@@ -408,9 +440,24 @@ ErrorCode InstallController::configureContainerWorker(const ServerCredentials &c
sshSession.replaceVars(amnezia::scriptData(ProtocolScriptType::configure_container, container), baseVars),
cbReadStdOut, cbReadStdErr);
if (e != ErrorCode::NoError) {
return e;
}
if (dockerDaemonContainerMissing(stdOut, ContainerUtils::containerToString(container))) {
qDebug() << "configureContainerWorker: Docker daemon reports container missing/stopped, output:" << stdOut;
return ErrorCode::ServerContainerMissingError;
}
updateContainerConfigAfterInstallation(container, config, stdOut);
return e;
if (container == DockerContainer::MtProxy) {
MtProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, config);
} else if (container == DockerContainer::Telemt) {
TelemtInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, config);
}
return ErrorCode::NoError;
}
ErrorCode InstallController::startupContainerWorker(const ServerCredentials &credentials, DockerContainer container, const ContainerConfig &config, SshSession &sshSession)
@@ -563,6 +610,79 @@ bool InstallController::isReinstallContainerRequired(DockerContainer container,
}
}
if (container == DockerContainer::MtProxy) {
const auto *oldMt = oldConfig.getMtProxyProtocolConfig();
const auto *newMt = newConfig.getMtProxyProtocolConfig();
if (oldMt && newMt) {
const QString oldPort =
oldMt->port.isEmpty() ? QString(protocols::mtProxy::defaultPort) : oldMt->port;
const QString newPort =
newMt->port.isEmpty() ? QString(protocols::mtProxy::defaultPort) : newMt->port;
if (oldPort != newPort) {
return true;
}
const QString oldTransport = oldMt->transportMode.isEmpty() ? QString(
protocols::mtProxy::transportModeStandard)
: oldMt->transportMode;
const QString newTransport = newMt->transportMode.isEmpty() ? QString(
protocols::mtProxy::transportModeStandard)
: newMt->transportMode;
if (oldTransport != newTransport) {
return true;
}
if (oldMt->tlsDomain != newMt->tlsDomain) {
return true;
}
}
}
if (container == DockerContainer::Telemt) {
const auto *oldT = oldConfig.getTelemtProtocolConfig();
const auto *newT = newConfig.getTelemtProtocolConfig();
if (oldT && newT) {
const QString oldPort =
oldT->port.isEmpty() ? QString(protocols::telemt::defaultPort) : oldT->port;
const QString newPort =
newT->port.isEmpty() ? QString(protocols::telemt::defaultPort) : newT->port;
if (oldPort != newPort) {
return true;
}
const QString oldTransport = oldT->transportMode.isEmpty()
? QString(protocols::telemt::transportModeStandard)
: oldT->transportMode;
const QString newTransport = newT->transportMode.isEmpty()
? QString(protocols::telemt::transportModeStandard)
: newT->transportMode;
if (oldTransport != newTransport) {
return true;
}
if (oldT->tlsDomain != newT->tlsDomain) {
return true;
}
if (oldT->maskEnabled != newT->maskEnabled) {
return true;
}
if (oldT->tlsEmulation != newT->tlsEmulation) {
return true;
}
if (oldT->useMiddleProxy != newT->useMiddleProxy) {
return true;
}
if (oldT->tag != newT->tag) {
return true;
}
const QString oldUser = oldT->userName.isEmpty()
? QString::fromUtf8(protocols::telemt::defaultUserName)
: oldT->userName;
const QString newUser = newT->userName.isEmpty()
? QString::fromUtf8(protocols::telemt::defaultUserName)
: newT->userName;
if (oldUser != newUser) {
return true;
}
}
}
if (container == DockerContainer::Socks5Proxy) {
return true;
}
@@ -654,7 +774,7 @@ ErrorCode InstallController::isUserInSudo(const ServerCredentials &credentials,
return ErrorCode::ServerUserDirectoryNotAccessible;
if (stdOut.contains("sudoers") || stdOut.contains("is not allowed to run sudo on"))
return ErrorCode::ServerUserNotAllowedInSudoers;
if (stdOut.contains("password is required"))
if (stdOut.contains("password is required") || stdOut.contains("authentication is required"))
return ErrorCode::ServerUserPasswordRequired;
return error;
@@ -823,6 +943,8 @@ QScopedPointer<InstallerBase> InstallController::createInstaller(DockerContainer
case DockerContainer::TorWebSite: return QScopedPointer<InstallerBase>(new TorInstaller(this));
case DockerContainer::Sftp: return QScopedPointer<InstallerBase>(new SftpInstaller(this));
case DockerContainer::Socks5Proxy: return QScopedPointer<InstallerBase>(new Socks5Installer(this));
case DockerContainer::MtProxy: return QScopedPointer<InstallerBase>(new MtProxyInstaller(this));
case DockerContainer::Telemt: return QScopedPointer<InstallerBase>(new TelemtInstaller(this));
default: return QScopedPointer<InstallerBase>(new InstallerBase(this));
}
}
@@ -861,6 +983,20 @@ bool InstallController::isUpdateDockerContainerRequired(DockerContainer containe
return false;
}
}
} else if (container == DockerContainer::MtProxy) {
const auto *oldMt = oldConfig.getMtProxyProtocolConfig();
const auto *newMt = newConfig.getMtProxyProtocolConfig();
if (!oldMt || !newMt) {
return true;
}
return !oldMt->equalsDockerDeploymentSettings(*newMt);
} else if (container == DockerContainer::Telemt) {
const auto *oldT = oldConfig.getTelemtProtocolConfig();
const auto *newT = newConfig.getTelemtProtocolConfig();
if (!oldT || !newT) {
return true;
}
return !oldT->equalsDockerDeploymentSettings(*newT);
}
return true;
@@ -1164,6 +1300,56 @@ void InstallController::updateContainerConfigAfterInstallation(DockerContainer c
onion.replace("\n", "");
torProtocolConfig->serverConfig.site = onion;
}
} else if (container == DockerContainer::MtProxy) {
if (auto* mtProxyConfig = containerConfig.getMtProxyProtocolConfig()) {
qDebug() << "amnezia mtproxy" << stdOut;
static const QRegularExpression reSecret(
QStringLiteral(R"(\[\*\]\s+Secret:\s+([0-9a-fA-F]{32}))"),
QRegularExpression::CaseInsensitiveOption);
static const QRegularExpression reTgLink(QStringLiteral(R"(\[\*\]\s+tg://\s+link:\s+(tg://proxy\?[^\s]+))"));
static const QRegularExpression reTmeLink(
QStringLiteral(R"(\[\*\]\s+t\.me\s+link:\s+(https://t\.me/proxy\?[^\s]+))"));
const QRegularExpressionMatch mSecret = reSecret.match(stdOut);
const QRegularExpressionMatch mTgLink = reTgLink.match(stdOut);
const QRegularExpressionMatch mTmeLink = reTmeLink.match(stdOut);
if (mSecret.hasMatch()) {
mtProxyConfig->secret = mSecret.captured(1);
}
if (mTgLink.hasMatch()) {
mtProxyConfig->tgLink = mTgLink.captured(1);
}
if (mTmeLink.hasMatch()) {
mtProxyConfig->tmeLink = mTmeLink.captured(1);
}
}
} else if (container == DockerContainer::Telemt) {
if (auto *telemtConfig = containerConfig.getTelemtProtocolConfig()) {
qDebug() << "amnezia-telemt configure stdout" << stdOut;
static const QRegularExpression reSecret(
QStringLiteral(R"(\[\*\]\s+Secret:\s+([0-9a-fA-F]{32}))"),
QRegularExpression::CaseInsensitiveOption);
static const QRegularExpression reTgLink(QStringLiteral(R"(\[\*\]\s+tg://\s+link:\s+(tg://proxy\?[^\s]+))"));
static const QRegularExpression reTmeLink(
QStringLiteral(R"(\[\*\]\s+t\.me\s+link:\s+(https://t\.me/proxy\?[^\s]+))"));
const QRegularExpressionMatch mSecret = reSecret.match(stdOut);
const QRegularExpressionMatch mTgLink = reTgLink.match(stdOut);
const QRegularExpressionMatch mTmeLink = reTmeLink.match(stdOut);
if (mSecret.hasMatch()) {
telemtConfig->secret = mSecret.captured(1);
}
if (mTgLink.hasMatch()) {
telemtConfig->tgLink = mTgLink.captured(1);
}
if (mTmeLink.hasMatch()) {
telemtConfig->tmeLink = mTmeLink.captured(1);
}
}
}
}
@@ -1248,3 +1434,126 @@ ErrorCode InstallController::getAlreadyInstalledContainers(const ServerCredentia
return ErrorCode::NoError;
}
ErrorCode InstallController::setDockerContainerEnabledState(const QString &serverId, DockerContainer container, bool enabled)
{
if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt) {
return ErrorCode::InternalError;
}
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
if (!adminConfig.has_value()) {
return ErrorCode::InternalError;
}
ServerCredentials credentials = adminConfig->credentials();
if (!credentials.isValid()) {
return ErrorCode::InternalError;
}
const QString containerName = ContainerUtils::containerToString(container);
SshSession sshSession(this);
const QString script = enabled ? QStringLiteral("sudo docker start %1").arg(containerName)
: QStringLiteral("sudo docker stop %1").arg(containerName);
const ErrorCode runError = sshSession.runScript(credentials, script);
if (runError != ErrorCode::NoError) {
return runError;
}
ContainerConfig currentConfig = adminConfig->containerConfig(container);
bool persist = false;
if (auto *mtConfig = currentConfig.getMtProxyProtocolConfig()) {
mtConfig->isEnabled = enabled;
persist = true;
} else if (auto *telemtConfig = currentConfig.getTelemtProtocolConfig()) {
telemtConfig->isEnabled = enabled;
persist = true;
}
if (persist) {
adminConfig->updateContainerConfig(container, currentConfig);
m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin);
}
return ErrorCode::NoError;
}
ErrorCode InstallController::queryDockerContainerStatus(const QString &serverId, DockerContainer container, int &statusOut)
{
statusOut = 3;
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
if (!adminConfig.has_value()) {
return ErrorCode::InternalError;
}
ServerCredentials credentials = adminConfig->credentials();
if (!credentials.isValid()) {
return ErrorCode::InternalError;
}
const QString containerName = ContainerUtils::containerToString(container);
QString stdOut;
auto cbReadStdOut = [&](const QString &data, libssh::Client &) {
stdOut += data;
return ErrorCode::NoError;
};
SshSession sshSession(this);
const QString script = QStringLiteral(
"sudo docker inspect --format '{{.State.Status}}' %1 2>/dev/null || echo 'not_found'")
.arg(containerName);
const ErrorCode errorCode = sshSession.runScript(credentials, script, cbReadStdOut);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
const QString status = stdOut.trimmed();
if (status == QLatin1String("running")) {
statusOut = 1;
} else if (status == QLatin1String("not_found") || status.isEmpty()) {
statusOut = 0;
} else if (status == QLatin1String("exited") || status == QLatin1String("created")
|| status == QLatin1String("paused")) {
statusOut = 2;
} else {
statusOut = 3;
}
return ErrorCode::NoError;
}
ErrorCode InstallController::queryMtProxyDiagnostics(const QString &serverId, DockerContainer container, int listenPort,
MtProxyContainerDiagnostics &out)
{
out = {};
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(this);
return MtProxyInstaller::queryDiagnostics(sshSession, credentials, container, listenPort, out);
}
QString InstallController::fetchDockerContainerSecret(const QString &serverId, DockerContainer container)
{
if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt) {
return {};
}
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
if (!adminConfig.has_value()) {
return {};
}
ServerCredentials credentials = adminConfig->credentials();
if (!credentials.isValid()) {
return {};
}
const QString containerName = ContainerUtils::containerToString(container);
QString stdOut;
auto cbReadStdOut = [&](const QString &data, libssh::Client &) {
stdOut += data;
return ErrorCode::NoError;
};
SshSession sshSession(this);
const QString path = QStringLiteral("/data/secret");
const QString cmd = QStringLiteral("sudo docker exec %1 cat %2").arg(containerName, path);
const ErrorCode errorCode = sshSession.runScript(credentials, cmd, cbReadStdOut);
if (errorCode != ErrorCode::NoError) {
return {};
}
const QString secret = stdOut.trimmed();
static const QRegularExpression hex32(QStringLiteral("^[0-9a-fA-F]{32}$"));
return hex32.match(secret).hasMatch() ? secret : QString();
}

View File

@@ -16,6 +16,7 @@
#include "core/models/containerConfig.h"
#include "core/repositories/secureServersRepository.h"
#include "core/repositories/secureAppSettingsRepository.h"
#include "core/installers/mtProxyInstaller.h"
class SshSession;
class InstallerBase;
@@ -39,6 +40,16 @@ public:
ErrorCode removeAllContainers(const QString &serverId);
ErrorCode removeContainer(const QString &serverId, DockerContainer container);
ErrorCode setDockerContainerEnabledState(const QString &serverId, DockerContainer container, bool enabled);
/// statusOut: 0 = not deployed, 1 = running, 2 = stopped, 3 = error
ErrorCode queryDockerContainerStatus(const QString &serverId, DockerContainer container, int &statusOut);
ErrorCode queryMtProxyDiagnostics(const QString &serverId, DockerContainer container, int listenPort,
MtProxyContainerDiagnostics &out);
QString fetchDockerContainerSecret(const QString &serverId, DockerContainer container);
ContainerConfig generateConfig(DockerContainer container, int port, TransportProto transportProto);
ErrorCode getAlreadyInstalledContainers(const ServerCredentials &credentials, QMap<DockerContainer, ContainerConfig> &installedContainers, SshSession &sshSession);

View File

@@ -0,0 +1,16 @@
#ifndef CONTAINERDIAGNOSTICS_H
#define CONTAINERDIAGNOSTICS_H
namespace amnezia
{
struct ContainerDiagnostics
{
bool available = false;
bool portReachable = false;
virtual ~ContainerDiagnostics() = default;
};
} // namespace amnezia
#endif // CONTAINERDIAGNOSTICS_H

View File

@@ -0,0 +1,18 @@
#ifndef MTPROXYDIAGNOSTICS_H
#define MTPROXYDIAGNOSTICS_H
#include "containerDiagnostics.h"
#include <QString>
namespace amnezia {
struct MtProxyDiagnostics : ContainerDiagnostics {
bool upstreamReachable = false;
int clientsConnected = -1;
QString lastConfigRefresh;
QString statsEndpoint;
};
} // namespace amnezia
#endif // MTPROXYDIAGNOSTICS_H

View File

@@ -0,0 +1,20 @@
#ifndef TELEMTDIAGNOSTICS_H
#define TELEMTDIAGNOSTICS_H
#include "containerDiagnostics.h"
#include <QString>
namespace amnezia
{
struct TelemtDiagnostics : ContainerDiagnostics
{
bool upstreamReachable = false;
int clientsConnected = -1;
QString lastConfigRefresh;
QString statsEndpoint;
};
} // namespace amnezia
#endif // TELEMTDIAGNOSTICS_H

View File

@@ -14,6 +14,8 @@
#include "core/models/protocols/xrayProtocolConfig.h"
#include "core/models/protocols/sftpProtocolConfig.h"
#include "core/models/protocols/socks5ProxyProtocolConfig.h"
#include "core/models/protocols/mtProxyProtocolConfig.h"
#include "core/models/protocols/telemtProtocolConfig.h"
#include "core/models/protocols/ikev2ProtocolConfig.h"
#include "core/models/protocols/torProtocolConfig.h"
@@ -91,6 +93,18 @@ ContainerConfig InstallerBase::createBaseConfig(DockerContainer container, int p
config.protocolConfig = socks5Config;
break;
}
case Proto::MtProxy: {
MtProxyProtocolConfig mtConfig;
mtConfig.port = portStr;
config.protocolConfig = mtConfig;
break;
}
case Proto::Telemt: {
TelemtProtocolConfig telemtConfig;
telemtConfig.port = portStr;
config.protocolConfig = telemtConfig;
break;
}
case Proto::Ikev2: {
Ikev2ProtocolConfig ikev2Config;
config.protocolConfig = ikev2Config;

View File

@@ -0,0 +1,130 @@
#include "mtProxyInstaller.h"
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
#include "core/utils/protocolEnum.h"
#include "core/utils/selfhosted/sshSession.h"
#include "core/models/containerConfig.h"
#include "core/models/protocols/mtProxyProtocolConfig.h"
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>
#include <QRegularExpression>
#include <QtGlobal>
using namespace amnezia;
namespace {
constexpr QLatin1String kMtProxyClientJsonPath("/data/amnezia-mtproxy-client.json");
constexpr QLatin1String kMtProxyClientJsonUploadPath("data/amnezia-mtproxy-client.json");
constexpr QLatin1String kMtProxySecretPath("/data/secret");
}
MtProxyInstaller::MtProxyInstaller(QObject *parent)
: InstallerBase(parent) {
}
ErrorCode MtProxyInstaller::extractConfigFromContainer(DockerContainer container, const ServerCredentials &credentials,
SshSession *sshSession, ContainerConfig &config) {
if (container != DockerContainer::MtProxy || !sshSession) {
return ErrorCode::NoError;
}
MtProxyProtocolConfig *mt = config.getMtProxyProtocolConfig();
if (!mt) {
return ErrorCode::NoError;
}
ErrorCode jsonErr = ErrorCode::NoError;
const QByteArray jsonRaw =
sshSession->getTextFileFromContainer(container, credentials, QString(kMtProxyClientJsonPath), jsonErr);
if (jsonErr == ErrorCode::NoError && !jsonRaw.trimmed().isEmpty()) {
QJsonParseError parseError;
const QJsonDocument doc = QJsonDocument::fromJson(jsonRaw.trimmed(), &parseError);
if (parseError.error == QJsonParseError::NoError && doc.isObject()) {
QJsonObject merged = mt->toJson();
const QJsonObject snap = doc.object();
for (auto it = snap.constBegin(); it != snap.constEnd(); ++it) {
merged.insert(it.key(), it.value());
}
*mt = MtProxyProtocolConfig::fromJson(merged);
}
}
ErrorCode secretErr = ErrorCode::NoError;
const QByteArray secretRaw =
sshSession->getTextFileFromContainer(container, credentials, QString(kMtProxySecretPath), secretErr);
const QString sec = QString::fromUtf8(secretRaw).trimmed();
if (sec.length() == 32) {
static const QRegularExpression hex32(QStringLiteral("^[0-9a-fA-F]{32}$"));
if (hex32.match(sec).hasMatch()) {
mt->secret = sec;
}
}
return ErrorCode::NoError;
}
ErrorCode MtProxyInstaller::queryDiagnostics(SshSession &sshSession, const ServerCredentials &credentials,
DockerContainer container, int listenPort,
MtProxyContainerDiagnostics &out)
{
out = {};
if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt) {
return ErrorCode::InternalError;
}
const QString containerName = ContainerUtils::containerToString(container);
const QString script =
QStringLiteral(
"PORT_OK=$(sudo docker exec %1 sh -c 'ss -tlnp 2>/dev/null | grep -q :%2 && echo yes || echo no' 2>/dev/null || echo no); "
"TG_OK=$(curl -s --max-time 5 -o /dev/null -w '%%{http_code}' https://core.telegram.org/getProxySecret 2>/dev/null | grep -q '200' && echo yes || echo no); "
"CLIENTS=$(sudo docker exec amnezia-mtproxy sh -c 'curl -s --max-time 3 http://localhost:2398/stats 2>/dev/null | grep -o \"total_special_connections:[0-9]*\" | cut -d: -f2' 2>/dev/null); "
"CONF_TIME=$(sudo docker exec amnezia-mtproxy sh -c 'stat -c \"%%y\" /data/proxy-multi.conf 2>/dev/null | cut -d. -f1' 2>/dev/null || echo unknown); "
"echo \"PORT_OK=${PORT_OK}\"; "
"echo \"TG_OK=${TG_OK}\"; "
"echo \"CLIENTS=${CLIENTS:-0}\"; "
"echo \"CONF_TIME=${CONF_TIME}\"; "
"echo \"STATS=http://localhost:2398/stats\";")
.arg(containerName)
.arg(listenPort);
QString stdOut;
auto cbReadStdOut = [&](const QString &data, libssh::Client &) {
stdOut += data;
return ErrorCode::NoError;
};
const ErrorCode errorCode = sshSession.runScript(credentials, script, cbReadStdOut);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
for (const QString &line : stdOut.split('\n', Qt::SkipEmptyParts)) {
if (line.startsWith(QLatin1String("PORT_OK="))) {
out.portReachable = line.mid(8).trimmed() == QLatin1String("yes");
} else if (line.startsWith(QLatin1String("TG_OK="))) {
out.upstreamReachable = line.mid(6).trimmed() == QLatin1String("yes");
} else if (line.startsWith(QLatin1String("CLIENTS="))) {
out.clientsConnected = line.mid(8).trimmed().toInt();
} else if (line.startsWith(QLatin1String("CONF_TIME="))) {
out.lastConfigRefresh = line.mid(10).trimmed();
} else if (line.startsWith(QLatin1String("STATS="))) {
out.statsEndpoint = line.mid(6).trimmed();
}
}
return ErrorCode::NoError;
}
void MtProxyInstaller::uploadClientSettingsSnapshot(SshSession &sshSession, const ServerCredentials &credentials,
DockerContainer container, const ContainerConfig &config) {
const MtProxyProtocolConfig *mt = config.getMtProxyProtocolConfig();
if (!mt) {
return;
}
const QByteArray payload = QJsonDocument(mt->toJson()).toJson(QJsonDocument::Compact);
const ErrorCode err = sshSession.uploadTextFileToContainer(container, credentials, QString::fromUtf8(payload),
QString(kMtProxyClientJsonUploadPath));
if (err != ErrorCode::NoError) {
qWarning() << "MtProxyInstaller::uploadClientSettingsSnapshot failed" << err;
}
}

View File

@@ -0,0 +1,34 @@
#ifndef MTPROXYINSTALLER_H
#define MTPROXYINSTALLER_H
#include "installerBase.h"
#include <QString>
struct MtProxyContainerDiagnostics {
bool portReachable = false;
bool upstreamReachable = false;
int clientsConnected = -1;
QString lastConfigRefresh;
QString statsEndpoint;
};
class MtProxyInstaller : public InstallerBase {
Q_OBJECT
public:
explicit MtProxyInstaller(QObject *parent = nullptr);
amnezia::ErrorCode
extractConfigFromContainer(amnezia::DockerContainer container, const amnezia::ServerCredentials &credentials,
SshSession *sshSession, amnezia::ContainerConfig &config) override;
static void uploadClientSettingsSnapshot(SshSession &sshSession, const amnezia::ServerCredentials &credentials,
amnezia::DockerContainer container,
const amnezia::ContainerConfig &config);
static amnezia::ErrorCode queryDiagnostics(SshSession &sshSession, const amnezia::ServerCredentials &credentials,
amnezia::DockerContainer container, int listenPort,
MtProxyContainerDiagnostics &out);
};
#endif // MTPROXYINSTALLER_H

View File

@@ -0,0 +1,79 @@
#include "telemtInstaller.h"
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
#include "core/utils/selfhosted/sshSession.h"
#include "core/models/containerConfig.h"
#include "core/models/protocols/telemtProtocolConfig.h"
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>
#include <QRegularExpression>
#include <QtGlobal>
using namespace amnezia;
namespace {
constexpr QLatin1String kTelemtClientJsonPath("/data/amnezia-telemt-client.json");
constexpr QLatin1String kTelemtClientJsonUploadPath("data/amnezia-telemt-client.json");
constexpr QLatin1String kTelemtSecretPath("/data/secret");
}
TelemtInstaller::TelemtInstaller(QObject *parent) : InstallerBase(parent) {}
ErrorCode TelemtInstaller::extractConfigFromContainer(DockerContainer container, const ServerCredentials &credentials,
SshSession *sshSession, ContainerConfig &config) {
if (container != DockerContainer::Telemt || !sshSession) {
return ErrorCode::NoError;
}
TelemtProtocolConfig *tc = config.getTelemtProtocolConfig();
if (!tc) {
return ErrorCode::NoError;
}
ErrorCode jsonErr = ErrorCode::NoError;
const QByteArray jsonRaw =
sshSession->getTextFileFromContainer(container, credentials, QString(kTelemtClientJsonPath), jsonErr);
if (jsonErr == ErrorCode::NoError && !jsonRaw.trimmed().isEmpty()) {
QJsonParseError parseError;
const QJsonDocument doc = QJsonDocument::fromJson(jsonRaw.trimmed(), &parseError);
if (parseError.error == QJsonParseError::NoError && doc.isObject()) {
QJsonObject merged = tc->toJson();
const QJsonObject snap = doc.object();
for (auto it = snap.constBegin(); it != snap.constEnd(); ++it) {
merged.insert(it.key(), it.value());
}
*tc = TelemtProtocolConfig::fromJson(merged);
}
}
ErrorCode secretErr = ErrorCode::NoError;
const QByteArray secretRaw =
sshSession->getTextFileFromContainer(container, credentials, QString(kTelemtSecretPath), secretErr);
const QString sec = QString::fromUtf8(secretRaw).trimmed();
if (sec.length() == 32) {
static const QRegularExpression hex32(QStringLiteral("^[0-9a-fA-F]{32}$"));
if (hex32.match(sec).hasMatch()) {
tc->secret = sec;
}
}
return ErrorCode::NoError;
}
void TelemtInstaller::uploadClientSettingsSnapshot(SshSession &sshSession, const ServerCredentials &credentials,
DockerContainer container, const ContainerConfig &config) {
const TelemtProtocolConfig *tc = config.getTelemtProtocolConfig();
if (!tc) {
return;
}
const QByteArray payload = QJsonDocument(tc->toJson()).toJson(QJsonDocument::Compact);
const ErrorCode err = sshSession.uploadTextFileToContainer(container, credentials, QString::fromUtf8(payload),
QString(kTelemtClientJsonUploadPath));
if (err != ErrorCode::NoError) {
qWarning() << "TelemtInstaller::uploadClientSettingsSnapshot failed" << err;
}
}

View File

@@ -0,0 +1,20 @@
#ifndef TELEMTINSTALLER_H
#define TELEMTINSTALLER_H
#include "installerBase.h"
class TelemtInstaller : public InstallerBase {
Q_OBJECT
public:
explicit TelemtInstaller(QObject *parent = nullptr);
amnezia::ErrorCode
extractConfigFromContainer(amnezia::DockerContainer container, const amnezia::ServerCredentials &credentials,
SshSession *sshSession, amnezia::ContainerConfig &config) override;
static void uploadClientSettingsSnapshot(SshSession &sshSession, const amnezia::ServerCredentials &credentials,
amnezia::DockerContainer container,
const amnezia::ContainerConfig &config);
};
#endif // TELEMTINSTALLER_H

View File

@@ -14,8 +14,18 @@
#include "core/models/protocols/xrayProtocolConfig.h"
#include "logger.h"
namespace {
namespace
{
Logger logger("XrayInstaller");
// Xray expects uTLS preset names (chrome, firefox, …). Old Amnezia/server templates used "Mozilla/5.0".
QString normalizeXrayFingerprint(const QString &fp)
{
if (fp.isEmpty() || fp.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)) {
return QString::fromLatin1(protocols::xray::defaultFingerprint);
}
return fp;
}
}
using namespace amnezia;
@@ -63,18 +73,251 @@ ErrorCode XrayInstaller::extractConfigFromContainer(DockerContainer container, c
}
QJsonObject streamSettings = inbound[protocols::xray::streamSettings].toObject();
QJsonObject realitySettings = streamSettings[protocols::xray::realitySettings].toObject();
if (!realitySettings.contains(protocols::xray::serverNames)) {
logger.error() << "Settings missing 'serverNames' field";
auto *xrayConfig = config.getXrayProtocolConfig();
if (!xrayConfig) {
logger.error() << "No XrayProtocolConfig in ContainerConfig";
return ErrorCode::InternalError;
}
QString siteName = realitySettings[protocols::xray::serverNames][0].toString();
XrayServerConfig &srv = xrayConfig->serverConfig;
if (auto* xrayConfig = config.getXrayProtocolConfig()) {
xrayConfig->serverConfig.site = siteName;
// ── 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) {
alpnList << v.toString();
}
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();
{
const QJsonObject hdrs = xhttpObj.value("headers").toObject();
if (hdrs.contains(QLatin1String("Host")) || !hdrs.isEmpty())
srv.xhttp.headersTemplate = QStringLiteral("HTTP");
}
if (xhttpObj.contains(QLatin1String("uplinkHTTPMethod")))
srv.xhttp.uplinkMethod = xhttpObj.value("uplinkHTTPMethod").toString();
else
srv.xhttp.uplinkMethod = xhttpObj.value("method").toString();
srv.xhttp.disableGrpc = xhttpObj.value("noGRPCHeader").toBool(true);
srv.xhttp.disableSse = xhttpObj.value("noSSEHeader").toBool(true);
auto sessionSeqUi = [](const QString &core) -> QString {
if (core.isEmpty() || core == QLatin1String("path"))
return QStringLiteral("Path");
if (core == QLatin1String("cookie"))
return QStringLiteral("Cookie");
if (core == QLatin1String("header"))
return QStringLiteral("Header");
if (core == QLatin1String("query"))
return QStringLiteral("Query");
return core;
};
QString sess = xhttpObj.value("sessionPlacement").toString();
if (sess.isEmpty())
sess = xhttpObj.value("scSessionPlacement").toString();
srv.xhttp.sessionPlacement = sessionSeqUi(sess);
QString seq = xhttpObj.value("seqPlacement").toString();
if (seq.isEmpty())
seq = xhttpObj.value("scSeqPlacement").toString();
srv.xhttp.seqPlacement = sessionSeqUi(seq);
auto uplinkDataUi = [](const QString &core) -> QString {
if (core.isEmpty() || core == QLatin1String("body"))
return QStringLiteral("Body");
if (core == QLatin1String("auto"))
return QStringLiteral("Auto");
if (core == QLatin1String("header"))
return QStringLiteral("Header");
if (core == QLatin1String("cookie"))
return QStringLiteral("Cookie");
return core;
};
QString udata = xhttpObj.value("uplinkDataPlacement").toString();
if (udata.isEmpty())
udata = xhttpObj.value("scUplinkDataPlacement").toString();
srv.xhttp.uplinkDataPlacement = uplinkDataUi(udata);
srv.xhttp.sessionKey = xhttpObj.value("sessionKey").toString();
srv.xhttp.seqKey = xhttpObj.value("seqKey").toString();
srv.xhttp.uplinkDataKey = xhttpObj.value("uplinkDataKey").toString();
if (xhttpObj.contains(QLatin1String("uplinkChunkSize"))) {
QJsonObject uc = xhttpObj.value("uplinkChunkSize").toObject();
if (!uc.isEmpty())
srv.xhttp.uplinkChunkSize = QString::number(uc.value("from").toInt());
} else if (xhttpObj.contains(QLatin1String("xhttpUplinkChunkSize"))) {
srv.xhttp.uplinkChunkSize = QString::number(xhttpObj.value("xhttpUplinkChunkSize").toInt());
}
if (xhttpObj.contains(QLatin1String("scMaxBufferedPosts"))) {
srv.xhttp.scMaxBufferedPosts = QString::number(xhttpObj.value("scMaxBufferedPosts").toVariant().toLongLong());
}
auto readRange = [&](const char *key, QString &minOut, QString &maxOut) {
QJsonObject r = xhttpObj.value(QLatin1String(key)).toObject();
if (!r.isEmpty()) {
minOut = QString::number(r.value("from").toInt());
maxOut = QString::number(r.value("to").toInt());
}
};
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();
QJsonObject bytesRange = pad.value("xPaddingBytes").toObject();
if (!bytesRange.isEmpty()) {
srv.xhttp.xPadding.bytesMin = QString::number(bytesRange.value("from").toInt());
srv.xhttp.xPadding.bytesMax = QString::number(bytesRange.value("to").toInt());
}
QString pl = srv.xhttp.xPadding.placement.toLower();
if (pl == QLatin1String("cookie"))
srv.xhttp.xPadding.placement = QStringLiteral("Cookie");
else if (pl == QLatin1String("header"))
srv.xhttp.xPadding.placement = QStringLiteral("Header");
else if (pl == QLatin1String("query"))
srv.xhttp.xPadding.placement = QStringLiteral("Query");
else if (pl == QLatin1String("queryinheader"))
srv.xhttp.xPadding.placement = QStringLiteral("Query in header");
QString met = srv.xhttp.xPadding.method.toLower();
if (met == QLatin1String("repeat-x"))
srv.xhttp.xPadding.method = QStringLiteral("Repeat-x");
else if (met == QLatin1String("tokenish"))
srv.xhttp.xPadding.method = QStringLiteral("Tokenish");
};
if (xhttpObj.contains(QLatin1String("xPaddingObfsMode")) || xhttpObj.contains(QLatin1String("xPaddingKey"))
|| !xhttpObj.value("xPaddingBytes").toObject().isEmpty()) {
loadPaddingFromObject(xhttpObj);
} else if (xhttpObj.contains(QLatin1String("xPadding")) && xhttpObj.value("xPadding").isObject()) {
const QJsonObject nested = xhttpObj.value("xPadding").toObject();
if (!nested.isEmpty()) {
loadPaddingFromObject(nested);
if (!nested.contains(QLatin1String("xPaddingObfsMode")))
srv.xhttp.xPadding.obfsMode = true;
}
}
if (xhttpObj.contains(QLatin1String("xmux"))) {
QJsonObject mux = xhttpObj.value("xmux").toObject();
srv.xhttp.xmux.enabled = true;
auto readMuxRange = [&](const char *key, QString &minOut, QString &maxOut) {
QJsonObject r = mux.value(QLatin1String(key)).toObject();
if (!r.isEmpty()) {
minOut = QString::number(r.value("from").toInt());
maxOut = QString::number(r.value("to").toInt());
}
};
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);
}
return ErrorCode::NoError;
}

View File

@@ -113,6 +113,26 @@ const Socks5ProxyProtocolConfig* ContainerConfig::getSocks5ProxyProtocolConfig()
return protocolConfig.as<Socks5ProxyProtocolConfig>();
}
MtProxyProtocolConfig* ContainerConfig::getMtProxyProtocolConfig()
{
return protocolConfig.as<MtProxyProtocolConfig>();
}
const MtProxyProtocolConfig* ContainerConfig::getMtProxyProtocolConfig() const
{
return protocolConfig.as<MtProxyProtocolConfig>();
}
TelemtProtocolConfig* ContainerConfig::getTelemtProtocolConfig()
{
return protocolConfig.as<TelemtProtocolConfig>();
}
const TelemtProtocolConfig* ContainerConfig::getTelemtProtocolConfig() const
{
return protocolConfig.as<TelemtProtocolConfig>();
}
Ikev2ProtocolConfig* ContainerConfig::getIkev2ProtocolConfig()
{
return protocolConfig.as<Ikev2ProtocolConfig>();

View File

@@ -57,6 +57,12 @@ struct ContainerConfig {
Socks5ProxyProtocolConfig* getSocks5ProxyProtocolConfig();
const Socks5ProxyProtocolConfig* getSocks5ProxyProtocolConfig() const;
MtProxyProtocolConfig* getMtProxyProtocolConfig();
const MtProxyProtocolConfig* getMtProxyProtocolConfig() const;
TelemtProtocolConfig* getTelemtProtocolConfig();
const TelemtProtocolConfig* getTelemtProtocolConfig() const;
Ikev2ProtocolConfig* getIkev2ProtocolConfig();
const Ikev2ProtocolConfig* getIkev2ProtocolConfig() const;

View File

@@ -9,6 +9,8 @@
#include "core/utils/protocolEnum.h"
#include "core/models/protocols/ikev2ProtocolConfig.h"
#include "core/models/protocols/dnsProtocolConfig.h"
#include "core/models/protocols/mtProxyProtocolConfig.h"
#include "core/models/protocols/telemtProtocolConfig.h"
namespace amnezia
{
@@ -38,6 +40,10 @@ Proto ProtocolConfig::type() const
return Proto::TorWebSite;
} else if constexpr (std::is_same_v<T, DnsProtocolConfig>) {
return Proto::Dns;
} else if constexpr (std::is_same_v<T, MtProxyProtocolConfig>) {
return Proto::MtProxy;
} else if constexpr (std::is_same_v<T, TelemtProtocolConfig>) {
return Proto::Telemt;
}
return Proto::Unknown;
}, data);
@@ -65,6 +71,10 @@ QString ProtocolConfig::port() const
return QString();
} else if constexpr (std::is_same_v<T, DnsProtocolConfig>) {
return QString();
} else if constexpr (std::is_same_v<T, MtProxyProtocolConfig>) {
return arg.port.isEmpty() ? QString(protocols::mtProxy::defaultPort) : arg.port;
} else if constexpr (std::is_same_v<T, TelemtProtocolConfig>) {
return arg.port.isEmpty() ? QString(protocols::telemt::defaultPort) : arg.port;
}
return QString();
}, data);
@@ -88,6 +98,10 @@ QString ProtocolConfig::transportProto() const
return QString();
} else if constexpr (std::is_same_v<T, DnsProtocolConfig>) {
return QString();
} else if constexpr (std::is_same_v<T, MtProxyProtocolConfig>) {
return QStringLiteral("tcp");
} else if constexpr (std::is_same_v<T, TelemtProtocolConfig>) {
return QStringLiteral("tcp");
}
return QString();
}, data);
@@ -299,6 +313,10 @@ ProtocolConfig ProtocolConfig::fromJson(const QJsonObject& json, Proto type)
return ProtocolConfig{TorProtocolConfig::fromJson(json)};
case Proto::Dns:
return ProtocolConfig{DnsProtocolConfig::fromJson(json)};
case Proto::MtProxy:
return ProtocolConfig{MtProxyProtocolConfig::fromJson(json)};
case Proto::Telemt:
return ProtocolConfig{TelemtProtocolConfig::fromJson(json)};
default:
return ProtocolConfig{AwgProtocolConfig{}};
}

View File

@@ -22,6 +22,8 @@
#include "core/models/protocols/ikev2ProtocolConfig.h"
#include "core/models/protocols/torProtocolConfig.h"
#include "core/models/protocols/dnsProtocolConfig.h"
#include "core/models/protocols/mtProxyProtocolConfig.h"
#include "core/models/protocols/telemtProtocolConfig.h"
namespace amnezia
{
@@ -36,6 +38,8 @@ struct ProtocolConfig {
XrayProtocolConfig,
SftpProtocolConfig,
Socks5ProxyProtocolConfig,
MtProxyProtocolConfig,
TelemtProtocolConfig,
Ikev2ProtocolConfig,
TorProtocolConfig,
DnsProtocolConfig

View File

@@ -0,0 +1,147 @@
#include "mtProxyProtocolConfig.h"
#include "../../../core/utils/protocolEnum.h"
#include "../../../core/protocols/protocolUtils.h"
#include "../../../core/utils/constants/configKeys.h"
#include "../../../core/utils/constants/protocolConstants.h"
#include <QJsonArray>
#include <algorithm>
using namespace amnezia;
namespace amnezia {
QJsonObject MtProxyProtocolConfig::toJson() const {
QJsonObject obj;
if (!port.isEmpty()) {
obj[configKey::port] = port;
}
if (!secret.isEmpty()) {
obj[protocols::mtProxy::secretKey] = secret;
}
if (!tag.isEmpty()) {
obj[protocols::mtProxy::tagKey] = tag;
}
if (!tgLink.isEmpty()) {
obj[protocols::mtProxy::tgLinkKey] = tgLink;
}
if (!tmeLink.isEmpty()) {
obj[protocols::mtProxy::tmeLinkKey] = tmeLink;
}
obj[protocols::mtProxy::isEnabledKey] = isEnabled;
if (!publicHost.isEmpty()) {
obj[protocols::mtProxy::publicHostKey] = publicHost;
}
if (!transportMode.isEmpty()) {
obj[protocols::mtProxy::transportModeKey] = transportMode;
}
if (!tlsDomain.isEmpty()) {
obj[protocols::mtProxy::tlsDomainKey] = tlsDomain;
}
if (!additionalSecrets.isEmpty()) {
obj[protocols::mtProxy::additionalSecretsKey] = QJsonArray::fromStringList(additionalSecrets);
}
if (!workersMode.isEmpty()) {
obj[protocols::mtProxy::workersModeKey] = workersMode;
}
if (!workers.isEmpty()) {
obj[protocols::mtProxy::workersKey] = workers;
}
obj[protocols::mtProxy::natEnabledKey] = natEnabled;
if (!natInternalIp.isEmpty()) {
obj[protocols::mtProxy::natInternalIpKey] = natInternalIp;
}
if (!natExternalIp.isEmpty()) {
obj[protocols::mtProxy::natExternalIpKey] = natExternalIp;
}
return obj;
}
MtProxyProtocolConfig MtProxyProtocolConfig::fromJson(const QJsonObject &json) {
MtProxyProtocolConfig config;
config.port = json.value(configKey::port).toString();
config.secret = json.value(protocols::mtProxy::secretKey).toString();
config.tag = json.value(protocols::mtProxy::tagKey).toString();
config.tgLink = json.value(protocols::mtProxy::tgLinkKey).toString();
config.tmeLink = json.value(protocols::mtProxy::tmeLinkKey).toString();
config.isEnabled = json.value(protocols::mtProxy::isEnabledKey).toBool(true);
config.publicHost = json.value(protocols::mtProxy::publicHostKey).toString();
config.transportMode = json.value(protocols::mtProxy::transportModeKey).toString();
config.tlsDomain = json.value(protocols::mtProxy::tlsDomainKey).toString();
for (const auto &v: json.value(protocols::mtProxy::additionalSecretsKey).toArray()) {
const QString s = v.toString();
if (!s.isEmpty()) {
config.additionalSecrets.append(s);
}
}
config.workersMode = json.value(protocols::mtProxy::workersModeKey).toString();
config.workers = json.value(protocols::mtProxy::workersKey).toString();
config.natEnabled = json.value(protocols::mtProxy::natEnabledKey).toBool(false);
config.natInternalIp = json.value(protocols::mtProxy::natInternalIpKey).toString();
config.natExternalIp = json.value(protocols::mtProxy::natExternalIpKey).toString();
return config;
}
bool MtProxyProtocolConfig::equalsDockerDeploymentSettings(const MtProxyProtocolConfig &other) const {
const auto normPort = [](const QString &p) {
return p.isEmpty() ? QString(protocols::mtProxy::defaultPort) : p;
};
const auto normTransport = [](const QString &t) {
return t.isEmpty() ? QString(protocols::mtProxy::transportModeStandard) : t;
};
const auto normWorkersMode = [](const QString &m) {
return m.isEmpty() ? QString(protocols::mtProxy::workersModeAuto) : m;
};
if (normPort(port) != normPort(other.port)) {
return false;
}
if (normTransport(transportMode) != normTransport(other.transportMode)) {
return false;
}
if (tlsDomain != other.tlsDomain) {
return false;
}
if (secret != other.secret) {
return false;
}
if (tag != other.tag) {
return false;
}
if (publicHost != other.publicHost) {
return false;
}
if (normWorkersMode(workersMode) != normWorkersMode(other.workersMode)) {
return false;
}
if (workers != other.workers) {
return false;
}
if (natEnabled != other.natEnabled) {
return false;
}
if (natInternalIp != other.natInternalIp) {
return false;
}
if (natExternalIp != other.natExternalIp) {
return false;
}
if (isEnabled != other.isEnabled) {
return false;
}
QStringList aa = additionalSecrets;
QStringList bb = other.additionalSecrets;
aa.removeAll(QString());
bb.removeAll(QString());
std::sort(aa.begin(), aa.end());
std::sort(bb.begin(), bb.end());
return aa == bb;
}
} // namespace amnezia

View File

@@ -0,0 +1,38 @@
#ifndef MTPROXYPROTOCOLCONFIG_H
#define MTPROXYPROTOCOLCONFIG_H
#include <QJsonObject>
#include <QString>
#include <QStringList>
namespace amnezia {
struct MtProxyProtocolConfig {
QString port;
QString secret;
QString tag;
QString tgLink;
QString tmeLink;
bool isEnabled = true;
QString publicHost;
QString transportMode;
QString tlsDomain;
QStringList additionalSecrets;
QString workersMode;
QString workers;
bool natEnabled = false;
QString natInternalIp;
QString natExternalIp;
QJsonObject toJson() const;
static MtProxyProtocolConfig fromJson(const QJsonObject &json);
// Port, transport, TLS, secrets, NAT, workers, isEnabled, additionalSecrets (order-independent).
// Ignores tgLink / tmeLink (derived / display).
bool equalsDockerDeploymentSettings(const MtProxyProtocolConfig &other) const;
};
} // namespace amnezia
#endif // MTPROXYPROTOCOLCONFIG_H

View File

@@ -0,0 +1,162 @@
#include "telemtProtocolConfig.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/constants/protocolConstants.h"
#include <QJsonArray>
#include <algorithm>
using namespace amnezia;
QJsonObject TelemtProtocolConfig::toJson() const
{
QJsonObject obj;
if (!port.isEmpty()) {
obj[QString(configKey::port)] = port;
}
if (!secret.isEmpty()) {
obj[protocols::telemt::secretKey] = secret;
}
if (!tag.isEmpty()) {
obj[protocols::telemt::tagKey] = tag;
}
if (!tgLink.isEmpty()) {
obj[protocols::telemt::tgLinkKey] = tgLink;
}
if (!tmeLink.isEmpty()) {
obj[protocols::telemt::tmeLinkKey] = tmeLink;
}
obj[protocols::telemt::isEnabledKey] = isEnabled;
if (!publicHost.isEmpty()) {
obj[protocols::telemt::publicHostKey] = publicHost;
}
if (!transportMode.isEmpty()) {
obj[protocols::telemt::transportModeKey] = transportMode;
}
if (!tlsDomain.isEmpty()) {
obj[protocols::telemt::tlsDomainKey] = tlsDomain;
}
obj[protocols::telemt::maskEnabledKey] = maskEnabled;
obj[protocols::telemt::tlsEmulationKey] = tlsEmulation;
obj[protocols::telemt::useMiddleProxyKey] = useMiddleProxy;
if (!userName.isEmpty()) {
obj[protocols::telemt::userNameKey] = userName;
}
if (!additionalSecrets.isEmpty()) {
obj[protocols::telemt::additionalSecretsKey] = QJsonArray::fromStringList(additionalSecrets);
}
if (!workersMode.isEmpty()) {
obj[protocols::telemt::workersModeKey] = workersMode;
}
if (!workers.isEmpty()) {
obj[protocols::telemt::workersKey] = workers;
}
obj[protocols::telemt::natEnabledKey] = natEnabled;
if (!natInternalIp.isEmpty()) {
obj[protocols::telemt::natInternalIpKey] = natInternalIp;
}
if (!natExternalIp.isEmpty()) {
obj[protocols::telemt::natExternalIpKey] = natExternalIp;
}
return obj;
}
TelemtProtocolConfig TelemtProtocolConfig::fromJson(const QJsonObject &json)
{
TelemtProtocolConfig c;
c.port = json.value(QString(configKey::port)).toString();
c.secret = json.value(protocols::telemt::secretKey).toString();
c.tag = json.value(protocols::telemt::tagKey).toString();
c.tgLink = json.value(protocols::telemt::tgLinkKey).toString();
c.tmeLink = json.value(protocols::telemt::tmeLinkKey).toString();
c.isEnabled = json.value(protocols::telemt::isEnabledKey).toBool(true);
c.publicHost = json.value(protocols::telemt::publicHostKey).toString();
c.transportMode = json.value(protocols::telemt::transportModeKey).toString();
c.tlsDomain = json.value(protocols::telemt::tlsDomainKey).toString();
c.maskEnabled = json.value(protocols::telemt::maskEnabledKey).toBool(true);
c.tlsEmulation = json.value(protocols::telemt::tlsEmulationKey).toBool(false);
c.useMiddleProxy = json.value(protocols::telemt::useMiddleProxyKey).toBool(true);
c.userName = json.value(protocols::telemt::userNameKey).toString();
for (const auto &v : json.value(protocols::telemt::additionalSecretsKey).toArray()) {
const QString s = v.toString();
if (!s.isEmpty()) {
c.additionalSecrets.append(s);
}
}
c.workersMode = json.value(protocols::telemt::workersModeKey).toString();
c.workers = json.value(protocols::telemt::workersKey).toString();
c.natEnabled = json.value(protocols::telemt::natEnabledKey).toBool(false);
c.natInternalIp = json.value(protocols::telemt::natInternalIpKey).toString();
c.natExternalIp = json.value(protocols::telemt::natExternalIpKey).toString();
return c;
}
bool TelemtProtocolConfig::equalsDockerDeploymentSettings(const TelemtProtocolConfig &other) const
{
const auto normPort = [](const QString &p) {
return p.isEmpty() ? QString(protocols::telemt::defaultPort) : p;
};
const auto normTransport = [](const QString &t) {
return t.isEmpty() ? QString(protocols::telemt::transportModeStandard) : t;
};
const auto normWorkersMode = [](const QString &m) {
return m.isEmpty() ? QString(protocols::telemt::workersModeAuto) : m;
};
if (normPort(port) != normPort(other.port)) {
return false;
}
if (normTransport(transportMode) != normTransport(other.transportMode)) {
return false;
}
if (tlsDomain != other.tlsDomain) {
return false;
}
if (secret != other.secret) {
return false;
}
if (tag != other.tag) {
return false;
}
if (publicHost != other.publicHost) {
return false;
}
if (maskEnabled != other.maskEnabled) {
return false;
}
if (tlsEmulation != other.tlsEmulation) {
return false;
}
if (useMiddleProxy != other.useMiddleProxy) {
return false;
}
if (userName != other.userName) {
return false;
}
if (normWorkersMode(workersMode) != normWorkersMode(other.workersMode)) {
return false;
}
if (workers != other.workers) {
return false;
}
if (natEnabled != other.natEnabled) {
return false;
}
if (natInternalIp != other.natInternalIp) {
return false;
}
if (natExternalIp != other.natExternalIp) {
return false;
}
if (isEnabled != other.isEnabled) {
return false;
}
QStringList aa = additionalSecrets;
QStringList bb = other.additionalSecrets;
aa.removeAll(QString());
bb.removeAll(QString());
std::sort(aa.begin(), aa.end());
std::sort(bb.begin(), bb.end());
return aa == bb;
}

View File

@@ -0,0 +1,38 @@
#ifndef TELEMTPROTOCOLCONFIG_H
#define TELEMTPROTOCOLCONFIG_H
#include <QJsonObject>
#include <QString>
#include <QStringList>
namespace amnezia {
struct TelemtProtocolConfig {
QString port;
QString secret;
QString tag;
QString tgLink;
QString tmeLink;
bool isEnabled = true;
QString publicHost;
QString transportMode;
QString tlsDomain;
bool maskEnabled = true;
bool tlsEmulation = false;
bool useMiddleProxy = true;
QString userName;
QStringList additionalSecrets;
QString workersMode;
QString workers;
bool natEnabled = false;
QString natInternalIp;
QString natExternalIp;
QJsonObject toJson() const;
static TelemtProtocolConfig fromJson(const QJsonObject &json);
bool equalsDockerDeploymentSettings(const TelemtProtocolConfig &other) const;
};
} // namespace amnezia
#endif // TELEMTPROTOCOLCONFIG_H

View File

@@ -3,20 +3,173 @@
#include <QJsonDocument>
#include <QJsonArray>
#include "../../../core/utils/protocolEnum.h"
#include "../../../core/protocols/protocolUtils.h"
#include "../../../core/utils/constants/configKeys.h"
#include "../../../core/utils/constants/protocolConstants.h"
#include "core/utils/protocolEnum.h"
#include "core/protocols/protocolUtils.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/constants/protocolConstants.h"
using namespace amnezia;
using namespace ProtocolUtils;
namespace amnezia
{
QJsonObject XrayXPaddingConfig::toJson() const
{
QJsonObject obj;
if (!bytesMin.isEmpty()) obj[configKey::xPaddingBytesMin] = bytesMin;
if (!bytesMax.isEmpty()) obj[configKey::xPaddingBytesMax] = bytesMax;
obj[configKey::xPaddingObfsMode] = obfsMode;
if (!key.isEmpty()) obj[configKey::xPaddingKey] = key;
if (!header.isEmpty()) obj[configKey::xPaddingHeader] = header;
if (!placement.isEmpty()) obj[configKey::xPaddingPlacement] = placement;
if (!method.isEmpty()) obj[configKey::xPaddingMethod] = method;
return obj;
}
XrayXPaddingConfig XrayXPaddingConfig::fromJson(const QJsonObject &json)
{
XrayXPaddingConfig c;
c.bytesMin = json.value(configKey::xPaddingBytesMin).toString();
c.bytesMax = json.value(configKey::xPaddingBytesMax).toString();
c.obfsMode = json.value(configKey::xPaddingObfsMode).toBool(true);
c.key = json.value(configKey::xPaddingKey).toString(protocols::xray::defaultSite);
c.header = json.value(configKey::xPaddingHeader).toString();
c.placement = json.value(configKey::xPaddingPlacement).toString(protocols::xray::defaultXPaddingPlacement);
c.method = json.value(configKey::xPaddingMethod).toString(protocols::xray::defaultXPaddingMethod);
return c;
}
QJsonObject XrayXmuxConfig::toJson() const
{
QJsonObject obj;
obj[configKey::xmuxEnabled] = enabled;
if (!maxConcurrencyMin.isEmpty()) obj[configKey::xmuxMaxConcurrencyMin] = maxConcurrencyMin;
if (!maxConcurrencyMax.isEmpty()) obj[configKey::xmuxMaxConcurrencyMax] = maxConcurrencyMax;
if (!maxConnectionsMin.isEmpty()) obj[configKey::xmuxMaxConnectionsMin] = maxConnectionsMin;
if (!maxConnectionsMax.isEmpty()) obj[configKey::xmuxMaxConnectionsMax] = maxConnectionsMax;
if (!cMaxReuseTimesMin.isEmpty()) obj[configKey::xmuxCMaxReuseTimesMin] = cMaxReuseTimesMin;
if (!cMaxReuseTimesMax.isEmpty()) obj[configKey::xmuxCMaxReuseTimesMax] = cMaxReuseTimesMax;
if (!hMaxRequestTimesMin.isEmpty()) obj[configKey::xmuxHMaxRequestTimesMin] = hMaxRequestTimesMin;
if (!hMaxRequestTimesMax.isEmpty()) obj[configKey::xmuxHMaxRequestTimesMax] = hMaxRequestTimesMax;
if (!hMaxReusableSecsMin.isEmpty()) obj[configKey::xmuxHMaxReusableSecsMin] = hMaxReusableSecsMin;
if (!hMaxReusableSecsMax.isEmpty()) obj[configKey::xmuxHMaxReusableSecsMax] = hMaxReusableSecsMax;
if (!hKeepAlivePeriod.isEmpty()) obj[configKey::xmuxHKeepAlivePeriod] = hKeepAlivePeriod;
return obj;
}
XrayXmuxConfig XrayXmuxConfig::fromJson(const QJsonObject &json)
{
XrayXmuxConfig c;
c.enabled = json.value(configKey::xmuxEnabled).toBool(true);
c.maxConcurrencyMin = json.value(configKey::xmuxMaxConcurrencyMin).toString("0");
c.maxConcurrencyMax = json.value(configKey::xmuxMaxConcurrencyMax).toString("0");
c.maxConnectionsMin = json.value(configKey::xmuxMaxConnectionsMin).toString("0");
c.maxConnectionsMax = json.value(configKey::xmuxMaxConnectionsMax).toString("0");
c.cMaxReuseTimesMin = json.value(configKey::xmuxCMaxReuseTimesMin).toString("0");
c.cMaxReuseTimesMax = json.value(configKey::xmuxCMaxReuseTimesMax).toString("0");
c.hMaxRequestTimesMin = json.value(configKey::xmuxHMaxRequestTimesMin).toString("0");
c.hMaxRequestTimesMax = json.value(configKey::xmuxHMaxRequestTimesMax).toString("0");
c.hMaxReusableSecsMin = json.value(configKey::xmuxHMaxReusableSecsMin).toString("0");
c.hMaxReusableSecsMax = json.value(configKey::xmuxHMaxReusableSecsMax).toString("0");
c.hKeepAlivePeriod = json.value(configKey::xmuxHKeepAlivePeriod).toString();
return c;
}
QJsonObject XrayXhttpConfig::toJson() const
{
QJsonObject obj;
if (!mode.isEmpty()) obj[configKey::xhttpMode] = mode;
if (!host.isEmpty()) obj[configKey::xhttpHost] = host;
if (!path.isEmpty()) obj[configKey::xhttpPath] = path;
if (!headersTemplate.isEmpty()) obj[configKey::xhttpHeadersTemplate] = headersTemplate;
if (!uplinkMethod.isEmpty()) obj[configKey::xhttpUplinkMethod] = uplinkMethod;
obj[configKey::xhttpDisableGrpc] = disableGrpc;
obj[configKey::xhttpDisableSse] = disableSse;
if (!sessionPlacement.isEmpty()) obj[configKey::xhttpSessionPlacement] = sessionPlacement;
if (!sessionKey.isEmpty()) obj[configKey::xhttpSessionKey] = sessionKey;
if (!seqPlacement.isEmpty()) obj[configKey::xhttpSeqPlacement] = seqPlacement;
if (!seqKey.isEmpty()) obj[configKey::xhttpSeqKey] = seqKey;
if (!uplinkDataPlacement.isEmpty()) obj[configKey::xhttpUplinkDataPlacement] = uplinkDataPlacement;
if (!uplinkDataKey.isEmpty()) obj[configKey::xhttpUplinkDataKey] = uplinkDataKey;
if (!uplinkChunkSize.isEmpty()) obj[configKey::xhttpUplinkChunkSize] = uplinkChunkSize;
if (!scMaxBufferedPosts.isEmpty()) obj[configKey::xhttpScMaxBufferedPosts] = scMaxBufferedPosts;
if (!scMaxEachPostBytesMin.isEmpty()) obj[configKey::xhttpScMaxEachPostBytesMin] = scMaxEachPostBytesMin;
if (!scMaxEachPostBytesMax.isEmpty()) obj[configKey::xhttpScMaxEachPostBytesMax] = scMaxEachPostBytesMax;
if (!scMinPostsIntervalMsMin.isEmpty()) obj[configKey::xhttpScMinPostsIntervalMsMin] = scMinPostsIntervalMsMin;
if (!scMinPostsIntervalMsMax.isEmpty()) obj[configKey::xhttpScMinPostsIntervalMsMax] = scMinPostsIntervalMsMax;
if (!scStreamUpServerSecsMin.isEmpty()) obj[configKey::xhttpScStreamUpServerSecsMin] = scStreamUpServerSecsMin;
if (!scStreamUpServerSecsMax.isEmpty()) obj[configKey::xhttpScStreamUpServerSecsMax] = scStreamUpServerSecsMax;
obj["xPadding"] = xPadding.toJson();
obj["xmux"] = xmux.toJson();
return obj;
}
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);
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();
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");
c.xPadding = XrayXPaddingConfig::fromJson(json.value("xPadding").toObject());
c.xmux = XrayXmuxConfig::fromJson(json.value("xmux").toObject());
return c;
}
QJsonObject XrayMkcpConfig::toJson() const
{
QJsonObject obj;
if (!tti.isEmpty()) obj[configKey::mkcpTti] = tti;
if (!uplinkCapacity.isEmpty()) obj[configKey::mkcpUplinkCapacity] = uplinkCapacity;
if (!downlinkCapacity.isEmpty()) obj[configKey::mkcpDownlinkCapacity] = downlinkCapacity;
if (!readBufferSize.isEmpty()) obj[configKey::mkcpReadBufferSize] = readBufferSize;
if (!writeBufferSize.isEmpty()) obj[configKey::mkcpWriteBufferSize] = writeBufferSize;
obj[configKey::mkcpCongestion] = congestion;
return obj;
}
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);
return c;
}
QJsonObject XrayServerConfig::toJson() const
{
QJsonObject obj;
// Existing fields
if (!port.isEmpty()) {
obj[configKey::port] = port;
}
@@ -29,60 +182,96 @@ QJsonObject XrayServerConfig::toJson() const
if (!site.isEmpty()) {
obj[configKey::site] = site;
}
if (isThirdPartyConfig) {
obj[configKey::isThirdPartyConfig] = isThirdPartyConfig;
}
// New: Security
if (!security.isEmpty()) {
obj[configKey::xraySecurity] = security;
}
if (!flow.isEmpty()) {
obj[configKey::xrayFlow] = flow;
}
if (!fingerprint.isEmpty()) {
obj[configKey::xrayFingerprint] = fingerprint;
}
if (!sni.isEmpty()) {
obj[configKey::xraySni] = sni;
}
if (!alpn.isEmpty()) {
obj[configKey::xrayAlpn] = alpn;
}
// New: Transport
if (!transport.isEmpty()) {
obj[configKey::xrayTransport] = transport;
}
obj["xhttp"] = xhttp.toJson();
obj["mkcp"] = mkcp.toJson();
return obj;
}
XrayServerConfig XrayServerConfig::fromJson(const QJsonObject& json)
XrayServerConfig XrayServerConfig::fromJson(const QJsonObject &json)
{
XrayServerConfig config;
config.port = json.value(configKey::port).toString();
config.transportProto = json.value(configKey::transportProto).toString();
config.subnetAddress = json.value(configKey::subnetAddress).toString();
config.site = json.value(configKey::site).toString();
config.isThirdPartyConfig = json.value(configKey::isThirdPartyConfig).toBool(false);
return config;
XrayServerConfig c;
// Existing fields
c.port = json.value(configKey::port).toString();
c.transportProto = json.value(configKey::transportProto).toString();
c.subnetAddress = json.value(configKey::subnetAddress).toString();
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);
}
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;
}
bool XrayServerConfig::hasEqualServerSettings(const XrayServerConfig& other) const
bool XrayServerConfig::hasEqualServerSettings(const XrayServerConfig &other) const
{
return port == other.port && site == other.site;
return port == other.port
&& site == other.site
&& security == other.security
&& flow == other.flow
&& transport == other.transport
&& fingerprint == other.fingerprint
&& sni == other.sni;
}
QJsonObject XrayClientConfig::toJson() const
{
QJsonObject obj;
if (!nativeConfig.isEmpty()) {
obj[configKey::config] = nativeConfig;
}
if (!localPort.isEmpty()) {
obj[configKey::localPort] = localPort;
}
if (!id.isEmpty()) {
obj[configKey::clientId] = id;
}
if (!nativeConfig.isEmpty()) obj[configKey::config] = nativeConfig;
if (!localPort.isEmpty()) obj[configKey::localPort] = localPort;
if (!id.isEmpty()) obj[configKey::clientId] = id;
return obj;
}
XrayClientConfig XrayClientConfig::fromJson(const QJsonObject& json)
XrayClientConfig XrayClientConfig::fromJson(const QJsonObject &json)
{
XrayClientConfig config;
config.nativeConfig = json.value(configKey::config).toString();
config.localPort = json.value(configKey::localPort).toString();
config.id = json.value(configKey::clientId).toString();
if (config.id.isEmpty() && !config.nativeConfig.isEmpty()) {
QJsonDocument doc = QJsonDocument::fromJson(config.nativeConfig.toUtf8());
XrayClientConfig c;
c.nativeConfig = json.value(configKey::config).toString();
c.localPort = json.value(configKey::localPort).toString();
c.id = json.value(configKey::clientId).toString();
if (c.id.isEmpty() && !c.nativeConfig.isEmpty()) {
QJsonDocument doc = QJsonDocument::fromJson(c.nativeConfig.toUtf8());
if (!doc.isNull() && doc.isObject()) {
QJsonObject configObj = doc.object();
if (configObj.contains(protocols::xray::outbounds)) {
@@ -100,7 +289,7 @@ XrayClientConfig XrayClientConfig::fromJson(const QJsonObject& json)
if (!users.isEmpty()) {
QJsonObject user = users[0].toObject();
if (user.contains(protocols::xray::id)) {
config.id = user[protocols::xray::id].toString();
c.id = user[protocols::xray::id].toString();
}
}
}
@@ -111,16 +300,15 @@ XrayClientConfig XrayClientConfig::fromJson(const QJsonObject& json)
}
}
}
return config;
return c;
}
QJsonObject XrayProtocolConfig::toJson() const
{
QJsonObject obj = serverConfig.toJson();
if (clientConfig.has_value()) {
// Third-party import: nativeConfig is raw Xray JSON (inbounds/outbounds)
QJsonDocument doc = QJsonDocument::fromJson(clientConfig->nativeConfig.toUtf8());
if (!doc.isNull() && doc.isObject() && doc.object().contains(protocols::xray::outbounds)
&& !doc.object().contains(configKey::config)) {
@@ -130,22 +318,20 @@ QJsonObject XrayProtocolConfig::toJson() const
obj[configKey::lastConfig] = QString::fromUtf8(QJsonDocument(clientJson).toJson(QJsonDocument::Compact));
}
}
return obj;
}
XrayProtocolConfig XrayProtocolConfig::fromJson(const QJsonObject& json)
XrayProtocolConfig XrayProtocolConfig::fromJson(const QJsonObject &json)
{
XrayProtocolConfig config;
config.serverConfig = XrayServerConfig::fromJson(json);
XrayProtocolConfig c;
c.serverConfig = XrayServerConfig::fromJson(json);
QString lastConfigStr = json.value(configKey::lastConfig).toString();
if (!lastConfigStr.isEmpty()) {
QJsonDocument doc = QJsonDocument::fromJson(lastConfigStr.toUtf8());
if (doc.isObject()) {
QJsonObject parsed = doc.object();
// Third-party import stores raw Xray config (inbounds/outbounds) directly
if (parsed.contains(protocols::xray::outbounds) && !parsed.contains(configKey::config)) {
XrayClientConfig clientCfg;
clientCfg.nativeConfig = lastConfigStr;
@@ -158,14 +344,14 @@ XrayProtocolConfig XrayProtocolConfig::fromJson(const QJsonObject& json)
}
}
}
config.clientConfig = clientCfg;
c.clientConfig = clientCfg;
} else {
config.clientConfig = XrayClientConfig::fromJson(parsed);
c.clientConfig = XrayClientConfig::fromJson(parsed);
}
}
}
return config;
return c;
}
bool XrayProtocolConfig::hasClientConfig() const
@@ -173,7 +359,7 @@ bool XrayProtocolConfig::hasClientConfig() const
return clientConfig.has_value();
}
void XrayProtocolConfig::setClientConfig(const XrayClientConfig& config)
void XrayProtocolConfig::setClientConfig(const XrayClientConfig &config)
{
clientConfig = config;
}
@@ -184,4 +370,3 @@ void XrayProtocolConfig::clearClientConfig()
}
} // namespace amnezia

View File

@@ -2,47 +2,145 @@
#define XRAYPROTOCOLCONFIG_H
#include <QJsonObject>
#include "core/utils/constants/protocolConstants.h"
#include <QString>
#include <optional>
namespace amnezia
{
// ── xPadding ─────────────────────────────────────────────────────────────────
struct XrayXPaddingConfig {
QString bytesMin; // xPaddingBytes min
QString bytesMax; // xPaddingBytes max
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
QJsonObject toJson() const;
static XrayXPaddingConfig fromJson(const QJsonObject &json);
};
// ── xmux ─────────────────────────────────────────────────────────────────────
struct XrayXmuxConfig {
bool enabled = true;
QString maxConcurrencyMin = "0";
QString maxConcurrencyMax = "0";
QString maxConnectionsMin = "0";
QString maxConnectionsMax = "0";
QString cMaxReuseTimesMin = "0";
QString cMaxReuseTimesMax = "0";
QString hMaxRequestTimesMin = "0";
QString hMaxRequestTimesMax = "0";
QString hMaxReusableSecsMin = "0";
QString hMaxReusableSecsMax = "0";
QString hKeepAlivePeriod;
QJsonObject toJson() const;
static XrayXmuxConfig fromJson(const QJsonObject &json);
};
// ── XHTTP transport ───────────────────────────────────────────────────────────
struct XrayXhttpConfig {
QString mode = protocols::xray::defaultXhttpMode; // Auto|Packet-up|Stream-up|Stream-one
QString host = protocols::xray::defaultXhttpHost;
QString path;
QString headersTemplate = protocols::xray::defaultXhttpHeadersTemplate; // HTTP|None
QString uplinkMethod = protocols::xray::defaultXhttpUplinkMethod; // POST|PUT|PATCH
bool disableGrpc = true;
bool disableSse = true;
// Session & Sequence
QString sessionPlacement = protocols::xray::defaultXhttpSessionPlacement;
QString sessionKey = protocols::xray::defaultXhttpSessionKey;
QString seqPlacement = protocols::xray::defaultXhttpSeqPlacement;
QString seqKey;
QString uplinkDataPlacement = protocols::xray::defaultXhttpUplinkDataPlacement;
QString uplinkDataKey;
// Traffic Shaping
QString uplinkChunkSize = protocols::xray::defaultXhttpUplinkChunkSize;
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;
XrayXPaddingConfig xPadding;
XrayXmuxConfig xmux;
QJsonObject toJson() const;
static XrayXhttpConfig fromJson(const QJsonObject &json);
};
// ── mKCP transport ────────────────────────────────────────────────────────────
struct XrayMkcpConfig {
QString tti;
QString uplinkCapacity;
QString downlinkCapacity;
QString readBufferSize;
QString writeBufferSize;
bool congestion = true;
QJsonObject toJson() const;
static XrayMkcpConfig fromJson(const QJsonObject &json);
};
// ── Server config (settings editable by user) ─────────────────────────────────
struct XrayServerConfig {
QString port;
QString transportProto;
QString subnetAddress;
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;
// New: Transport
QString transport = protocols::xray::defaultTransport;
XrayXhttpConfig xhttp;
XrayMkcpConfig mkcp;
QJsonObject toJson() const;
static XrayServerConfig fromJson(const QJsonObject& json);
bool hasEqualServerSettings(const XrayServerConfig& other) const;
static XrayServerConfig fromJson(const QJsonObject &json);
bool hasEqualServerSettings(const XrayServerConfig &other) const;
};
// ── Client config (generated, not edited by user) ─────────────────────────────
struct XrayClientConfig {
QString nativeConfig;
QString localPort;
QString id;
QJsonObject toJson() const;
static XrayClientConfig fromJson(const QJsonObject& json);
static XrayClientConfig fromJson(const QJsonObject &json);
};
// ── Top-level protocol config ──────────────────────────────────────────────────
struct XrayProtocolConfig {
XrayServerConfig serverConfig;
std::optional<XrayClientConfig> clientConfig;
QJsonObject toJson() const;
static XrayProtocolConfig fromJson(const QJsonObject& json);
static XrayProtocolConfig fromJson(const QJsonObject &json);
bool hasClientConfig() const;
void setClientConfig(const XrayClientConfig& config);
void setClientConfig(const XrayClientConfig &config);
void clearClientConfig();
};
} // namespace amnezia
#endif // XRAYPROTOCOLCONFIG_H

View File

@@ -68,7 +68,10 @@ QMap<Proto, QString> ProtocolUtils::protocolHumanNames()
{ Proto::TorWebSite, "Website in Tor network" },
{ Proto::Dns, "DNS Service" },
{ Proto::Sftp, QObject::tr("SFTP service") },
{ Proto::Socks5Proxy, QObject::tr("SOCKS5 proxy server") } };
{ Proto::Socks5Proxy, QObject::tr("SOCKS5 proxy server") },
{ Proto::MtProxy, QObject::tr("MTProxy (Telegram)") },
{ Proto::Telemt, QObject::tr("Telemt (Telegram)") },
};
}
QMap<Proto, QString> ProtocolUtils::protocolDescriptions()
@@ -92,6 +95,8 @@ ServiceType ProtocolUtils::protocolService(Proto p)
case Proto::Dns: return ServiceType::Other;
case Proto::Sftp: return ServiceType::Other;
case Proto::Socks5Proxy: return ServiceType::Other;
case Proto::MtProxy: return ServiceType::Other;
case Proto::Telemt: return ServiceType::Other;
default: return ServiceType::Other;
}
}
@@ -104,6 +109,8 @@ int ProtocolUtils::getPortForInstall(Proto p)
case OpenVpn:
case Socks5Proxy:
return QRandomGenerator::global()->bounded(30000, 50000);
case MtProxy:
case Telemt:
default:
return defaultPort(p);
}
@@ -123,6 +130,8 @@ int ProtocolUtils::defaultPort(Proto p)
case Proto::Dns: return 53;
case Proto::Sftp: return 222;
case Proto::Socks5Proxy: return 38080;
case Proto::MtProxy: return QString(protocols::mtProxy::defaultPort).toInt();
case Proto::Telemt: return QString(protocols::telemt::defaultPort).toInt();
default: return -1;
}
}
@@ -141,6 +150,8 @@ bool ProtocolUtils::defaultPortChangeable(Proto p)
case Proto::Dns: return false;
case Proto::Sftp: return true;
case Proto::Socks5Proxy: return true;
case Proto::MtProxy: return true;
case Proto::Telemt: return true;
default: return false;
}
}
@@ -161,6 +172,8 @@ TransportProto ProtocolUtils::defaultTransportProto(Proto p)
case Proto::Dns: return TransportProto::Udp;
case Proto::Sftp: return TransportProto::Tcp;
case Proto::Socks5Proxy: return TransportProto::Tcp;
case Proto::MtProxy: return TransportProto::Tcp;
case Proto::Telemt: return TransportProto::Tcp;
default: return TransportProto::Udp;
}
}
@@ -180,9 +193,10 @@ bool ProtocolUtils::defaultTransportProtoChangeable(Proto p)
case Proto::Dns: return false;
case Proto::Sftp: return false;
case Proto::Socks5Proxy: return false;
case Proto::MtProxy: return false;
case Proto::Telemt: return false;
default: return false;
}
return false;
}
QString ProtocolUtils::key_proto_config_data(Proto p)
@@ -208,4 +222,3 @@ QString ProtocolUtils::getProtocolVersionString(const QJsonObject &protocolConfi
if (version == protocols::awg::awgV1_5) return QObject::tr(" (version 1.5)");
return "";
}

48
client/core/protocols/xrayProtocol.cpp Executable file → Normal file
View File

@@ -2,6 +2,7 @@
#include "core/protocols/protocolUtils.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/constants/protocolConstants.h"
#include "core/utils/ipcClient.h"
#include "core/utils/networkUtilities.h"
#include "core/utils/serialization/serialization.h"
@@ -9,6 +10,7 @@
#include <QCryptographicHash>
#include <QJsonDocument>
#include <QTimer>
#include <QJsonObject>
#include <QNetworkInterface>
#include <QtCore/qlogging.h>
@@ -79,12 +81,29 @@ ErrorCode XrayProtocol::start()
m_socksPassword = creds.password;
m_socksPort = creds.port;
const QString xrayConfigStr = QJsonDocument(m_xrayConfig).toJson(QJsonDocument::Compact);
QString xrayConfigStr = QJsonDocument(m_xrayConfig).toJson(QJsonDocument::Compact);
if (xrayConfigStr.isEmpty()) {
qCritical() << "Xray config is empty";
return ErrorCode::XrayExecutableCrashed;
}
// Fix fingerprint: old configs may contain "Mozilla/5.0" which xray-core rejects.
// Replace with the correct default at runtime so stale stored configs still work.
if (xrayConfigStr.contains("Mozilla/5.0", Qt::CaseInsensitive)) {
xrayConfigStr.replace("Mozilla/5.0", amnezia::protocols::xray::defaultFingerprint,
Qt::CaseInsensitive);
qDebug() << "XrayProtocol: patched legacy fingerprint to"
<< amnezia::protocols::xray::defaultFingerprint;
}
// Fix inbound listen address: old configs may use "10.33.0.2" which doesn't exist
// until TUN is created. xray must listen on 127.0.0.1 so tun2socks can connect.
if (xrayConfigStr.contains(amnezia::protocols::xray::defaultLocalAddr)) {
xrayConfigStr.replace(amnezia::protocols::xray::defaultLocalAddr,
amnezia::protocols::xray::defaultLocalListenAddr);
qDebug() << "XrayProtocol: patched legacy inbound listen address to 127.0.0.1";
}
return IpcClient::withInterface(
[&](QSharedPointer<IpcInterfaceReplica> iface) {
auto xrayStart = iface->xrayStart(xrayConfigStr);
@@ -188,6 +207,33 @@ ErrorCode XrayProtocol::startTun2Socks()
connect(
m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::finished, this,
[this](int exitCode, QProcess::ExitStatus exitStatus) {
// Check stdout for "resource busy" — the TUN device was not yet released
// by the previous tun2socks instance. Retry after a short delay.
bool resourceBusy = false;
if (m_tun2socksProcess) {
auto readOut = m_tun2socksProcess->readAllStandardOutput();
if (readOut.waitForFinished()) {
resourceBusy = readOut.returnValue().contains("resource busy");
}
}
if (resourceBusy && m_tun2socksRetryCount < maxTun2SocksRetries) {
m_tun2socksRetryCount++;
qWarning() << QString("Tun2socks: TUN resource busy, retrying (%1/%2) in %3ms...")
.arg(m_tun2socksRetryCount)
.arg(maxTun2SocksRetries)
.arg(tun2socksRetryDelayMs);
QTimer::singleShot(tun2socksRetryDelayMs, this, [this]() {
if (ErrorCode err = startTun2Socks(); err != ErrorCode::NoError) {
stop();
setLastError(err);
}
});
return;
}
m_tun2socksRetryCount = 0;
if (exitStatus == QProcess::ExitStatus::CrashExit) {
qCritical() << "Tun2socks process crashed!";
} else {

View File

@@ -35,6 +35,9 @@ private:
int m_socksPort = 10808;
QSharedPointer<IpcProcessInterfaceReplica> m_tun2socksProcess;
int m_tun2socksRetryCount = 0;
static constexpr int maxTun2SocksRetries = 5;
static constexpr int tun2socksRetryDelayMs = 400;
};
#endif // XRAYPROTOCOL_H

View File

@@ -451,4 +451,12 @@ void SecureAppSettingsRepository::setInstallationUuid(const QString &uuid)
m_settings->setValue("Conf/installationUuid", uuid);
}
QByteArray SecureAppSettingsRepository::xraySavedConfigs() const
{
return value("Xray/savedConfigs").toByteArray();
}
void SecureAppSettingsRepository::setXraySavedConfigs(const QByteArray &data)
{
setValue("Xray/savedConfigs", data);
}

View File

@@ -92,6 +92,9 @@ public:
QString nextAvailableServerName() const;
QByteArray xraySavedConfigs() const;
void setXraySavedConfigs(const QByteArray &data);
signals:
void appLanguageChanged(QLocale locale);
void allowedDnsServersChanged(const QStringList &servers);

View File

@@ -93,6 +93,8 @@ namespace amnezia
constexpr QLatin1String xray("xray");
constexpr QLatin1String ssxray("ssxray");
constexpr QLatin1String socks5proxy("socks5proxy");
constexpr QLatin1String mtproxy("mtproxy");
constexpr QLatin1String telemt("telemt");
constexpr QLatin1String splitTunnelSites("splitTunnelSites");
constexpr QLatin1String splitTunnelType("splitTunnelType");
@@ -124,6 +126,76 @@ namespace amnezia
constexpr QLatin1String dataSent("dataSent");
constexpr QLatin1String storageServerId("storageServerId");
// ── Xray-specific keys ────────────────────────────────────────
// Security
constexpr QLatin1String xraySecurity("xray_security"); // none | tls | reality
constexpr QLatin1String xrayFlow("xray_flow"); // "" | xtls-rprx-vision | xtls-rprx-vision-udp443
constexpr QLatin1String xrayFingerprint("xray_fingerprint"); // Mozilla/5.0 | chrome | firefox | ...
constexpr QLatin1String xraySni("xray_sni"); // Server Name (SNI)
constexpr QLatin1String xrayAlpn("xray_alpn"); // HTTP/2 | HTTP/1.1 | HTTP/2,HTTP/1.1
// Transport — common
constexpr QLatin1String xrayTransport("xray_transport"); // raw | xhttp | mkcp
// Transport — XHTTP
constexpr QLatin1String xhttpMode("xhttp_mode"); // Auto | Packet-up | Stream-up | Stream-one
constexpr QLatin1String xhttpHost("xhttp_host");
constexpr QLatin1String xhttpPath("xhttp_path");
constexpr QLatin1String xhttpHeadersTemplate("xhttp_headers_template"); // HTTP | None
constexpr QLatin1String xhttpUplinkMethod("xhttp_uplink_method"); // POST | PUT | PATCH
constexpr QLatin1String xhttpDisableGrpc("xhttp_disable_grpc"); // bool
constexpr QLatin1String xhttpDisableSse("xhttp_disable_sse"); // bool
// Transport — XHTTP Session & Sequence
constexpr QLatin1String xhttpSessionPlacement("xhttp_session_placement"); // Path | Header | Cookie | None
constexpr QLatin1String xhttpSessionKey("xhttp_session_key");
constexpr QLatin1String xhttpSeqPlacement("xhttp_seq_placement");
constexpr QLatin1String xhttpSeqKey("xhttp_seq_key");
constexpr QLatin1String xhttpUplinkDataPlacement("xhttp_uplink_data_placement"); // Body | Query
constexpr QLatin1String xhttpUplinkDataKey("xhttp_uplink_data_key");
// Transport — XHTTP Traffic Shaping
constexpr QLatin1String xhttpUplinkChunkSize("xhttp_uplink_chunk_size");
constexpr QLatin1String xhttpScMaxBufferedPosts("xhttp_sc_max_buffered_posts");
constexpr QLatin1String xhttpScMaxEachPostBytesMin("xhttp_sc_max_each_post_bytes_min");
constexpr QLatin1String xhttpScMaxEachPostBytesMax("xhttp_sc_max_each_post_bytes_max");
constexpr QLatin1String xhttpScMinPostsIntervalMsMin("xhttp_sc_min_posts_interval_ms_min");
constexpr QLatin1String xhttpScMinPostsIntervalMsMax("xhttp_sc_min_posts_interval_ms_max");
constexpr QLatin1String xhttpScStreamUpServerSecsMin("xhttp_sc_stream_up_server_secs_min");
constexpr QLatin1String xhttpScStreamUpServerSecsMax("xhttp_sc_stream_up_server_secs_max");
// Transport — mKCP
constexpr QLatin1String mkcpTti("mkcp_tti");
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
// xPadding
constexpr QLatin1String xPaddingBytesMin("xpadding_bytes_min");
constexpr QLatin1String xPaddingBytesMax("xpadding_bytes_max");
constexpr QLatin1String xPaddingObfsMode("xpadding_obfs_mode"); // bool
constexpr QLatin1String xPaddingKey("xpadding_key");
constexpr QLatin1String xPaddingHeader("xpadding_header");
constexpr QLatin1String xPaddingPlacement("xpadding_placement"); // Cookie | Header | Query | Body
constexpr QLatin1String xPaddingMethod("xpadding_method"); // Repeat-x | Random | Zero
// xmux
constexpr QLatin1String xmuxEnabled("xmux_enabled"); // bool
constexpr QLatin1String xmuxMaxConcurrencyMin("xmux_max_concurrency_min");
constexpr QLatin1String xmuxMaxConcurrencyMax("xmux_max_concurrency_max");
constexpr QLatin1String xmuxMaxConnectionsMin("xmux_max_connections_min");
constexpr QLatin1String xmuxMaxConnectionsMax("xmux_max_connections_max");
constexpr QLatin1String xmuxCMaxReuseTimesMin("xmux_c_max_reuse_times_min");
constexpr QLatin1String xmuxCMaxReuseTimesMax("xmux_c_max_reuse_times_max");
constexpr QLatin1String xmuxHMaxRequestTimesMin("xmux_h_max_request_times_min");
constexpr QLatin1String xmuxHMaxRequestTimesMax("xmux_h_max_request_times_max");
constexpr QLatin1String xmuxHMaxReusableSecsMin("xmux_h_max_reusable_secs_min");
constexpr QLatin1String xmuxHMaxReusableSecsMax("xmux_h_max_reusable_secs_max");
constexpr QLatin1String xmuxHKeepAlivePeriod("xmux_h_keep_alive_period");
}
}

View File

@@ -3,6 +3,7 @@
namespace amnezia
{
namespace protocols
{
@@ -57,6 +58,40 @@ namespace amnezia
constexpr char defaultPort[] = "443";
constexpr char defaultLocalProxyPort[] = "10808";
constexpr char defaultLocalAddr[] = "10.33.0.2";
constexpr char defaultLocalListenAddr[] = "127.0.0.1";
constexpr char defaultSecurity[] = "reality";
constexpr char defaultFlow[] = "xtls-rprx-vision";
constexpr char defaultTransport[] = "raw";
constexpr char defaultFingerprint[] = "chrome";
constexpr char defaultSni[] = "cdn.example.com";
constexpr char defaultAlpn[] = "HTTP/2";
constexpr char defaultXhttpMode[] = "Auto";
constexpr char defaultXhttpHeadersTemplate[] = "HTTP";
constexpr char defaultXhttpUplinkMethod[] = "POST";
constexpr char defaultXhttpSessionPlacement[] = "Path";
constexpr char defaultXhttpSessionKey[] = "Path";
constexpr char defaultXhttpSeqPlacement[] = "Path";
constexpr char defaultXhttpUplinkDataPlacement[] = "Body";
constexpr char defaultXhttpHost[] = "www.googletagmanager.com";
constexpr char defaultXhttpUplinkChunkSize[] = "0";
constexpr char defaultXhttpScMaxEachPostBytesMin[] = "1";
constexpr char defaultXhttpScMaxEachPostBytesMax[] = "100";
constexpr char defaultXhttpScMinPostsIntervalMsMin[] = "100";
constexpr char defaultXhttpScMinPostsIntervalMsMax[] = "800";
constexpr char defaultXhttpScStreamUpServerSecsMin[] = "1";
constexpr char defaultXhttpScStreamUpServerSecsMax[] = "100";
constexpr char defaultXPaddingPlacement[] = "Cookie";
constexpr char defaultXPaddingMethod[] = "Repeat-x";
constexpr char defaultMkcpTti[] = "50";
constexpr char defaultMkcpUplinkCapacity[] = "5";
constexpr char defaultMkcpDownlinkCapacity[] = "20";
constexpr char defaultMkcpReadBufferSize[] = "2";
constexpr char defaultMkcpWriteBufferSize[] = "2";
constexpr char outbounds[] = "outbounds";
constexpr char inbounds[] = "inbounds";
@@ -174,9 +209,71 @@ namespace amnezia
constexpr char proxyConfigPath[] = "/usr/local/3proxy/conf/3proxy.cfg";
}
namespace mtProxy
{
constexpr char secretKey[] = "mtproxy_secret";
constexpr char tagKey[] = "mtproxy_tag";
constexpr char tgLinkKey[] = "mtproxy_tg_link";
constexpr char tmeLinkKey[] = "mtproxy_tme_link";
constexpr char isEnabledKey[] = "mtproxy_is_enabled";
constexpr char publicHostKey[] = "mtproxy_public_host";
constexpr char transportModeKey[] = "mtproxy_transport_mode";
constexpr char tlsDomainKey[] = "mtproxy_tls_domain";
constexpr char additionalSecretsKey[] = "mtproxy_additional_secrets";
constexpr char workersKey[] = "mtproxy_workers";
constexpr char workersModeKey[] = "mtproxy_workers_mode";
constexpr char natEnabledKey[] = "mtproxy_nat_enabled";
constexpr char natInternalIpKey[] = "mtproxy_nat_internal_ip";
constexpr char natExternalIpKey[] = "mtproxy_nat_external_ip";
constexpr char transportModeStandard[] = "standard";
constexpr char transportModeFakeTLS[] = "faketls";
constexpr char workersModeAuto[] = "auto";
constexpr char workersModeManual[] = "manual";
constexpr char defaultPort[] = "443";
constexpr char defaultWorkers[] = "2";
constexpr int maxWorkers = 32;
constexpr int botTagHexLength = 32;
constexpr char defaultTlsDomain[] = "googletagmanager.com";
}
namespace telemt
{
constexpr char secretKey[] = "telemt_secret";
constexpr char tagKey[] = "telemt_tag";
constexpr char tgLinkKey[] = "telemt_tg_link";
constexpr char tmeLinkKey[] = "telemt_tme_link";
constexpr char isEnabledKey[] = "telemt_is_enabled";
constexpr char publicHostKey[] = "telemt_public_host";
constexpr char transportModeKey[] = "telemt_transport_mode";
constexpr char tlsDomainKey[] = "telemt_tls_domain";
constexpr char maskEnabledKey[] = "telemt_mask_enabled";
constexpr char tlsEmulationKey[] = "telemt_tls_emulation";
constexpr char useMiddleProxyKey[] = "telemt_use_middle_proxy";
constexpr char userNameKey[] = "telemt_user_name";
// Stored for UI only (Telemt server ignores these; same controls as MTProxy page)
constexpr char additionalSecretsKey[] = "telemt_additional_secrets";
constexpr char workersKey[] = "telemt_workers";
constexpr char workersModeKey[] = "telemt_workers_mode";
constexpr char natEnabledKey[] = "telemt_nat_enabled";
constexpr char natInternalIpKey[] = "telemt_nat_internal_ip";
constexpr char natExternalIpKey[] = "telemt_nat_external_ip";
constexpr char transportModeStandard[] = "standard";
constexpr char transportModeFakeTLS[] = "faketls";
constexpr char defaultPort[] = "443";
constexpr char defaultTlsDomain[] = "googletagmanager.com";
constexpr char defaultUserName[] = "amnezia";
constexpr char defaultWorkers[] = "2";
constexpr char workersModeAuto[] = "auto";
constexpr char workersModeManual[] = "manual";
constexpr int maxWorkers = 32;
}
} // namespace protocols
}
#endif // PROTOCOLCONSTANTS_H

View File

@@ -23,7 +23,9 @@ namespace amnezia
TorWebSite,
Dns,
Sftp,
Socks5Proxy
Socks5Proxy,
MtProxy,
Telemt,
};
Q_ENUM_NS(DockerContainer)
} // namespace ContainerEnumNS

View File

@@ -72,7 +72,10 @@ QMap<DockerContainer, QString> ContainerUtils::containerHumanNames()
{ DockerContainer::TorWebSite, QObject::tr("Website in Tor network") },
{ DockerContainer::Dns, QObject::tr("AmneziaDNS") },
{ DockerContainer::Sftp, QObject::tr("SFTP file sharing service") },
{ DockerContainer::Socks5Proxy, QObject::tr("SOCKS5 proxy server") } };
{ DockerContainer::Socks5Proxy, QObject::tr("SOCKS5 proxy server") },
{ DockerContainer::MtProxy, QObject::tr("MTProxy (Telegram)") },
{ DockerContainer::Telemt, QObject::tr("Telemt (Telegram)") },
};
}
QMap<DockerContainer, QString> ContainerUtils::containerDescriptions()
@@ -102,7 +105,12 @@ QMap<DockerContainer, QString> ContainerUtils::containerDescriptions()
{ DockerContainer::Sftp,
QObject::tr("Create a file vault on your server to securely store and transfer files.") },
{ DockerContainer::Socks5Proxy,
QObject::tr("") } };
QObject::tr("") },
{ DockerContainer::MtProxy,
QObject::tr("Telegram MTProto proxy server") },
{ DockerContainer::Telemt,
QObject::tr("Telegram MTProto proxy (Telemt, Rust)") },
};
}
QMap<DockerContainer, QString> ContainerUtils::containerDetailedDescriptions()
@@ -172,7 +180,15 @@ QMap<DockerContainer, QString> ContainerUtils::containerDetailedDescriptions()
"You will be able to access it using\n FileZilla or other SFTP clients, "
"as well as mount the disk on your device to access\n it directly from your device.\n\n"
"For more detailed information, you can\n find it in the support section under \"Create SFTP file storage.\" ") },
{ DockerContainer::Socks5Proxy, QObject::tr("SOCKS5 proxy server") }
{ DockerContainer::Socks5Proxy, QObject::tr("SOCKS5 proxy server") },
{ DockerContainer::MtProxy,
QObject::tr("Telegram MTProto proxy server. "
"Allows Telegram clients to connect through your server "
"using the MTProto protocol. Supports FakeTLS mode for "
"bypassing DPI-based blocking.") },
{ DockerContainer::Telemt,
QObject::tr("Telegram MTProto proxy powered by Telemt (Rust). "
"Supports secure and TLS fronting modes with optional traffic masking.") },
};
}
@@ -197,6 +213,8 @@ Proto ContainerUtils::defaultProtocol(DockerContainer c)
case DockerContainer::Dns: return Proto::Dns;
case DockerContainer::Sftp: return Proto::Sftp;
case DockerContainer::Socks5Proxy: return Proto::Socks5Proxy;
case DockerContainer::MtProxy: return Proto::MtProxy;
case DockerContainer::Telemt: return Proto::Telemt;
default: return Proto::Unknown;
}
}
@@ -224,6 +242,8 @@ bool ContainerUtils::isSupportedByCurrentPlatform(DockerContainer c)
case DockerContainer::Awg: return true;
case DockerContainer::Xray: return true;
case DockerContainer::SSXray: return true;
case DockerContainer::MtProxy: return true;
case DockerContainer::Telemt: return true;
default:
return false;
}
@@ -237,7 +257,8 @@ bool ContainerUtils::isSupportedByCurrentPlatform(DockerContainer c)
case DockerContainer::Awg: return true;
case DockerContainer::Xray: return true;
case DockerContainer::SSXray: return true;
return false;
case DockerContainer::MtProxy: return true;
case DockerContainer::Telemt: return true;
default:
return false;
}
@@ -256,6 +277,8 @@ bool ContainerUtils::isSupportedByCurrentPlatform(DockerContainer c)
case DockerContainer::Awg: return true;
case DockerContainer::Xray: return true;
case DockerContainer::SSXray: return true;
case DockerContainer::MtProxy: return true;
case DockerContainer::Telemt: return true;
default: return false;
}
@@ -318,6 +341,8 @@ bool ContainerUtils::isShareable(DockerContainer container)
case DockerContainer::Dns: return false;
case DockerContainer::Sftp: return false;
case DockerContainer::Socks5Proxy: return false;
case DockerContainer::MtProxy: return false;
case DockerContainer::Telemt: return false;
default: return true;
}
}
@@ -346,8 +371,10 @@ int ContainerUtils::installPageOrder(DockerContainer container)
case DockerContainer::Xray: return 3;
case DockerContainer::Ipsec: return 7;
case DockerContainer::SSXray: return 8;
case DockerContainer::MtProxy:
case DockerContainer::Telemt:
return 20;
default: return 0;
}
}

View File

@@ -30,7 +30,9 @@ namespace amnezia
TorWebSite,
Dns,
Sftp,
Socks5Proxy
Socks5Proxy,
MtProxy,
Telemt,
};
Q_ENUM_NS(Proto)

View File

@@ -9,7 +9,6 @@
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
#include "core/utils/protocolEnum.h"
#include "core/utils/protocolEnum.h"
#include "core/protocols/protocolUtils.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/constants/protocolConstants.h"
@@ -20,6 +19,8 @@
#include "core/models/protocols/xrayProtocolConfig.h"
#include "core/models/protocols/sftpProtocolConfig.h"
#include "core/models/protocols/socks5ProxyProtocolConfig.h"
#include "core/models/protocols/mtProxyProtocolConfig.h"
#include "core/models/protocols/telemtProtocolConfig.h"
using namespace amnezia;
using namespace ProtocolUtils;
@@ -38,6 +39,8 @@ QString amnezia::scriptFolder(amnezia::DockerContainer container)
case DockerContainer::Dns: return QLatin1String("dns");
case DockerContainer::Sftp: return QLatin1String("sftp");
case DockerContainer::Socks5Proxy: return QLatin1String("socks5_proxy");
case DockerContainer::MtProxy: return QLatin1String("mtproxy");
case DockerContainer::Telemt: return QLatin1String("telemt");
default: return QString();
}
}
@@ -284,6 +287,86 @@ amnezia::ScriptVars amnezia::genSocks5ProxyVars(const ContainerConfig &container
return vars;
}
amnezia::ScriptVars amnezia::genMtProxyVars(const ContainerConfig &containerConfig) {
ScriptVars vars;
if (auto *mtProxyProtocolConfig = containerConfig.getMtProxyProtocolConfig()) {
const MtProxyProtocolConfig &c = *mtProxyProtocolConfig;
vars.append({{"$MTPROXY_PORT", c.port.isEmpty() ? QString(protocols::mtProxy::defaultPort) : c.port}});
vars.append({{"$MTPROXY_SECRET", c.secret}});
vars.append({{"$MTPROXY_TAG", c.tag}});
vars.append({{"$MTPROXY_TRANSPORT_MODE",
c.transportMode.isEmpty() ? QString(protocols::mtProxy::transportModeStandard)
: c.transportMode}});
QString tlsDomain = c.tlsDomain;
if (tlsDomain.isEmpty()) {
tlsDomain = QString(protocols::mtProxy::defaultTlsDomain);
}
vars.append({{"$MTPROXY_TLS_DOMAIN", tlsDomain}});
vars.append({{"$MTPROXY_PUBLIC_HOST", c.publicHost}});
QStringList additionalList;
for (const QString &s: c.additionalSecrets) {
if (!s.isEmpty()) {
additionalList << s;
}
}
vars.append({{"$MTPROXY_ADDITIONAL_SECRETS", additionalList.join(QLatin1Char(','))}});
const QString workersMode = c.workersMode.isEmpty() ? QString(protocols::mtProxy::workersModeAuto)
: c.workersMode;
QString workers;
if (workersMode == QLatin1String(protocols::mtProxy::workersModeManual)) {
workers = c.workers.isEmpty() ? QString(protocols::mtProxy::defaultWorkers) : c.workers;
} else {
const QString transportMode =
c.transportMode.isEmpty() ? QString(protocols::mtProxy::transportModeStandard) : c.transportMode;
workers = (transportMode == QLatin1String(protocols::mtProxy::transportModeFakeTLS)) ? QStringLiteral("0")
: QStringLiteral("2");
}
vars.append({{"$MTPROXY_WORKERS", workers}});
vars.append({{"$MTPROXY_NAT_ENABLED", c.natEnabled ? QStringLiteral("1") : QStringLiteral("0")}});
vars.append({{"$MTPROXY_NAT_INTERNAL_IP", c.natInternalIp}});
vars.append({{"$MTPROXY_NAT_EXTERNAL_IP", c.natExternalIp}});
}
return vars;
}
amnezia::ScriptVars amnezia::genTelemtVars(const ContainerConfig &containerConfig)
{
ScriptVars vars;
if (auto *telemtProtocolConfig = containerConfig.getTelemtProtocolConfig()) {
const TelemtProtocolConfig &c = *telemtProtocolConfig;
const QString transport = c.transportMode.isEmpty() ? QString(protocols::telemt::transportModeStandard)
: c.transportMode;
const bool faketls = (transport == QLatin1String(protocols::telemt::transportModeFakeTLS));
vars.append({ { "$TELEMT_TOML_SECURE", faketls ? QLatin1String("false") : QLatin1String("true") } });
vars.append({ { "$TELEMT_TOML_TLS", faketls ? QLatin1String("true") : QLatin1String("false") } });
vars.append({ { "$TELEMT_PORT", c.port.isEmpty() ? QString(protocols::telemt::defaultPort) : c.port } });
vars.append({ { "$TELEMT_SECRET", c.secret } });
vars.append({ { "$TELEMT_TAG", c.tag } });
QString tlsDomain = c.tlsDomain;
if (tlsDomain.isEmpty()) {
tlsDomain = QString(protocols::telemt::defaultTlsDomain);
}
vars.append({ { "$TELEMT_TLS_DOMAIN", tlsDomain } });
vars.append({ { "$TELEMT_PUBLIC_HOST", c.publicHost } });
vars.append({ { "$TELEMT_USER_NAME",
c.userName.isEmpty() ? QString::fromUtf8(protocols::telemt::defaultUserName) : c.userName } });
vars.append({ { "$TELEMT_USE_MIDDLE_PROXY", c.useMiddleProxy ? QLatin1String("true") : QLatin1String("false") } });
vars.append({ { "$TELEMT_MASK", c.maskEnabled ? QLatin1String("true") : QLatin1String("false") } });
vars.append({ { "$TELEMT_TLS_EMULATION", c.tlsEmulation ? QLatin1String("true") : QLatin1String("false") } });
}
return vars;
}
amnezia::ScriptVars amnezia::genProtocolVarsForContainer(DockerContainer container, const ContainerConfig &containerConfig)
{
ScriptVars vars;
@@ -308,6 +391,12 @@ amnezia::ScriptVars amnezia::genProtocolVarsForContainer(DockerContainer contain
case Proto::Socks5Proxy:
vars.append(genSocks5ProxyVars(containerConfig));
break;
case Proto::MtProxy:
vars.append(genMtProxyVars(containerConfig));
break;
case Proto::Telemt:
vars.append(genTelemtVars(containerConfig));
break;
default:
break;
}

View File

@@ -67,6 +67,8 @@ ScriptVars genWireGuardVars(const ContainerConfig &containerConfig);
ScriptVars genAwgVars(const ContainerConfig &containerConfig);
ScriptVars genSftpVars(const ContainerConfig &containerConfig);
ScriptVars genSocks5ProxyVars(const ContainerConfig &containerConfig);
ScriptVars genMtProxyVars(const ContainerConfig &containerConfig);
ScriptVars genTelemtVars(const ContainerConfig &containerConfig);
ScriptVars genProtocolVarsForContainer(DockerContainer container, const ContainerConfig &containerConfig);
}

View File

@@ -56,7 +56,7 @@ namespace libssh {
QEventLoop wait;
connect(&watcher, &QFutureWatcher<ErrorCode>::finished, &wait, &QEventLoop::quit);
watcher.setFuture(future);
wait.exec();
wait.exec(QEventLoop::ExcludeUserInputEvents);
int connectionResult = watcher.result();
@@ -189,7 +189,7 @@ namespace libssh {
QEventLoop wait;
QObject::connect(this, &Client::writeToChannelFinished, &wait, &QEventLoop::quit);
wait.exec();
wait.exec(QEventLoop::ExcludeUserInputEvents);
return watcher.result();
}
@@ -284,7 +284,7 @@ namespace libssh {
QEventLoop wait;
QObject::connect(this, &Client::scpFileCopyFinished, &wait, &QEventLoop::quit);
wait.exec();
wait.exec(QEventLoop::ExcludeUserInputEvents);
closeScpSession();
return watcher.result();

View File

@@ -103,8 +103,8 @@ ErrorCode SshSession::runContainerScript(const ServerCredentials &credentials, D
if (e)
return e;
QString runner =
QString("sudo docker exec -i $CONTAINER_NAME %2 %1 ").arg(fileName, (container == DockerContainer::Socks5Proxy ? "sh" : "bash"));
const bool useSh = container == DockerContainer::Socks5Proxy || container == DockerContainer::MtProxy || container == DockerContainer::Telemt;
QString runner = QString("sudo docker exec -i $CONTAINER_NAME %2 %1 ").arg(fileName, useSh ? "sh" : "bash");
e = runScript(credentials, replaceVars(runner, amnezia::genBaseVars(credentials, container, QString(), QString())), cbReadStdOut, cbReadStdErr);
QString remover = QString("sudo docker exec -i $CONTAINER_NAME rm %1 ").arg(fileName);

View File

@@ -220,7 +220,7 @@ bool IosController::connectVpn(amnezia::Proto proto, const QJsonObject& configur
m_rawConfig = configuration;
m_serverAddress = configuration.value(configKey::hostName).toString().toNSString();
const QString serverDescription = configuration.value(config_key::description).toString().trimmed();
const QString serverDescription = configuration.value(configKey::description).toString().trimmed();
QString tunnelName;
if (serverDescription.isEmpty()) {
tunnelName = ProtocolUtils::protoToString(proto);

View File

@@ -1,4 +1,4 @@
if which apt-get > /dev/null 2>&1; then pm=$(which apt-get); silent_inst="-yq install"; check_pkgs="-yq update"; docker_pkg="docker.io"; dist="debian";\
if which apt-get > /dev/null 2>&1; then pm=$(which apt-get); silent_inst="-yq install --install-recommends"; check_pkgs="-yq update"; docker_pkg="docker.io"; dist="debian";\
elif which dnf > /dev/null 2>&1; then pm=$(which dnf); silent_inst="-yq install"; check_pkgs="-yq check-update"; docker_pkg="docker"; dist="fedora";\
elif which yum > /dev/null 2>&1; then pm=$(which yum); silent_inst="-y -q install"; check_pkgs="-y -q check-update"; docker_pkg="docker"; dist="centos";\
elif which zypper > /dev/null 2>&1; then pm=$(which zypper); silent_inst="-nq install"; check_pkgs="-nq refresh"; docker_pkg="docker"; dist="opensuse";\

View File

@@ -0,0 +1,9 @@
FROM amneziavpn/mtproxy:latest
RUN mkdir -p /opt/amnezia /data
RUN printf '#!/bin/sh\ntail -f /dev/null\n' > /opt/amnezia/start.sh && \
chmod a+x /opt/amnezia/start.sh
VOLUME /data
ENTRYPOINT ["/bin/sh", "/opt/amnezia/start.sh"]
CMD [""]

View File

@@ -0,0 +1,60 @@
#!/bin/sh
# Download Telegram config files
curl -s https://core.telegram.org/getProxySecret -o /data/proxy-secret
curl -s https://core.telegram.org/getProxyConfig -o /data/proxy-multi.conf
# Determine secret: env var -> saved file -> generate new
if [ -n "$MTPROXY_SECRET" ]; then
SECRET="$MTPROXY_SECRET"
elif [ -f /data/secret ]; then
SECRET=$(cat /data/secret)
else
SECRET=$(openssl rand -hex 16)
fi
# Validate: must be exactly 32 hex chars
echo "$SECRET" | grep -qE '^[0-9a-fA-F]{32}$' || SECRET=$(openssl rand -hex 16)
# Persist secret for start.sh restarts
echo "$SECRET" > /data/secret
# Detect external IP
IP=$(curl -s --max-time 5 https://api.ipify.org 2>/dev/null)
[ -z "$IP" ] && IP=$(curl -s --max-time 5 https://ifconfig.me 2>/dev/null)
[ -z "$IP" ] && IP=$(curl -s --max-time 5 https://icanhazip.com 2>/dev/null)
# Use custom public host/domain if provided, otherwise fall back to detected IP
if [ -n "$MTPROXY_PUBLIC_HOST" ]; then
LINK_HOST="$MTPROXY_PUBLIC_HOST"
else
LINK_HOST="$IP"
fi
PORT=$MTPROXY_PORT
# Transport mode is substituted by replaceVars — plain variable, no curly braces
TRANSPORT_MODE=$MTPROXY_TRANSPORT_MODE
PADDED_SECRET="dd${SECRET}"
if [ "$TRANSPORT_MODE" = "faketls" ] && [ -n "$MTPROXY_TLS_DOMAIN" ]; then
DOMAIN_HEX=$(echo -n "$MTPROXY_TLS_DOMAIN" | od -A n -t x1 | tr -d ' \n')
FAKETLS_SECRET="ee${SECRET}${DOMAIN_HEX}"
else
FAKETLS_SECRET=""
fi
# Active link secret depends on transport mode
if [ "$TRANSPORT_MODE" = "faketls" ] && [ -n "$FAKETLS_SECRET" ]; then
LINK_SECRET="$FAKETLS_SECRET"
else
LINK_SECRET="$PADDED_SECRET"
fi
# Output stable markers — parsed by updateContainerConfigAfterInstallation()
echo "[*] MTProxy configuration"
echo "[*] Secret: ${SECRET}"
echo "[*] FakeTLS: ${FAKETLS_SECRET}"
echo "[*] tg:// link: tg://proxy?server=${LINK_HOST}&port=${PORT}&secret=${LINK_SECRET}"
echo "[*] t.me link: https://t.me/proxy?server=${LINK_HOST}&port=${PORT}&secret=${LINK_SECRET}"

View File

@@ -0,0 +1,9 @@
# Run container
sudo docker run -d \
--log-driver none \
--restart always \
-p $MTPROXY_PORT:$MTPROXY_PORT/tcp \
-v amnezia-mtproxy-data:/data \
--name $CONTAINER_NAME \
$CONTAINER_NAME

View File

@@ -0,0 +1,71 @@
#!/bin/sh
echo "Container startup"
# Read persisted secret
SECRET=""
if [ -f /data/secret ]; then
SECRET=$(cat /data/secret)
fi
if [ -z "$SECRET" ]; then
echo "ERROR: /data/secret not found — run configure_container first"
tail -f /dev/null
exit 1
fi
# Build tag argument
TAG_ARG=""
if [ -n "$MTPROXY_TAG" ]; then
TAG_ARG="-P $MTPROXY_TAG"
fi
# Build domain argument for FakeTLS mode
DOMAIN_ARG=""
if [ "$MTPROXY_TRANSPORT_MODE" = "faketls" ] && [ -n "$MTPROXY_TLS_DOMAIN" ]; then
DOMAIN_ARG="--domain $MTPROXY_TLS_DOMAIN"
fi
WORKERS=$MTPROXY_WORKERS
STATS_PORT=2398
LISTEN_PORT=$MTPROXY_PORT
NAT_FLAG=""
NAT_VALUE=""
if [ "$MTPROXY_NAT_ENABLED" = "1" ] && [ -n "$MTPROXY_NAT_INTERNAL_IP" ] && [ -n "$MTPROXY_NAT_EXTERNAL_IP" ]; then
NAT_FLAG="--nat-info"
NAT_VALUE="$MTPROXY_NAT_INTERNAL_IP:$MTPROXY_NAT_EXTERNAL_IP"
else
INTERNAL_IP=$(hostname -i 2>/dev/null | awk '{print $1}')
EXTERNAL_IP=$(curl -s --max-time 5 https://api.ipify.org 2>/dev/null)
[ -z "$EXTERNAL_IP" ] && EXTERNAL_IP=$(curl -s --max-time 5 https://ifconfig.me 2>/dev/null)
if [ -n "$INTERNAL_IP" ] && [ -n "$EXTERNAL_IP" ] && [ "$INTERNAL_IP" != "$EXTERNAL_IP" ]; then
NAT_FLAG="--nat-info"
NAT_VALUE="${INTERNAL_IP}:${EXTERNAL_IP}"
fi
fi
# Build additional secrets arguments
ADDITIONAL_SECRETS_ARG=""
if [ -n "$MTPROXY_ADDITIONAL_SECRETS" ]; then
for S in $(echo "$MTPROXY_ADDITIONAL_SECRETS" | tr ',' ' '); do
ADDITIONAL_SECRETS_ARG="$ADDITIONAL_SECRETS_ARG -S $S"
done
fi
# Start proxy (foreground)
exec mtproto-proxy \
-u root \
-p ${STATS_PORT} \
-H ${LISTEN_PORT} \
-S ${SECRET} \
${ADDITIONAL_SECRETS_ARG} \
--aes-pwd /data/proxy-secret \
-M ${WORKERS} \
-C 60000 \
--allow-skip-dh \
${NAT_FLAG:+${NAT_FLAG} ${NAT_VALUE}} \
${TAG_ARG} \
${DOMAIN_ARG} \
/data/proxy-multi.conf

View File

@@ -24,6 +24,14 @@
<file>ipsec/run_container.sh</file>
<file>ipsec/start.sh</file>
<file>ipsec/strongswan.profile</file>
<file>mtproxy/configure_container.sh</file>
<file>mtproxy/Dockerfile</file>
<file>mtproxy/run_container.sh</file>
<file>mtproxy/start.sh</file>
<file>telemt/configure_container.sh</file>
<file>telemt/Dockerfile</file>
<file>telemt/run_container.sh</file>
<file>telemt/start.sh</file>
<file>openvpn/configure_container.sh</file>
<file>openvpn/Dockerfile</file>
<file>openvpn/run_container.sh</file>
@@ -55,4 +63,3 @@
<file>xray/template.json</file>
</qresource>
</RCC>

View File

@@ -0,0 +1,42 @@
# syntax=docker/dockerfile:1
# Debian-based image with Telemt binary (shell + jq for Amnezia configure scripts).
# Binary from https://github.com/telemt/telemt releases (same pattern as upstream Dockerfile minimal stage).
FROM debian:12-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
binutils \
ca-certificates \
curl \
jq \
openssl \
tar \
&& rm -rf /var/lib/apt/lists/*
# Use machine arch (works with classic `docker build`; TARGETARCH is only set with BuildKit).
RUN set -eux; \
ARCH="$(uname -m)"; \
case "$ARCH" in \
x86_64) ASSET="telemt-x86_64-linux-musl.tar.gz" ;; \
aarch64|arm64) ASSET="telemt-aarch64-linux-musl.tar.gz" ;; \
*) echo "Unsupported architecture: $ARCH" >&2; exit 1 ;; \
esac; \
curl -fL --retry 5 --retry-delay 3 --connect-timeout 10 --max-time 120 \
-o "/tmp/${ASSET}" "https://github.com/telemt/telemt/releases/latest/download/${ASSET}"; \
curl -fL --retry 5 --retry-delay 3 --connect-timeout 10 --max-time 120 \
-o "/tmp/${ASSET}.sha256" "https://github.com/telemt/telemt/releases/latest/download/${ASSET}.sha256"; \
cd /tmp && sha256sum -c "${ASSET}.sha256"; \
tar -xzf "${ASSET}" -C /tmp; \
test -f /tmp/telemt; \
install -m 0755 /tmp/telemt /usr/local/bin/telemt; \
strip --strip-unneeded /usr/local/bin/telemt || true; \
rm -f "/tmp/${ASSET}" "/tmp/${ASSET}.sha256" /tmp/telemt
RUN mkdir -p /opt/amnezia /data
RUN printf '#!/bin/sh\ntail -f /dev/null\n' > /opt/amnezia/start.sh && \
chmod a+x /opt/amnezia/start.sh
VOLUME /data
ENTRYPOINT ["/bin/sh", "/opt/amnezia/start.sh"]
CMD [""]

View File

@@ -0,0 +1,73 @@
#!/bin/sh
# Do not use set -e: Telemt / curl / kill edge cases should not abort the whole configure step.
echo "[*] Amnezia Telemt: configure script start"
mkdir -p /data/tlsfront
# Secret: substituted $TELEMT_SECRET -> saved file -> openssl (same rules as MTProxy configure)
if [ -n "$TELEMT_SECRET" ]; then
SECRET="$TELEMT_SECRET"
elif [ -f /data/secret ]; then
SECRET=$(cat /data/secret)
else
SECRET=$(openssl rand -hex 16)
fi
# Must be exactly 32 hex chars
echo "$SECRET" | grep -qE '^[0-9a-fA-F]{32}$' || SECRET=$(openssl rand -hex 16)
# Build config.toml (other variables substituted on the host by Amnezia before upload)
rm -f /data/config.toml
{
echo "### Amnezia Telemt — generated"
echo "[general]"
echo "use_middle_proxy = $TELEMT_USE_MIDDLE_PROXY"
echo "log_level = \"normal\""
if [ -n "$TELEMT_TAG" ]; then
echo "ad_tag = \"$TELEMT_TAG\""
fi
echo ""
echo "[general.modes]"
echo "classic = false"
echo "secure = $TELEMT_TOML_SECURE"
echo "tls = $TELEMT_TOML_TLS"
echo ""
echo "[general.links]"
echo "show = \"*\""
if [ -n "$TELEMT_PUBLIC_HOST" ]; then
echo "public_host = \"$TELEMT_PUBLIC_HOST\""
fi
echo "public_port = $TELEMT_PORT"
echo ""
echo "[server]"
echo "port = $TELEMT_PORT"
echo ""
echo "[server.api]"
echo "enabled = true"
echo "listen = \"0.0.0.0:9091\""
# Match upstream Telemt default: localhost API only (curl in this script uses 127.0.0.1).
echo "whitelist = [\"127.0.0.0/8\"]"
echo ""
echo "[[server.listeners]]"
echo "ip = \"0.0.0.0\""
echo ""
echo "[censorship]"
echo "tls_domain = \"$TELEMT_TLS_DOMAIN\""
echo "mask = $TELEMT_MASK"
echo "tls_emulation = $TELEMT_TLS_EMULATION"
echo "tls_front_dir = \"/data/tlsfront\""
echo ""
echo "[access.users]"
echo "$TELEMT_USER_NAME = \"$SECRET\""
} > /data/config.toml
echo "$SECRET" > /data/secret
chmod 600 /data/secret 2>/dev/null || true
# Do not start telemt here: a long-lived process + curl loop inside `docker exec` can confuse SSH/Docker
# timing and is unnecessary — start.sh runs telemt after configure. Links can be empty until the service
# is up; the client still parses Secret below.
echo "[*] Telemt configuration"
echo "[*] Secret: $SECRET"
echo "[*] tg:// link: "
echo "[*] t.me link: "

View File

@@ -0,0 +1,9 @@
# Run container (ulimit per Telemt docs — avoids "Too many open files" under load)
sudo docker run -d \
--log-driver none \
--restart always \
--ulimit nofile=65536:65536 \
-p $TELEMT_PORT:$TELEMT_PORT/tcp \
-v amnezia-telemt-data:/data \
--name $CONTAINER_NAME \
$CONTAINER_NAME

View File

@@ -0,0 +1,12 @@
#!/bin/sh
echo "Container startup (Telemt)"
if [ ! -f /data/config.toml ]; then
echo "ERROR: /data/config.toml not found — run configure_container first"
tail -f /dev/null
exit 1
fi
mkdir -p /data/tlsfront
exec /usr/local/bin/telemt /data/config.toml

View File

@@ -1312,6 +1312,21 @@ Thank you for staying with us!</source>
</context>
<context>
<name>PageProtocolXraySettings</name>
<message>
<location filename="../ui/qml/Pages2/PageProtocolXraySettings.qml" line="61"/>
<source>XRay VLESS settings</source>
<translation>Настройки XRay VLESS</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolXraySettings.qml" line="80"/>
<source>More about settings</source>
<translation>Подробнее о настройках</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolXraySettings.qml" line="188"/>
<source>Reset settings</source>
<translation>Сбросить настройки</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolXraySettings.qml" line="57"/>
<source>XRay settings</source>

View File

@@ -201,3 +201,12 @@ bool ImportUiController::decodeQrCode(const QString &code)
return mInstance->parseQrCodeChunk(code);
}
#endif
QString ImportUiController::readTextFile(const QString &fileName)
{
QFile file(fileName);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
return {};
}
return QString::fromUtf8(file.readAll());
}

View File

@@ -28,6 +28,7 @@ public slots:
QString getMaliciousWarningText();
bool isNativeWireGuardConfig();
void processNativeWireGuardConfig();
QString readTextFile(const QString &fileName);
#if defined Q_OS_ANDROID || defined Q_OS_IOS
void startDecodingQr();

View File

@@ -0,0 +1,46 @@
#include "networkReachabilityController.h"
#include <QNetworkInformation>
namespace {
bool reachabilityAllowsRemoteOperations(QNetworkInformation::Reachability r) {
using R = QNetworkInformation::Reachability;
// Unknown: no backend or not yet determined — do not block UI.
return r == R::Online || r == R::Unknown;
}
} // namespace
NetworkReachabilityController::NetworkReachabilityController(QObject *parent) : QObject(parent) {
attachToNetworkInformation();
}
bool NetworkReachabilityController::hasInternetAccess() const {
return m_hasInternetAccess;
}
void NetworkReachabilityController::attachToNetworkInformation() {
if (!QNetworkInformation::loadDefaultBackend()) {
return;
}
QNetworkInformation *ni = QNetworkInformation::instance();
if (!ni) {
return;
}
const bool initial = reachabilityAllowsRemoteOperations(ni->reachability());
const bool previous = m_hasInternetAccess;
m_hasInternetAccess = initial;
if (previous != m_hasInternetAccess) {
emit hasInternetAccessChanged();
}
connect(ni, &QNetworkInformation::reachabilityChanged, this,
[this](QNetworkInformation::Reachability r) {
const bool ok = reachabilityAllowsRemoteOperations(r);
if (ok == m_hasInternetAccess) {
return;
}
m_hasInternetAccess = ok;
emit hasInternetAccessChanged();
});
}

View File

@@ -0,0 +1,30 @@
#ifndef NETWORKREACHABILITYCONTROLLER_H
#define NETWORKREACHABILITYCONTROLLER_H
#include <QObject>
// Exposes QNetworkInformation to QML for UI that must not run remote operations offline.
// Note: mozilla/networkwatcher.h has NetworkWatcher::getReachability() using the same API,
// but networkwatcher.cpp is not linked into the desktop client (only the service process).
class NetworkReachabilityController final : public QObject {
Q_OBJECT
Q_PROPERTY(bool hasInternetAccess READ hasInternetAccess NOTIFY hasInternetAccessChanged)
public:
explicit NetworkReachabilityController(QObject *parent = nullptr);
bool hasInternetAccess() const;
signals:
void hasInternetAccessChanged();
private:
void attachToNetworkInformation();
bool m_hasInternetAccess = true;
};
#endif // NETWORKREACHABILITYCONTROLLER_H

View File

@@ -50,6 +50,8 @@ namespace PageLoader
PageServiceTorWebsiteSettings,
PageServiceDnsSettings,
PageServiceSocksProxySettings,
PageServiceMtProxySettings,
PageServiceTelemtSettings,
PageSetupWizardStart,
PageSetupWizardCredentials,
@@ -80,7 +82,15 @@ namespace PageLoader
PageSetupWizardApiPremiumInfo,
PageSetupWizardApiTrialEmail,
PageDevMenu
PageDevMenu,
PageProtocolXraySnapshots,
PageProtocolXrayTransportSettings,
PageProtocolXrayXmuxSettings,
PageProtocolXrayXPaddingSettings,
PageProtocolXrayFlowSettings,
PageProtocolXraySecuritySettings,
PageProtocolXrayXPaddingBytesSettings,
};
Q_ENUM_NS(PageEnum)

View File

@@ -3,6 +3,7 @@
#include <QDebug>
#include "../systemController.h"
#include "core/utils/qrCodeUtils.h"
ExportUiController::ExportUiController(ExportController* exportController, QObject *parent)
: QObject(parent),
@@ -53,6 +54,14 @@ void ExportUiController::generateXrayConfig(const QString &serverId, const QStri
applyExportResult(result);
}
void ExportUiController::generateQrFromString(const QString &text)
{
clearPreviousConfig();
m_config = text;
m_qrCodes = qrCodeUtils::generateQrCodeImageSeries(text.toUtf8());
emit exportConfigChanged();
}
QString ExportUiController::getConfig()
{
return m_config;
@@ -118,3 +127,13 @@ void ExportUiController::applyExportResult(const ExportController::ExportResult
emit exportConfigChanged();
}
void ExportUiController::setConfigFromString(const QString &config, const QString &fileName)
{
clearPreviousConfig();
m_config = config;
emit exportConfigChanged();
if (!fileName.isEmpty()) {
SystemController::saveFile(fileName, m_config);
}
}

View File

@@ -25,12 +25,14 @@ public slots:
void generateWireGuardConfig(const QString &serverId, const QString &clientName);
void generateAwgConfig(const QString &serverId, int containerIndex, const QString &clientName);
void generateXrayConfig(const QString &serverId, const QString &clientName);
void generateQrFromString(const QString &text);
QString getConfig();
QString getNativeConfigString();
QList<QString> getQrCodes();
void exportConfig(const QString &fileName);
void setConfigFromString(const QString &config, const QString &fileName);
void updateClientManagementModel(const QString &serverId, int containerIndex);

136
client/ui/controllers/selfhosted/installUiController.cpp Executable file → Normal file
View File

@@ -5,11 +5,13 @@
#include <QEventLoop>
#include <QJsonObject>
#include <QRandomGenerator>
#include <QRegularExpression>
#include <QStandardPaths>
#include <QFutureWatcher>
#include <QtConcurrent>
#include "core/utils/api/apiUtils.h"
#include "core/controllers/selfhosted/installController.h"
#include "core/utils/selfhosted/sshSession.h"
#include "core/utils/networkUtilities.h"
#include "core/utils/protocolEnum.h"
#include "core/protocols/protocolUtils.h"
@@ -47,6 +49,8 @@ InstallUiController::InstallUiController(InstallController *installController,
#endif
SftpConfigModel *sftpConfigModel,
Socks5ProxyConfigModel *socks5ConfigModel,
MtProxyConfigModel* mtConfigModel,
TelemtConfigModel *telemtConfigModel,
QObject *parent)
: QObject(parent),
m_installController(installController),
@@ -63,7 +67,9 @@ InstallUiController::InstallUiController(InstallController *installController,
m_ikev2ConfigModel(ikev2ConfigModel),
#endif
m_sftpConfigModel(sftpConfigModel),
m_socks5ConfigModel(socks5ConfigModel)
m_socks5ConfigModel(socks5ConfigModel),
m_mtProxyConfigModel(mtConfigModel),
m_telemtConfigModel(telemtConfigModel)
{
connect(m_installController, &InstallController::configValidated, this, &InstallUiController::configValidated);
connect(m_installController, &InstallController::validationErrorOccurred, this, [this](ErrorCode errorCode) {
@@ -199,7 +205,7 @@ void InstallUiController::scanServerForInstalledContainers(const QString &server
emit installationErrorOccurred(errorCode);
}
void InstallUiController::updateContainer(const QString &serverId, int containerIndex, int protocolIndex)
void InstallUiController::updateContainer(const QString &serverId, int containerIndex, int protocolIndex, bool closePage)
{
DockerContainer container = static_cast<DockerContainer>(containerIndex);
@@ -238,6 +244,14 @@ void InstallUiController::updateContainer(const QString &serverId, int container
containerConfig.protocolConfig = m_socks5ConfigModel->getProtocolConfig();
break;
}
case Proto::MtProxy: {
containerConfig.protocolConfig = m_mtProxyConfigModel->getProtocolConfig();
break;
}
case Proto::Telemt: {
containerConfig.protocolConfig = m_telemtConfigModel->getProtocolConfig();
break;
}
#ifdef Q_OS_WINDOWS
case Proto::Ikev2: {
containerConfig.protocolConfig = m_ikev2ConfigModel->getProtocolConfig();
@@ -249,19 +263,128 @@ void InstallUiController::updateContainer(const QString &serverId, int container
}
ContainerConfig oldContainerConfig = m_serversController->getContainerConfig(serverId, container);
if (container == DockerContainer::MtProxy || container == DockerContainer::Telemt) {
emit serverIsBusy(true);
auto *watcher = new QFutureWatcher<ErrorCode>(this);
QObject::connect(watcher, &QFutureWatcher<ErrorCode>::finished, this,
[this, watcher, serverId, container, closePage]() {
const ErrorCode errorCode = watcher->result();
watcher->deleteLater();
emit serverIsBusy(false);
if (errorCode == ErrorCode::NoError) {
const ContainerConfig updatedConfig =
m_serversController->getContainerConfig(serverId, container);
m_protocolModel->updateModel(updatedConfig);
const auto defaultContainer =
m_serversController->getDefaultContainer(serverId);
if ((serverId == m_serversController->getDefaultServerId())
&& (container == defaultContainer)) {
emit currentContainerUpdated();
} else {
emit updateContainerFinished(tr("Settings updated successfully"), closePage);
}
} else {
emit installationErrorOccurred(errorCode);
}
});
ContainerConfig newConfigCopy = containerConfig;
ContainerConfig oldConfigCopy = oldContainerConfig;
InstallController *installController = m_installController;
QFuture<ErrorCode> future =
QtConcurrent::run([installController, serverId, container, oldConfigCopy,
newConfigCopy]() mutable -> ErrorCode {
return installController->updateContainer(serverId, container, oldConfigCopy, newConfigCopy);
});
watcher->setFuture(future);
return;
}
ErrorCode errorCode = m_installController->updateContainer(serverId, container, oldContainerConfig, containerConfig);
if (errorCode == ErrorCode::NoError) {
ContainerConfig updatedConfig = m_serversController->getContainerConfig(serverId, container);
m_protocolModel->updateModel(updatedConfig);
emit updateContainerFinished(tr("Settings updated successfully"));
const auto defaultContainer = m_serversController->getDefaultContainer(serverId);
if ((serverId == m_serversController->getDefaultServerId()) && (container == defaultContainer)) {
emit currentContainerUpdated();
} else {
emit updateContainerFinished(tr("Settings updated successfully"), closePage);
}
return;
}
emit installationErrorOccurred(errorCode);
}
void InstallUiController::setContainerEnabled(const QString &serverId, int containerIndex, bool enabled)
{
const DockerContainer container = static_cast<DockerContainer>(containerIndex);
if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt) {
return;
}
emit serverIsBusy(true);
const ErrorCode errorCode = m_installController->setDockerContainerEnabledState(serverId, container, enabled);
emit serverIsBusy(false);
if (errorCode == ErrorCode::NoError) {
const ContainerConfig currentConfig = m_serversController->getContainerConfig(serverId, container);
m_protocolModel->updateModel(currentConfig);
emit setContainerEnabledFinished(enabled);
return;
}
emit installationErrorOccurred(errorCode);
}
void InstallUiController::refreshContainerStatus(const QString &serverId, int containerIndex)
{
const DockerContainer container = static_cast<DockerContainer>(containerIndex);
if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt) {
return;
}
int status = 3;
const ErrorCode errorCode = m_installController->queryDockerContainerStatus(serverId, container, status);
if (errorCode != ErrorCode::NoError) {
emit containerStatusRefreshed(3);
return;
}
emit containerStatusRefreshed(status);
}
void InstallUiController::refreshContainerDiagnostics(const QString &serverId, int containerIndex, int port)
{
const DockerContainer container = static_cast<DockerContainer>(containerIndex);
if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt) {
return;
}
MtProxyContainerDiagnostics diag;
const ErrorCode errorCode = m_installController->queryMtProxyDiagnostics(serverId, container, port, diag);
if (errorCode != ErrorCode::NoError) {
emit containerDiagnosticsRefreshed(false, false, -1, QString(), QString());
return;
}
emit containerDiagnosticsRefreshed(diag.portReachable, diag.upstreamReachable, diag.clientsConnected,
diag.lastConfigRefresh, diag.statsEndpoint);
}
void InstallUiController::fetchContainerSecret(const QString &serverId, int containerIndex)
{
const DockerContainer container = static_cast<DockerContainer>(containerIndex);
if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt) {
return;
}
const QString secret = m_installController->fetchDockerContainerSecret(serverId, container);
emit containerSecretFetched(secret);
}
void InstallUiController::rebootServer(const QString &serverId)
{
const QString serverName = m_serversController->notificationDisplayName(serverId);
@@ -469,10 +592,13 @@ void InstallUiController::updateProtocolConfigModel(const QString &serverId, int
case Proto::Awg: updateIfPresent(m_awgConfigModel, containerConfig.getAwgProtocolConfig()); break;
case Proto::WireGuard: updateIfPresent(m_wireGuardConfigModel, containerConfig.getWireGuardProtocolConfig()); break;
case Proto::OpenVpn: updateIfPresent(m_openVpnConfigModel, containerConfig.getOpenVpnProtocolConfig()); break;
case Proto::Xray: updateIfPresent(m_xrayConfigModel, containerConfig.getXrayProtocolConfig()); break;
case Proto::Xray:
case Proto::SSXray: updateIfPresent(m_xrayConfigModel, containerConfig.getXrayProtocolConfig()); break;
case Proto::TorWebSite: updateIfPresent(m_torConfigModel, containerConfig.getTorProtocolConfig()); break;
case Proto::Sftp: updateIfPresent(m_sftpConfigModel, containerConfig.getSftpProtocolConfig()); break;
case Proto::Socks5Proxy: updateIfPresent(m_socks5ConfigModel, containerConfig.getSocks5ProxyProtocolConfig()); break;
case Proto::MtProxy: updateIfPresent(m_mtProxyConfigModel, containerConfig.getMtProxyProtocolConfig()); break;
case Proto::Telemt: updateIfPresent(m_telemtConfigModel, containerConfig.getTelemtProtocolConfig()); break;
#ifdef Q_OS_WINDOWS
case Proto::Ikev2: updateIfPresent(m_ikev2ConfigModel, containerConfig.getIkev2ProtocolConfig()); break;
#endif

View File

@@ -28,6 +28,8 @@
#include "ui/models/services/torConfigModel.h"
#include "core/models/protocols/sftpProtocolConfig.h"
#include "core/models/protocols/socks5ProxyProtocolConfig.h"
#include "ui/models/services/mtProxyConfigModel.h"
#include "ui/models/services/telemtConfigModel.h"
class InstallUiController : public QObject
{
@@ -48,6 +50,8 @@ public:
#endif
SftpConfigModel* sftpConfigModel,
Socks5ProxyConfigModel* socks5ConfigModel,
MtProxyConfigModel* mtConfigModel,
TelemtConfigModel* telemtConfigModel,
QObject *parent = nullptr);
~InstallUiController();
@@ -58,12 +62,16 @@ public slots:
void scanServerForInstalledContainers(const QString &serverId);
void updateContainer(const QString &serverId, int containerIndex, int protocolIndex);
void updateContainer(const QString &serverId, int containerIndex, int protocolIndex, bool closePage = true);
void removeServer(const QString &serverId);
void rebootServer(const QString &serverId);
void removeAllContainers(const QString &serverId);
void removeContainer(const QString &serverId, int containerIndex);
void setContainerEnabled(const QString &serverId, int containerIndex, bool enabled);
void refreshContainerStatus(const QString &serverId, int containerIndex);
void refreshContainerDiagnostics(const QString &serverId, int containerIndex, int port);
void fetchContainerSecret(const QString &serverId, int containerIndex);
void clearCachedProfile(const QString &serverId, int containerIndex);
@@ -94,7 +102,7 @@ signals:
void installContainerFinished(const QString &finishMessage, bool isServiceInstall);
void installServerFinished(const QString &finishMessage);
void updateContainerFinished(const QString &message);
void updateContainerFinished(const QString &message, bool closePage);
void scanServerFinished(bool isInstalledContainerFound);
@@ -102,6 +110,11 @@ signals:
void removeServerFinished(const QString &finishedMessage);
void removeAllContainersFinished(const QString &finishedMessage);
void removeContainerFinished(const QString &finishedMessage);
void setContainerEnabledFinished(bool enabled);
void containerStatusRefreshed(int status);
void containerDiagnosticsRefreshed(bool portReachable, bool upstreamReachable, int clientsConnected,
const QString &lastConfigRefresh, const QString &statsEndpoint);
void containerSecretFetched(const QString &secret);
void installationErrorOccurred(ErrorCode errorCode);
void wrongInstallationUser(const QString &message);
@@ -114,6 +127,8 @@ signals:
void serverIsBusy(const bool isBusy);
void cancelInstallation();
void currentContainerUpdated();
void cachedProfileCleared(const QString &message);
void apiConfigRemoved(const QString &message);
@@ -138,6 +153,8 @@ private:
#endif
SftpConfigModel* m_sftpConfigModel;
Socks5ProxyConfigModel* m_socks5ConfigModel;
MtProxyConfigModel* m_mtProxyConfigModel;
TelemtConfigModel* m_telemtConfigModel;
ServerCredentials m_processedServerCredentials;

View File

@@ -510,6 +510,10 @@ QStringList ServersUiController::getAllInstalledServicesName(int serverIndex) co
servicesName.append("TOR");
} else if (container == DockerContainer::Socks5Proxy) {
servicesName.append("SOCKS5");
} else if (container == DockerContainer::MtProxy) {
servicesName.append("MTProxy");
} else if (container == DockerContainer::Telemt) {
servicesName.append("Telemt");
}
}
}

View File

@@ -74,6 +74,8 @@ QVariant ContainersModel::data(const QModelIndex &index, int role) const
case IsSftpRole: return container == DockerContainer::Sftp;
case IsTorWebsiteRole: return container == DockerContainer::TorWebSite;
case IsSocks5ProxyRole: return container == DockerContainer::Socks5Proxy;
case IsMtProxyRole: return container == DockerContainer::MtProxy;
case IsTelemtRole: return container == DockerContainer::Telemt;
case InstallPageOrderRole: return ContainerUtils::installPageOrder(container);
}
@@ -184,5 +186,7 @@ QHash<int, QByteArray> ContainersModel::roleNames() const
roles[IsSftpRole] = "isSftp";
roles[IsTorWebsiteRole] = "isTorWebsite";
roles[IsSocks5ProxyRole] = "isSocks5Proxy";
roles[IsMtProxyRole] = "isMtProxy";
roles[IsTelemtRole] = "isTelemt";
return roles;
}

View File

@@ -48,7 +48,9 @@ public:
IsDnsRole,
IsSftpRole,
IsTorWebsiteRole,
IsSocks5ProxyRole
IsSocks5ProxyRole,
IsMtProxyRole,
IsTelemtRole,
};
Q_INVOKABLE void openContainerSettings(int containerIndex);

View File

@@ -8,94 +8,575 @@
using namespace amnezia;
using namespace ProtocolUtils;
XrayConfigModel::XrayConfigModel(QObject *parent) : QAbstractListModel(parent)
XrayConfigModel::XrayConfigModel(QObject* parent) : QAbstractListModel(parent)
{
}
int XrayConfigModel::rowCount(const QModelIndex &parent) const
int XrayConfigModel::rowCount(const QModelIndex& parent) const
{
Q_UNUSED(parent);
return 1;
}
bool XrayConfigModel::setData(const QModelIndex &index, const QVariant &value, int role)
bool XrayConfigModel::setData(const QModelIndex& index, const QVariant& value, int role)
{
if (!index.isValid() || index.row() < 0 || index.row() >= ContainerUtils::allContainers().size()) {
// This model always has a single row (row 0). Using rowCount() avoids
// coupling editing ability to global container list size.
if (!index.isValid() || index.row() < 0 || index.row() >= rowCount())
{
return false;
}
QString strValue = value.toString();
const bool wasUnsavedChanges = hasUnsavedChanges();
auto& srv = m_protocolConfig.serverConfig;
auto& xhttp = srv.xhttp;
auto& mkcp = srv.mkcp;
auto& pad = xhttp.xPadding;
auto& mux = xhttp.xmux;
QString str = value.toString();
switch (role)
{
// ── Main ──────────────────────────────────────────────────────────
case Roles::SiteRole: srv.site = str;
break;
case Roles::PortRole: srv.port = str;
break;
case Roles::TransportRole: srv.transport = str;
break;
case Roles::SecurityRole: srv.security = str;
break;
case Roles::FlowRole: srv.flow = str;
break;
// ── Security ──────────────────────────────────────────────────────
case Roles::FingerprintRole: srv.fingerprint = str;
break;
case Roles::SniRole: srv.sni = str;
break;
case Roles::AlpnRole: srv.alpn = str;
break;
// ── XHTTP ─────────────────────────────────────────────────────────
case Roles::XhttpModeRole: xhttp.mode = str;
break;
case Roles::XhttpHostRole: xhttp.host = str;
break;
case Roles::XhttpPathRole: xhttp.path = str;
break;
case Roles::XhttpHeadersTemplateRole: xhttp.headersTemplate = str;
break;
case Roles::XhttpUplinkMethodRole: xhttp.uplinkMethod = str;
break;
case Roles::XhttpDisableGrpcRole: xhttp.disableGrpc = value.toBool();
break;
case Roles::XhttpDisableSseRole: xhttp.disableSse = value.toBool();
break;
case Roles::XhttpSessionPlacementRole: xhttp.sessionPlacement = str;
break;
case Roles::XhttpSessionKeyRole: xhttp.sessionKey = str;
break;
case Roles::XhttpSeqPlacementRole: xhttp.seqPlacement = str;
break;
case Roles::XhttpSeqKeyRole: xhttp.seqKey = str;
break;
case Roles::XhttpUplinkDataPlacementRole: xhttp.uplinkDataPlacement = str;
break;
case Roles::XhttpUplinkDataKeyRole: xhttp.uplinkDataKey = str;
break;
case Roles::XhttpUplinkChunkSizeRole: xhttp.uplinkChunkSize = str;
break;
case Roles::XhttpScMaxBufferedPostsRole: xhttp.scMaxBufferedPosts = str;
break;
case Roles::XhttpScMaxEachPostBytesMinRole: xhttp.scMaxEachPostBytesMin = str;
break;
case Roles::XhttpScMaxEachPostBytesMaxRole: xhttp.scMaxEachPostBytesMax = str;
break;
case Roles::XhttpScMinPostsIntervalMsMinRole: xhttp.scMinPostsIntervalMsMin = str;
break;
case Roles::XhttpScMinPostsIntervalMsMaxRole: xhttp.scMinPostsIntervalMsMax = str;
break;
case Roles::XhttpScStreamUpServerSecsMinRole: xhttp.scStreamUpServerSecsMin = str;
break;
case Roles::XhttpScStreamUpServerSecsMaxRole: xhttp.scStreamUpServerSecsMax = str;
break;
// ── mKCP ──────────────────────────────────────────────────────────
case Roles::MkcpTtiRole: mkcp.tti = str;
break;
case Roles::MkcpUplinkCapacityRole: mkcp.uplinkCapacity = str;
break;
case Roles::MkcpDownlinkCapacityRole: mkcp.downlinkCapacity = str;
break;
case Roles::MkcpReadBufferSizeRole: mkcp.readBufferSize = str;
break;
case Roles::MkcpWriteBufferSizeRole: mkcp.writeBufferSize = str;
break;
case Roles::MkcpCongestionRole: mkcp.congestion = value.toBool();
break;
// ── xPadding ──────────────────────────────────────────────────────
case Roles::XPaddingBytesMinRole: pad.bytesMin = str;
break;
case Roles::XPaddingBytesMaxRole: pad.bytesMax = str;
break;
case Roles::XPaddingObfsModeRole: pad.obfsMode = value.toBool();
break;
case Roles::XPaddingKeyRole: pad.key = str;
break;
case Roles::XPaddingHeaderRole: pad.header = str;
break;
case Roles::XPaddingPlacementRole: pad.placement = str;
break;
case Roles::XPaddingMethodRole: pad.method = str;
break;
// ── xmux ──────────────────────────────────────────────────────────
case Roles::XmuxEnabledRole: mux.enabled = value.toBool();
break;
case Roles::XmuxMaxConcurrencyMinRole: mux.maxConcurrencyMin = str;
break;
case Roles::XmuxMaxConcurrencyMaxRole: mux.maxConcurrencyMax = str;
break;
case Roles::XmuxMaxConnectionsMinRole: mux.maxConnectionsMin = str;
break;
case Roles::XmuxMaxConnectionsMaxRole: mux.maxConnectionsMax = str;
break;
case Roles::XmuxCMaxReuseTimesMinRole: mux.cMaxReuseTimesMin = str;
break;
case Roles::XmuxCMaxReuseTimesMaxRole: mux.cMaxReuseTimesMax = str;
break;
case Roles::XmuxHMaxRequestTimesMinRole: mux.hMaxRequestTimesMin = str;
break;
case Roles::XmuxHMaxRequestTimesMaxRole: mux.hMaxRequestTimesMax = str;
break;
case Roles::XmuxHMaxReusableSecsMinRole: mux.hMaxReusableSecsMin = str;
break;
case Roles::XmuxHMaxReusableSecsMaxRole: mux.hMaxReusableSecsMax = str;
break;
case Roles::XmuxHKeepAlivePeriodRole: mux.hKeepAlivePeriod = str;
break;
switch (role) {
case Roles::SiteRole: m_protocolConfig.serverConfig.site = strValue; break;
case Roles::PortRole: m_protocolConfig.serverConfig.port = strValue; break;
default:
return false;
}
emit dataChanged(index, index, QList { role });
emit dataChanged(index, index, QList{role});
if (wasUnsavedChanges != hasUnsavedChanges()) {
emit hasUnsavedChangesChanged();
}
return true;
}
QVariant XrayConfigModel::data(const QModelIndex &index, int role) const
QVariant XrayConfigModel::data(const QModelIndex& index, int role) const
{
if (!index.isValid() || index.row() < 0 || index.row() >= rowCount()) {
return QVariant();
}
switch (role) {
case Roles::SiteRole: return m_protocolConfig.serverConfig.site;
case Roles::PortRole: return m_protocolConfig.serverConfig.port;
const auto& srv = m_protocolConfig.serverConfig;
const auto& xhttp = srv.xhttp;
const auto& mkcp = srv.mkcp;
const auto& pad = xhttp.xPadding;
const auto& mux = xhttp.xmux;
switch (role)
{
// ── Main ──────────────────────────────────────────────────────────
case Roles::SiteRole: return srv.site;
case Roles::PortRole: return srv.port;
case Roles::TransportRole: return srv.transport;
case Roles::SecurityRole: return srv.security;
case Roles::FlowRole: return srv.flow;
// ── Security ──────────────────────────────────────────────────────
case Roles::FingerprintRole: return srv.fingerprint;
case Roles::SniRole: return srv.sni;
case Roles::AlpnRole: return srv.alpn;
// ── XHTTP ─────────────────────────────────────────────────────────
case Roles::XhttpModeRole: return xhttp.mode;
case Roles::XhttpHostRole: return xhttp.host;
case Roles::XhttpPathRole: return xhttp.path;
case Roles::XhttpHeadersTemplateRole: return xhttp.headersTemplate;
case Roles::XhttpUplinkMethodRole: return xhttp.uplinkMethod;
case Roles::XhttpDisableGrpcRole: return xhttp.disableGrpc;
case Roles::XhttpDisableSseRole: return xhttp.disableSse;
case Roles::XhttpSessionPlacementRole: return xhttp.sessionPlacement;
case Roles::XhttpSessionKeyRole: return xhttp.sessionKey;
case Roles::XhttpSeqPlacementRole: return xhttp.seqPlacement;
case Roles::XhttpSeqKeyRole: return xhttp.seqKey;
case Roles::XhttpUplinkDataPlacementRole: return xhttp.uplinkDataPlacement;
case Roles::XhttpUplinkDataKeyRole: return xhttp.uplinkDataKey;
case Roles::XhttpUplinkChunkSizeRole: return xhttp.uplinkChunkSize;
case Roles::XhttpScMaxBufferedPostsRole: return xhttp.scMaxBufferedPosts;
case Roles::XhttpScMaxEachPostBytesMinRole: return xhttp.scMaxEachPostBytesMin;
case Roles::XhttpScMaxEachPostBytesMaxRole: return xhttp.scMaxEachPostBytesMax;
case Roles::XhttpScMinPostsIntervalMsMinRole: return xhttp.scMinPostsIntervalMsMin;
case Roles::XhttpScMinPostsIntervalMsMaxRole: return xhttp.scMinPostsIntervalMsMax;
case Roles::XhttpScStreamUpServerSecsMinRole: return xhttp.scStreamUpServerSecsMin;
case Roles::XhttpScStreamUpServerSecsMaxRole: return xhttp.scStreamUpServerSecsMax;
// ── mKCP ──────────────────────────────────────────────────────────
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;
// ── xPadding ──────────────────────────────────────────────────────
case Roles::XPaddingBytesMinRole: return pad.bytesMin;
case Roles::XPaddingBytesMaxRole: return pad.bytesMax;
case Roles::XPaddingObfsModeRole: return pad.obfsMode;
case Roles::XPaddingKeyRole: return pad.key;
case Roles::XPaddingHeaderRole: return pad.header;
case Roles::XPaddingPlacementRole: return pad.placement;
case Roles::XPaddingMethodRole: return pad.method;
// ── xmux ──────────────────────────────────────────────────────────
case Roles::XmuxEnabledRole: return mux.enabled;
case Roles::XmuxMaxConcurrencyMinRole: return mux.maxConcurrencyMin;
case Roles::XmuxMaxConcurrencyMaxRole: return mux.maxConcurrencyMax;
case Roles::XmuxMaxConnectionsMinRole: return mux.maxConnectionsMin;
case Roles::XmuxMaxConnectionsMaxRole: return mux.maxConnectionsMax;
case Roles::XmuxCMaxReuseTimesMinRole: return mux.cMaxReuseTimesMin;
case Roles::XmuxCMaxReuseTimesMaxRole: return mux.cMaxReuseTimesMax;
case Roles::XmuxHMaxRequestTimesMinRole: return mux.hMaxRequestTimesMin;
case Roles::XmuxHMaxRequestTimesMaxRole: return mux.hMaxRequestTimesMax;
case Roles::XmuxHMaxReusableSecsMinRole: return mux.hMaxReusableSecsMin;
case Roles::XmuxHMaxReusableSecsMaxRole: return mux.hMaxReusableSecsMax;
case Roles::XmuxHKeepAlivePeriodRole: return mux.hKeepAlivePeriod;
}
return QVariant();
}
void XrayConfigModel::updateModel(amnezia::DockerContainer container, const amnezia::XrayProtocolConfig &protocolConfig)
void XrayConfigModel::updateModel(amnezia::DockerContainer container, const amnezia::XrayProtocolConfig& protocolConfig)
{
const bool wasUnsavedChanges = hasUnsavedChanges();
beginResetModel();
m_container = container;
m_protocolConfig = protocolConfig;
applyDefaultsToServerConfig(m_protocolConfig.serverConfig);
m_originalProtocolConfig = m_protocolConfig;
endResetModel();
if (wasUnsavedChanges != hasUnsavedChanges()) {
emit hasUnsavedChangesChanged();
}
}
void XrayConfigModel::applyDefaultsToServerConfig(amnezia::XrayServerConfig& config)
void XrayConfigModel::applyDefaultsToServerConfig(amnezia::XrayServerConfig &config)
{
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 (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.headersTemplate.isEmpty()) {
config.xhttp.headersTemplate = protocols::xray::defaultXhttpHeadersTemplate;
}
if (config.xhttp.uplinkMethod.isEmpty()) {
config.xhttp.uplinkMethod = protocols::xray::defaultXhttpUplinkMethod;
}
if (config.xhttp.sessionPlacement.isEmpty()) {
config.xhttp.sessionPlacement = protocols::xray::defaultXhttpSessionPlacement;
}
if (config.xhttp.sessionKey.isEmpty()) {
config.xhttp.sessionKey = protocols::xray::defaultXhttpSessionKey;
}
if (config.xhttp.seqPlacement.isEmpty()) {
config.xhttp.seqPlacement = protocols::xray::defaultXhttpSeqPlacement;
}
if (config.xhttp.uplinkDataPlacement.isEmpty()) {
config.xhttp.uplinkDataPlacement = protocols::xray::defaultXhttpUplinkDataPlacement;
}
// xPadding defaults
if (config.xhttp.xPadding.placement.isEmpty()) {
config.xhttp.xPadding.placement = protocols::xray::defaultXPaddingPlacement;
}
if (config.xhttp.xPadding.method.isEmpty()) {
config.xhttp.xPadding.method = protocols::xray::defaultXPaddingMethod;
}
}
amnezia::XrayProtocolConfig XrayConfigModel::getProtocolConfig()
{
bool serverSettingsChanged = !m_protocolConfig.serverConfig.hasEqualServerSettings(m_originalProtocolConfig.serverConfig);
const bool serverSettingsChanged =
!m_protocolConfig.serverConfig.hasEqualServerSettings(m_originalProtocolConfig.serverConfig);
if (serverSettingsChanged) {
m_protocolConfig.clearClientConfig();
}
return m_protocolConfig;
}
bool XrayConfigModel::isServerSettingsEqual() const
{
return m_protocolConfig.serverConfig.hasEqualServerSettings(m_originalProtocolConfig.serverConfig);
}
bool XrayConfigModel::hasUnsavedChanges() const
{
return !isServerSettingsEqual();
}
QHash<int, QByteArray> XrayConfigModel::roleNames() const
{
QHash<int, QByteArray> roles;
// Main
roles[SiteRole] = "site";
roles[PortRole] = "port";
roles[TransportRole] = "transport";
roles[SecurityRole] = "security";
roles[FlowRole] = "flow";
// Security
roles[FingerprintRole] = "fingerprint";
roles[SniRole] = "sni";
roles[AlpnRole] = "alpn";
// XHTTP
roles[XhttpModeRole] = "xhttpMode";
roles[XhttpHostRole] = "xhttpHost";
roles[XhttpPathRole] = "xhttpPath";
roles[XhttpHeadersTemplateRole] = "xhttpHeadersTemplate";
roles[XhttpUplinkMethodRole] = "xhttpUplinkMethod";
roles[XhttpDisableGrpcRole] = "xhttpDisableGrpc";
roles[XhttpDisableSseRole] = "xhttpDisableSse";
roles[XhttpSessionPlacementRole] = "xhttpSessionPlacement";
roles[XhttpSessionKeyRole] = "xhttpSessionKey";
roles[XhttpSeqPlacementRole] = "xhttpSeqPlacement";
roles[XhttpSeqKeyRole] = "xhttpSeqKey";
roles[XhttpUplinkDataPlacementRole] = "xhttpUplinkDataPlacement";
roles[XhttpUplinkDataKeyRole] = "xhttpUplinkDataKey";
roles[XhttpUplinkChunkSizeRole] = "xhttpUplinkChunkSize";
roles[XhttpScMaxBufferedPostsRole] = "xhttpScMaxBufferedPosts";
roles[XhttpScMaxEachPostBytesMinRole] = "xhttpScMaxEachPostBytesMin";
roles[XhttpScMaxEachPostBytesMaxRole] = "xhttpScMaxEachPostBytesMax";
roles[XhttpScMinPostsIntervalMsMinRole] = "xhttpScMinPostsIntervalMsMin";
roles[XhttpScMinPostsIntervalMsMaxRole] = "xhttpScMinPostsIntervalMsMax";
roles[XhttpScStreamUpServerSecsMinRole] = "xhttpScStreamUpServerSecsMin";
roles[XhttpScStreamUpServerSecsMaxRole] = "xhttpScStreamUpServerSecsMax";
// mKCP
roles[MkcpTtiRole] = "mkcpTti";
roles[MkcpUplinkCapacityRole] = "mkcpUplinkCapacity";
roles[MkcpDownlinkCapacityRole] = "mkcpDownlinkCapacity";
roles[MkcpReadBufferSizeRole] = "mkcpReadBufferSize";
roles[MkcpWriteBufferSizeRole] = "mkcpWriteBufferSize";
roles[MkcpCongestionRole] = "mkcpCongestion";
// xPadding
roles[XPaddingBytesMinRole] = "xPaddingBytesMin";
roles[XPaddingBytesMaxRole] = "xPaddingBytesMax";
roles[XPaddingObfsModeRole] = "xPaddingObfsMode";
roles[XPaddingKeyRole] = "xPaddingKey";
roles[XPaddingHeaderRole] = "xPaddingHeader";
roles[XPaddingPlacementRole] = "xPaddingPlacement";
roles[XPaddingMethodRole] = "xPaddingMethod";
// xmux
roles[XmuxEnabledRole] = "xmuxEnabled";
roles[XmuxMaxConcurrencyMinRole] = "xmuxMaxConcurrencyMin";
roles[XmuxMaxConcurrencyMaxRole] = "xmuxMaxConcurrencyMax";
roles[XmuxMaxConnectionsMinRole] = "xmuxMaxConnectionsMin";
roles[XmuxMaxConnectionsMaxRole] = "xmuxMaxConnectionsMax";
roles[XmuxCMaxReuseTimesMinRole] = "xmuxCMaxReuseTimesMin";
roles[XmuxCMaxReuseTimesMaxRole] = "xmuxCMaxReuseTimesMax";
roles[XmuxHMaxRequestTimesMinRole] = "xmuxHMaxRequestTimesMin";
roles[XmuxHMaxRequestTimesMaxRole] = "xmuxHMaxRequestTimesMax";
roles[XmuxHMaxReusableSecsMinRole] = "xmuxHMaxReusableSecsMin";
roles[XmuxHMaxReusableSecsMaxRole] = "xmuxHMaxReusableSecsMax";
roles[XmuxHKeepAlivePeriodRole] = "xmuxHKeepAlivePeriod";
return roles;
}
void XrayConfigModel::resetToDefaults()
{
const bool wasUnsavedChanges = hasUnsavedChanges();
beginResetModel();
m_protocolConfig.serverConfig = amnezia::XrayServerConfig{};
applyDefaultsToServerConfig(m_protocolConfig.serverConfig);
endResetModel();
if (wasUnsavedChanges != hasUnsavedChanges()) {
emit hasUnsavedChangesChanged();
}
}
void XrayConfigModel::applyServerConfig(const amnezia::XrayServerConfig &serverConfig)
{
const bool wasUnsavedChanges = hasUnsavedChanges();
beginResetModel();
m_protocolConfig.serverConfig = serverConfig;
// Clear client config since server settings changed
m_protocolConfig.clearClientConfig();
m_originalProtocolConfig = m_protocolConfig;
endResetModel();
if (wasUnsavedChanges != hasUnsavedChanges()) {
emit hasUnsavedChangesChanged();
}
}
QStringList XrayConfigModel::flowOptions()
{
return {
"", // Empty (no flow)
"xtls-rprx-vision",
"xtls-rprx-vision-udp443"
};
}
QStringList XrayConfigModel::securityOptions()
{
return { "none", "tls", "reality" };
}
QStringList XrayConfigModel::transportOptions()
{
return { "raw", "xhttp", "mkcp" };
}
QStringList XrayConfigModel::fingerprintOptions()
{
return { "chrome", "firefox", "safari", "ios", "android", "edge", "360", "qq", "random" };
}
QStringList XrayConfigModel::alpnOptions()
{
return { "HTTP/2", "HTTP/1.1", "HTTP/2,HTTP/1.1" };
}
QStringList XrayConfigModel::xhttpModeOptions()
{
return { "Auto", "Packet-up", "Stream-up", "Stream-one" };
}
QStringList XrayConfigModel::xhttpHeadersTemplateOptions()
{
return { "HTTP", "None" };
}
QStringList XrayConfigModel::xhttpUplinkMethodOptions()
{
return { "POST", "PUT", "PATCH" };
}
QStringList XrayConfigModel::xhttpSessionPlacementOptions()
{
return { "Path", "Header", "Cookie", "None" };
}
QStringList XrayConfigModel::xhttpSessionKeyOptions()
{
return { "Path", "Header", "None" };
}
QStringList XrayConfigModel::xhttpSeqPlacementOptions()
{
return { "Path", "Header", "Cookie", "None" };
}
QStringList XrayConfigModel::xhttpUplinkDataPlacementOptions()
{
// Matches splithttp uplink payload placement (packet-up / advanced)
return { "Body", "Auto", "Header", "Cookie" };
}
QStringList XrayConfigModel::xPaddingPlacementOptions()
{
// Xray-core: cookie | header | query | queryInHeader (not "body")
return { "Cookie", "Header", "Query", "Query in header" };
}
QStringList XrayConfigModel::xPaddingMethodOptions()
{
return { "Repeat-x", "Tokenish" };
}
QString XrayConfigModel::mkcpDefaultTti()
{
return QString::fromLatin1(protocols::xray::defaultMkcpTti);
}
QString XrayConfigModel::mkcpDefaultUplinkCapacity()
{
return QString::fromLatin1(protocols::xray::defaultMkcpUplinkCapacity);
}
QString XrayConfigModel::mkcpDefaultDownlinkCapacity()
{
return QString::fromLatin1(protocols::xray::defaultMkcpDownlinkCapacity);
}
QString XrayConfigModel::mkcpDefaultReadBufferSize()
{
return QString::fromLatin1(protocols::xray::defaultMkcpReadBufferSize);
}
QString XrayConfigModel::mkcpDefaultWriteBufferSize()
{
return QString::fromLatin1(protocols::xray::defaultMkcpWriteBufferSize);
}

View File

@@ -2,6 +2,7 @@
#define XRAYCONFIGMODEL_H
#include <QAbstractListModel>
#include <QStringList>
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
@@ -11,23 +12,122 @@
class XrayConfigModel : public QAbstractListModel
{
Q_OBJECT
Q_PROPERTY(bool hasUnsavedChanges READ hasUnsavedChanges NOTIFY hasUnsavedChangesChanged)
public:
enum Roles {
SiteRole,
PortRole
enum Roles
{
// ── Main page ─────────────────────────────────────────────────
SiteRole = Qt::UserRole + 1,
PortRole,
TransportRole, // "raw" | "xhttp" | "mkcp" (display in main page row)
SecurityRole, // "none" | "tls" | "reality" (display in main page row)
FlowRole, // "" | "xtls-rprx-vision" | "xtls-rprx-vision-udp443"
// ── Security ──────────────────────────────────────────────────
FingerprintRole,
SniRole,
AlpnRole,
// ── Transport — XHTTP ─────────────────────────────────────────
XhttpModeRole,
XhttpHostRole,
XhttpPathRole,
XhttpHeadersTemplateRole,
XhttpUplinkMethodRole,
XhttpDisableGrpcRole,
XhttpDisableSseRole,
// Session & Sequence
XhttpSessionPlacementRole,
XhttpSessionKeyRole,
XhttpSeqPlacementRole,
XhttpSeqKeyRole,
XhttpUplinkDataPlacementRole,
XhttpUplinkDataKeyRole,
// Traffic Shaping
XhttpUplinkChunkSizeRole,
XhttpScMaxBufferedPostsRole,
XhttpScMaxEachPostBytesMinRole,
XhttpScMaxEachPostBytesMaxRole,
XhttpScMinPostsIntervalMsMinRole,
XhttpScMinPostsIntervalMsMaxRole,
XhttpScStreamUpServerSecsMinRole,
XhttpScStreamUpServerSecsMaxRole,
// ── Transport — mKCP ──────────────────────────────────────────
MkcpTtiRole,
MkcpUplinkCapacityRole,
MkcpDownlinkCapacityRole,
MkcpReadBufferSizeRole,
MkcpWriteBufferSizeRole,
MkcpCongestionRole,
// ── xPadding ──────────────────────────────────────────────────
XPaddingBytesMinRole,
XPaddingBytesMaxRole,
XPaddingObfsModeRole,
XPaddingKeyRole,
XPaddingHeaderRole,
XPaddingPlacementRole,
XPaddingMethodRole,
// ── xmux ──────────────────────────────────────────────────────
XmuxEnabledRole,
XmuxMaxConcurrencyMinRole,
XmuxMaxConcurrencyMaxRole,
XmuxMaxConnectionsMinRole,
XmuxMaxConnectionsMaxRole,
XmuxCMaxReuseTimesMinRole,
XmuxCMaxReuseTimesMaxRole,
XmuxHMaxRequestTimesMinRole,
XmuxHMaxRequestTimesMaxRole,
XmuxHMaxReusableSecsMinRole,
XmuxHMaxReusableSecsMaxRole,
XmuxHKeepAlivePeriodRole,
};
explicit XrayConfigModel(QObject *parent = nullptr);
explicit XrayConfigModel(QObject* parent = nullptr);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
bool setData(const QModelIndex &index, const QVariant &value, int role) override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
bool setData(const QModelIndex& index, const QVariant& value, int role) override;
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
// ── Static option lists (for QML DropDown models) ─────────────────
Q_INVOKABLE static QStringList flowOptions();
Q_INVOKABLE static QStringList securityOptions();
Q_INVOKABLE static QStringList transportOptions();
Q_INVOKABLE static QStringList fingerprintOptions();
Q_INVOKABLE static QStringList alpnOptions();
Q_INVOKABLE static QStringList xhttpModeOptions();
Q_INVOKABLE static QStringList xhttpHeadersTemplateOptions();
Q_INVOKABLE static QStringList xhttpUplinkMethodOptions();
Q_INVOKABLE static QStringList xhttpSessionPlacementOptions();
Q_INVOKABLE static QStringList xhttpSessionKeyOptions();
Q_INVOKABLE static QStringList xhttpSeqPlacementOptions();
Q_INVOKABLE static QStringList xhttpUplinkDataPlacementOptions();
Q_INVOKABLE static QStringList xPaddingPlacementOptions();
Q_INVOKABLE static QStringList xPaddingMethodOptions();
// mKCP display defaults (protocolConstants.h — must match xrayConfigurator empty-field behavior)
Q_INVOKABLE static QString mkcpDefaultTti();
Q_INVOKABLE static QString mkcpDefaultUplinkCapacity();
Q_INVOKABLE static QString mkcpDefaultDownlinkCapacity();
Q_INVOKABLE static QString mkcpDefaultReadBufferSize();
Q_INVOKABLE static QString mkcpDefaultWriteBufferSize();
public slots:
void updateModel(amnezia::DockerContainer container, const amnezia::XrayProtocolConfig &protocolConfig);
void updateModel(amnezia::DockerContainer container, const amnezia::XrayProtocolConfig& protocolConfig);
amnezia::XrayProtocolConfig getProtocolConfig();
bool isServerSettingsEqual() const;
bool hasUnsavedChanges() const;
void resetToDefaults();
void applyServerConfig(const amnezia::XrayServerConfig &serverConfig);
signals:
void hasUnsavedChangesChanged();
protected:
QHash<int, QByteArray> roleNames() const override;
@@ -36,7 +136,7 @@ private:
amnezia::DockerContainer m_container;
amnezia::XrayProtocolConfig m_protocolConfig;
amnezia::XrayProtocolConfig m_originalProtocolConfig;
void applyDefaultsToServerConfig(amnezia::XrayServerConfig& config);
};

View File

@@ -0,0 +1,216 @@
#include "xrayConfigSnapshotsModel.h"
#include <QJsonDocument>
#include <QUuid>
#include "core/repositories/secureAppSettingsRepository.h"
#include "core/utils/constants/configKeys.h"
QJsonObject XrayConfigSnapshot::toJson() const
{
QJsonObject obj;
obj["id"] = id;
obj["displayName"] = displayName;
obj["createdAt"] = createdAt.toString(Qt::ISODate);
obj["serverConfig"] = serverConfig.toJson();
return obj;
}
XrayConfigSnapshot XrayConfigSnapshot::fromJson(const QJsonObject &json)
{
XrayConfigSnapshot s;
s.id = json.value("id").toString();
s.displayName = json.value("displayName").toString();
s.createdAt = QDateTime::fromString(json.value("createdAt").toString(), Qt::ISODate);
s.serverConfig = amnezia::XrayServerConfig::fromJson(json.value("serverConfig").toObject());
return s;
}
XrayConfigSnapshotsModel::XrayConfigSnapshotsModel(SecureAppSettingsRepository *appSettings,
XrayConfigModel *xrayConfigModel, QObject *parent)
: QAbstractListModel(parent), m_appSettings(appSettings), m_xrayConfigModel(xrayConfigModel)
{
loadAll();
}
void XrayConfigSnapshotsModel::loadAll()
{
m_configs.clear();
QByteArray raw = m_appSettings->xraySavedConfigs();
if (raw.isEmpty()) {
return;
}
QJsonArray arr = QJsonDocument::fromJson(raw).array();
for (const QJsonValue &v : arr) {
m_configs.append(XrayConfigSnapshot::fromJson(v.toObject()));
}
}
void XrayConfigSnapshotsModel::persistAll()
{
QJsonArray arr;
for (const XrayConfigSnapshot &s : m_configs) {
arr.append(s.toJson());
}
m_appSettings->setXraySavedConfigs(QJsonDocument(arr).toJson(QJsonDocument::Compact));
}
int XrayConfigSnapshotsModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return m_configs.size();
}
QVariant XrayConfigSnapshotsModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || index.row() < 0 || index.row() >= m_configs.size()) {
return QVariant();
}
const XrayConfigSnapshot &s = m_configs.at(index.row());
switch (role) {
case IdRole: {
return s.id;
}
case DisplayNameRole: {
return s.displayName;
}
case CreatedAtRole: {
return s.createdAt.toString("dd.MM.yyyy HH:mm");
}
}
return QVariant();
}
QHash<int, QByteArray> XrayConfigSnapshotsModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[IdRole] = "configId";
roles[DisplayNameRole] = "configName";
roles[CreatedAtRole] = "configDate";
return roles;
}
void XrayConfigSnapshotsModel::reload()
{
beginResetModel();
loadAll();
endResetModel();
}
void XrayConfigSnapshotsModel::createFromCurrent(const amnezia::XrayServerConfig &serverConfig)
{
XrayConfigSnapshot snapshot;
snapshot.id = QUuid::createUuid().toString(QUuid::WithoutBraces);
snapshot.displayName = buildDisplayName(serverConfig);
snapshot.createdAt = QDateTime::currentDateTime();
snapshot.serverConfig = serverConfig;
beginInsertRows(QModelIndex(), m_configs.size(), m_configs.size());
m_configs.append(snapshot);
endInsertRows();
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()) {
return;
}
beginRemoveRows(QModelIndex(), index, index);
m_configs.removeAt(index);
endRemoveRows();
persistAll();
emit configRemoved(index);
}
QString XrayConfigSnapshotsModel::exportToJson(int index) const
{
if (index < 0 || index >= m_configs.size()) {
return {};
}
return QString::fromUtf8(QJsonDocument(m_configs.at(index).toJson()).toJson(QJsonDocument::Indented));
}
bool XrayConfigSnapshotsModel::importFromJson(const QString &jsonString)
{
QJsonDocument doc = QJsonDocument::fromJson(jsonString.toUtf8());
if (!doc.isObject()) {
emit importFailed(tr("Invalid JSON format"));
return false;
}
XrayConfigSnapshot snapshot = XrayConfigSnapshot::fromJson(doc.object());
if (snapshot.id.isEmpty()) {
snapshot.id = QUuid::createUuid().toString(QUuid::WithoutBraces);
}
if (snapshot.displayName.isEmpty()) {
snapshot.displayName = buildDisplayName(snapshot.serverConfig);
}
snapshot.createdAt = QDateTime::currentDateTime();
beginInsertRows(QModelIndex(), m_configs.size(), m_configs.size());
m_configs.append(snapshot);
endInsertRows();
persistAll();
return true;
}
QString XrayConfigSnapshotsModel::buildDisplayName(const amnezia::XrayServerConfig &cfg)
{
// Build a human-readable name: "XHTTP TLS Reality", "RAW Reality", etc.
QString transport;
if (cfg.transport == "xhttp") {
transport = "XHTTP";
} else if (cfg.transport == "mkcp") {
transport = "mKCP";
} else {
transport = "RAW (TCP)";
}
QString security;
if (cfg.security == "tls") {
security = "TLS";
} else if (cfg.security == "reality") {
security = "Reality";
} else {
security = "None";
}
return QString("%1 %2").arg(transport, security).trimmed();
}
void XrayConfigSnapshotsModel::createFromCurrentModel()
{
if (!m_xrayConfigModel) {
return;
}
createFromCurrent(m_xrayConfigModel->getProtocolConfig().serverConfig);
}
void XrayConfigSnapshotsModel::applyConfigToCurrentModel(int index)
{
if (!m_xrayConfigModel) {
return;
}
amnezia::XrayServerConfig cfg = applyConfig(index);
if (cfg.port.isEmpty()) {
return; // guard against invalid index
}
m_xrayConfigModel->applyServerConfig(cfg);
}

View File

@@ -0,0 +1,76 @@
#ifndef XRAYCONFIGSMODEL_H
#define XRAYCONFIGSMODEL_H
#include <QAbstractListModel>
#include <QDateTime>
#include <QJsonArray>
#include <QJsonObject>
#include <QString>
#include <QVector>
#include "core/models/protocols/xrayProtocolConfig.h"
#include "ui/models/protocols/xrayConfigModel.h"
class SecureAppSettingsRepository;
struct XrayConfigSnapshot
{
QString id;
QString displayName; // auto-generated: "XHTTP TLS Reality", "RAW Reality", etc.
QDateTime createdAt;
amnezia::XrayServerConfig serverConfig;
QJsonObject toJson() const;
static XrayConfigSnapshot fromJson(const QJsonObject &json);
};
class XrayConfigSnapshotsModel : public QAbstractListModel
{
Q_OBJECT
public:
enum Roles {
IdRole = Qt::UserRole + 1,
DisplayNameRole,
CreatedAtRole, // "dd.MM.yyyy HH:mm"
};
explicit XrayConfigSnapshotsModel(SecureAppSettingsRepository *appSettings, XrayConfigModel *xrayConfigModel,
QObject *parent = nullptr);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
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;
Q_INVOKABLE bool importFromJson(const QString &jsonString);
// Convenience: create snapshot from live model, apply snapshot back to model
Q_INVOKABLE void createFromCurrentModel();
Q_INVOKABLE void applyConfigToCurrentModel(int index);
signals:
void configApplied(int index);
void configRemoved(int index);
void importFailed(const QString &errorMessage);
protected:
QHash<int, QByteArray> roleNames() const override;
private:
SecureAppSettingsRepository *m_appSettings;
XrayConfigModel *m_xrayConfigModel;
QVector<XrayConfigSnapshot> m_configs;
void persistAll();
void loadAll();
static QString buildDisplayName(const amnezia::XrayServerConfig &cfg);
};
#endif // XRAYCONFIGSMODEL_H

View File

@@ -42,6 +42,8 @@ QHash<int, QByteArray> ProtocolsModel::roleNames() const
roles[IsSftpRole] = "isSftp";
roles[IsIpsecRole] = "isIpsec";
roles[IsSocks5ProxyRole] = "isSocks5Proxy";
roles[IsMtProxyRole] = "isMtProxy";
roles[IsTelemtRole] = "isTelemt";
return roles;
}
@@ -71,6 +73,8 @@ QVariant ProtocolsModel::data(const QModelIndex &index, int role) const
case IsSftpRole: return proto == Proto::Sftp;
case IsIpsecRole: return proto == Proto::Ikev2;
case IsSocks5ProxyRole: return proto == Proto::Socks5Proxy;
case IsMtProxyRole: return proto == Proto::MtProxy;
case IsTelemtRole: return proto == Proto::Telemt;
case RawConfigRole:
return getRawConfig();
case IsClientProtocolExistsRole:
@@ -124,6 +128,8 @@ PageLoader::PageEnum ProtocolsModel::serverProtocolPage(Proto protocol) const
case Proto::Dns: return PageLoader::PageEnum::PageServiceDnsSettings;
case Proto::Sftp: return PageLoader::PageEnum::PageServiceSftpSettings;
case Proto::Socks5Proxy: return PageLoader::PageEnum::PageServiceSocksProxySettings;
case Proto::MtProxy: return PageLoader::PageEnum::PageServiceMtProxySettings;
case Proto::Telemt: return PageLoader::PageEnum::PageServiceTelemtSettings;
default: return PageLoader::PageEnum::PageProtocolOpenVpnSettings;
}
}

View File

@@ -25,7 +25,9 @@ public:
IsXrayRole,
IsSftpRole,
IsIpsecRole,
IsSocks5ProxyRole
IsSocks5ProxyRole,
IsMtProxyRole,
IsTelemtRole,
};
explicit ProtocolsModel(QObject *parent = nullptr);

View File

@@ -0,0 +1,714 @@
#include "mtProxyConfigModel.h"
#include "ui/models/utils/mtproxy_public_host_input.h"
#include "core/utils/networkUtilities.h"
#include "core/utils/qrCodeUtils.h"
#include "core/utils/constants/protocolConstants.h"
#include "core/utils/constants/configKeys.h"
#include "qrcodegen.hpp"
#include <QClipboard>
#include <QGuiApplication>
#include <QHostAddress>
#include <QRegExp>
#include <QRegularExpression>
#include <QtGlobal>
#include <qqml.h>
using namespace amnezia;
MtProxyConfigModel::MtProxyConfigModel(QObject *parent) : QAbstractListModel(parent) {
qmlRegisterType<PublicHostInputValidator>("MtProxyConfig", 1, 0, "PublicHostInputValidator");
}
int MtProxyConfigModel::rowCount(const QModelIndex &parent) const {
Q_UNUSED(parent);
return 1;
}
bool MtProxyConfigModel::setData(const QModelIndex &index, const QVariant &value, int role) {
if (!index.isValid() || index.row() != 0) {
return false;
}
switch (role) {
case Roles::PortRole: {
m_protocolConfig.port = value.toString();
break;
}
case Roles::SecretRole: {
m_protocolConfig.secret = value.toString();
break;
}
case Roles::TagRole: {
const QString tag = sanitizeMtProxyTagFieldText(value.toString());
if (!isValidMtProxyTag(tag)) {
return false;
}
m_protocolConfig.tag = tag;
break;
}
case Roles::IsEnabledRole: {
m_protocolConfig.isEnabled = value.toBool();
break;
}
case Roles::PublicHostRole: {
const QString h = value.toString().trimmed();
if (!isValidPublicHost(h)) {
return false;
}
m_protocolConfig.publicHost = h;
break;
}
case Roles::TransportModeRole: {
m_protocolConfig.transportMode = value.toString();
break;
}
case Roles::TlsDomainRole: {
const QString d = value.toString().trimmed();
if (!isValidFakeTlsDomain(d)) {
return false;
}
m_protocolConfig.tlsDomain = d;
break;
}
case Roles::AdditionalSecretsRole: {
m_protocolConfig.additionalSecrets = value.toStringList();
break;
}
case Roles::WorkersModeRole: {
m_protocolConfig.workersMode = value.toString();
break;
}
case Roles::WorkersRole: {
m_protocolConfig.workers = value.toString();
break;
}
case Roles::NatEnabledRole: {
m_protocolConfig.natEnabled = value.toBool();
break;
}
case Roles::NatInternalIpRole: {
const QString ip = value.toString().trimmed();
if (!isValidOptionalIpv4(ip)) {
return false;
}
m_protocolConfig.natInternalIp = ip;
break;
}
case Roles::NatExternalIpRole: {
const QString ip = value.toString().trimmed();
if (!isValidOptionalIpv4(ip)) {
return false;
}
m_protocolConfig.natExternalIp = ip;
break;
}
default: {
return false;
}
}
emit dataChanged(index, index, QList{role});
return true;
}
QVariant MtProxyConfigModel::data(const QModelIndex &index, int role) const {
if (!index.isValid() || index.row() != 0) {
return QVariant();
}
switch (role) {
case Roles::PortRole: {
return m_protocolConfig.port.isEmpty() ? QString(protocols::mtProxy::defaultPort) : m_protocolConfig.port;
}
case Roles::SecretRole: {
return m_protocolConfig.secret;
}
case Roles::TagRole: {
return m_protocolConfig.tag;
}
case Roles::TgLinkRole: {
return m_protocolConfig.tgLink;
}
case Roles::TmeLinkRole: {
return m_protocolConfig.tmeLink;
}
case Roles::IsEnabledRole: {
return m_protocolConfig.isEnabled;
}
case Roles::PublicHostRole: {
return m_protocolConfig.publicHost.isEmpty()
? m_fullConfig.value(configKey::hostName).toString()
: m_protocolConfig.publicHost;
}
case Roles::TransportModeRole: {
return m_protocolConfig.transportMode.isEmpty()
? QString(protocols::mtProxy::transportModeStandard)
: m_protocolConfig.transportMode;
}
case Roles::TlsDomainRole: {
return m_protocolConfig.tlsDomain;
}
case Roles::AdditionalSecretsRole: {
return m_protocolConfig.additionalSecrets;
}
case Roles::WorkersModeRole: {
return m_protocolConfig.workersMode.isEmpty()
? QString(protocols::mtProxy::workersModeAuto)
: m_protocolConfig.workersMode;
}
case Roles::WorkersRole: {
return m_protocolConfig.workers.isEmpty() ? QString(protocols::mtProxy::defaultWorkers)
: m_protocolConfig.workers;
}
case Roles::NatEnabledRole: {
return m_protocolConfig.natEnabled;
}
case Roles::NatInternalIpRole: {
return m_protocolConfig.natInternalIp;
}
case Roles::NatExternalIpRole: {
return m_protocolConfig.natExternalIp;
}
}
return QVariant();
}
void MtProxyConfigModel::updateModel(amnezia::DockerContainer container,
const amnezia::MtProxyProtocolConfig &protocolConfig) {
beginResetModel();
m_container = container;
m_protocolConfig = protocolConfig;
endResetModel();
}
void MtProxyConfigModel::updateModel(const QJsonObject &config) {
beginResetModel();
m_fullConfig = config;
m_protocolConfig = MtProxyProtocolConfig::fromJson(config.value(configKey::mtproxy).toObject());
if (m_protocolConfig.port.isEmpty()) m_protocolConfig.port = protocols::mtProxy::defaultPort;
if (m_protocolConfig.transportMode.isEmpty()) m_protocolConfig.transportMode = protocols::mtProxy::transportModeStandard;
if (m_protocolConfig.workersMode.isEmpty()) m_protocolConfig.workersMode = protocols::mtProxy::workersModeAuto;
if (m_protocolConfig.workers.isEmpty()) m_protocolConfig.workers = protocols::mtProxy::defaultWorkers;
{
QString tagIn = sanitizeMtProxyTagFieldText(m_protocolConfig.tag);
if (!isValidMtProxyTag(tagIn)) {
tagIn.clear();
}
m_protocolConfig.tag = tagIn;
}
endResetModel();
}
QJsonObject MtProxyConfigModel::getConfig() {
m_fullConfig.insert(configKey::mtproxy, m_protocolConfig.toJson());
return m_fullConfig;
}
void MtProxyConfigModel::generateSecret() {
// Generate 16 random bytes = 32 hex chars
QString secret;
for (int i = 0; i < 16; ++i) {
quint32 byte = QRandomGenerator::global()->bounded(256);
secret += QString("%1").arg(byte, 2, 16, QChar('0'));
}
m_protocolConfig.secret = secret;
emit dataChanged(index(0), index(0), QList<int>{SecretRole});
}
void MtProxyConfigModel::setSecret(const QString &secret) {
if (secret.isEmpty()) {
return;
}
setData(index(0), secret, SecretRole);
}
bool MtProxyConfigModel::validateAndSetSecret(const QString &rawSecret) {
if (!QRegularExpression("^[0-9a-fA-F]{32}$").match(rawSecret).hasMatch()) {
return false;
}
setData(index(0), rawSecret, SecretRole);
return true;
}
void MtProxyConfigModel::setPort(const QString &port) {
setData(index(0), port, PortRole);
}
void MtProxyConfigModel::setTag(const QString &tag) {
setData(index(0), tag, TagRole);
}
void MtProxyConfigModel::setPublicHost(const QString &host) {
const QString t = host.trimmed();
if (!isValidPublicHost(t)) {
return;
}
setData(index(0), t, PublicHostRole);
}
void MtProxyConfigModel::setTransportMode(const QString &mode) {
setData(index(0), mode, TransportModeRole);
}
QString MtProxyConfigModel::getTransportMode() const {
return m_protocolConfig.transportMode.isEmpty()
? QString(protocols::mtProxy::transportModeStandard)
: m_protocolConfig.transportMode;
}
QString MtProxyConfigModel::getTlsDomain() const {
return m_protocolConfig.tlsDomain.isEmpty()
? QString(protocols::mtProxy::defaultTlsDomain)
: m_protocolConfig.tlsDomain;
}
QString MtProxyConfigModel::getPublicHost() const {
return m_protocolConfig.publicHost;
}
void MtProxyConfigModel::setTlsDomain(const QString &domain) {
const QString t = domain.trimmed();
if (!isValidFakeTlsDomain(t)) {
return;
}
setData(index(0), t, TlsDomainRole);
}
void MtProxyConfigModel::setWorkersMode(const QString &mode) {
setData(index(0), mode, WorkersModeRole);
}
void MtProxyConfigModel::setWorkers(const QString &workers) {
setData(index(0), workers, WorkersRole);
}
void MtProxyConfigModel::setNatEnabled(bool enabled) {
setData(index(0), enabled, NatEnabledRole);
}
void MtProxyConfigModel::setNatInternalIp(const QString &ip) {
const QString t = ip.trimmed();
if (!isValidOptionalIpv4(t)) {
return;
}
setData(index(0), t, NatInternalIpRole);
}
void MtProxyConfigModel::setNatExternalIp(const QString &ip) {
const QString t = ip.trimmed();
if (!isValidOptionalIpv4(t)) {
return;
}
setData(index(0), t, NatExternalIpRole);
}
void MtProxyConfigModel::addAdditionalSecret() {
QString newSecret;
for (int i = 0; i < 16; ++i) {
quint32 byte = QRandomGenerator::global()->bounded(256);
newSecret += QString("%1").arg(byte, 2, 16, QChar('0'));
}
m_protocolConfig.additionalSecrets.append(newSecret);
emit dataChanged(index(0), index(0), QList<int>{AdditionalSecretsRole});
}
void MtProxyConfigModel::removeAdditionalSecret(int idx) {
if (idx < 0 || idx >= m_protocolConfig.additionalSecrets.size()) {
return;
}
m_protocolConfig.additionalSecrets.removeAt(idx);
emit dataChanged(index(0), index(0), QList<int>{AdditionalSecretsRole});
}
QVariantList MtProxyConfigModel::additionalSecretsList() const {
QVariantList out;
out.reserve(m_protocolConfig.additionalSecrets.size());
for (const auto &s: m_protocolConfig.additionalSecrets) {
if (!s.isEmpty()) {
out.append(s);
}
}
return out;
}
void MtProxyConfigModel::setEnabled(bool enabled) {
m_protocolConfig.isEnabled = enabled;
emit dataChanged(index(0), index(0), QList<int>{IsEnabledRole});
}
QString MtProxyConfigModel::generateQrCode(const QString &text) {
if (text.isEmpty()) {
return "";
}
auto qr = qrCodeUtils::generateQrCode(text.toUtf8());
return qrCodeUtils::svgToBase64(QString::fromStdString(toSvgString(qr, 1)));
}
QString MtProxyConfigModel::defaultTlsDomain() const {
return protocols::mtProxy::defaultTlsDomain;
}
QString MtProxyConfigModel::defaultPort() const {
return protocols::mtProxy::defaultPort;
}
QString MtProxyConfigModel::defaultWorkers() const {
return protocols::mtProxy::defaultWorkers;
}
int MtProxyConfigModel::maxWorkers() const {
return protocols::mtProxy::maxWorkers;
}
QString MtProxyConfigModel::transportModeStandard() const {
return protocols::mtProxy::transportModeStandard;
}
QString MtProxyConfigModel::transportModeFakeTLS() const {
return protocols::mtProxy::transportModeFakeTLS;
}
QString MtProxyConfigModel::workersModeAuto() const {
return protocols::mtProxy::workersModeAuto;
}
QString MtProxyConfigModel::workersModeManual() const {
return protocols::mtProxy::workersModeManual;
}
bool MtProxyConfigModel::isValidPublicHost(const QString &host) const {
const QString t = host.trimmed();
if (t.isEmpty()) {
return true;
}
if (t.length() > 253) {
return false;
}
QHostAddress a(t);
if (a.protocol() == QHostAddress::IPv4Protocol) {
return NetworkUtilities::checkIPv4Format(t);
}
if (a.protocol() == QHostAddress::IPv6Protocol) {
return true;
}
static const QRegularExpression onlyAsciiDigits(QStringLiteral(R"(^\d+$)"));
if (onlyAsciiDigits.match(t).hasMatch()) {
return false;
}
return NetworkUtilities::domainRegExp().exactMatch(t);
}
bool MtProxyConfigModel::isPublicHostInputAllowed(const QString &text) const {
return mtproxyPublicHostInputAllowed(text);
}
bool MtProxyConfigModel::isPublicHostTypingIncomplete(const QString &text) const {
const QString t = text.trimmed();
if (isValidPublicHost(t)) {
return false;
}
static const QRegularExpression onlyDigitDot(QStringLiteral(R"(^[0-9.]+$)"));
if (onlyDigitDot.match(t).hasMatch()) {
if (t.endsWith(QLatin1Char('.'))) {
return true;
}
const QStringList parts = t.split(QLatin1Char('.'), Qt::KeepEmptyParts);
if (parts.size() < 4) {
return true;
}
for (const QString &part: parts) {
if (part.isEmpty()) {
return true;
}
}
return false;
}
if (t.contains(QLatin1Char(':'))) {
if (t.contains(QLatin1String(":::"))) {
return false;
}
if (t.endsWith(QLatin1Char(':'))) {
return true;
}
QHostAddress a(t);
if (a.protocol() == QHostAddress::IPv6Protocol) {
return false;
}
if (!t.contains(QLatin1String("::")) && t.count(QLatin1Char(':')) < 7 && !t.contains(QLatin1Char('.'))) {
return true;
}
return false;
}
if (!t.contains(QLatin1Char('.'))) {
return true;
}
return false;
}
bool MtProxyConfigModel::isValidMtProxyTag(const QString &tag) const {
if (tag.isEmpty()) {
return true;
}
static const QRegularExpression re(
QStringLiteral("^([0-9a-fA-F]{%1})$").arg(protocols::mtProxy::botTagHexLength));
return re.match(tag).hasMatch();
}
bool MtProxyConfigModel::isMtProxyTagTypingIncomplete(const QString &text) const {
const QString t = text.trimmed();
if (t.isEmpty()) {
return true;
}
static const QRegularExpression hexOnly(QStringLiteral(R"(^[0-9a-fA-F]*$)"));
if (!hexOnly.match(t).hasMatch()) {
return false;
}
return t.size() < protocols::mtProxy::botTagHexLength;
}
int MtProxyConfigModel::mtProxyBotTagHexLength() const {
return protocols::mtProxy::botTagHexLength;
}
bool MtProxyConfigModel::isValidFakeTlsDomain(const QString &domain) const {
const QString t = domain.trimmed();
if (t.isEmpty()) {
return true;
}
if (t.length() > 253) {
return false;
}
QHostAddress addr;
if (addr.setAddress(t)) {
return false;
}
static const QRegularExpression onlyAsciiDigits(QStringLiteral(R"(^\d+$)"));
if (onlyAsciiDigits.match(t).hasMatch()) {
return false;
}
QRegExp re(NetworkUtilities::domainRegExp());
re.setCaseSensitivity(Qt::CaseInsensitive);
if (!re.exactMatch(t)) {
return false;
}
// ee + 32 hex (base secret) + hex(UTF-8 domain); keep headroom under typical client limits.
if (t.toUtf8().size() > 111) {
return false;
}
return true;
}
QString MtProxyConfigModel::clipboardText() const {
if (QClipboard *c = QGuiApplication::clipboard()) {
return c->text();
}
return QString();
}
QString MtProxyConfigModel::sanitizeFakeTlsDomainFieldText(const QString &input) const {
const QString t = normalizeFakeTlsDomainInput(input);
QString out;
out.reserve(t.size());
for (const QChar &c: t) {
const ushort u = c.unicode();
const bool letter = (u >= 'a' && u <= 'z') || (u >= 'A' && u <= 'Z');
const bool digit = (u >= '0' && u <= '9');
if (letter || digit || u == '.' || u == '-') {
out.append(c);
}
}
if (out.size() > 253) {
out.truncate(253);
}
return out;
}
bool MtProxyConfigModel::isFakeTlsDomainInputAllowed(const QString &text) const {
if (text.length() > 253) {
return false;
}
static const QRegularExpression re(QStringLiteral(R"(^[a-zA-Z0-9.-]*$)"));
return re.match(text).hasMatch();
}
QString MtProxyConfigModel::sanitizePublicHostFieldText(const QString &input) const {
QString out;
const int cap = qMin(input.size(), 253);
out.reserve(cap);
for (const QChar &c: input) {
if (out.size() >= 253) {
break;
}
const ushort u = c.unicode();
if ((u >= 'a' && u <= 'z') || (u >= 'A' && u <= 'Z') || (u >= '0' && u <= '9') || u == '.' || u == ':' ||
u == '-') {
out.append(c);
}
}
return out;
}
QString MtProxyConfigModel::sanitizePortFieldText(const QString &input) const {
QString out;
out.reserve(qMin(input.size(), 5));
for (const QChar &c: input) {
const ushort u = c.unicode();
if (u >= '0' && u <= '9' && out.size() < 5) {
out.append(c);
}
}
return out;
}
QString MtProxyConfigModel::sanitizeMtProxyTagFieldText(const QString &input) const {
QString trimmed = input.trimmed();
if (trimmed.startsWith(QLatin1String("0x"), Qt::CaseInsensitive)) {
trimmed = trimmed.mid(2).trimmed();
}
// Prefer a contiguous 32-hex run (paste from bot message with extra text).
static const QRegularExpression runHex(QStringLiteral(R"(([0-9a-fA-F]{32}))"));
const QRegularExpressionMatch m = runHex.match(trimmed);
if (m.hasMatch()) {
return m.captured(1);
}
const int cap = protocols::mtProxy::botTagHexLength;
QString out;
out.reserve(qMin(trimmed.size(), cap));
for (const QChar &c: trimmed) {
if (out.size() >= cap) {
break;
}
const ushort u = c.unicode();
if ((u >= '0' && u <= '9') || (u >= 'a' && u <= 'f') || (u >= 'A' && u <= 'F')) {
out.append(c);
}
}
return out;
}
QString MtProxyConfigModel::sanitizeWorkersFieldText(const QString &input) const {
QString out;
out.reserve(qMin(input.size(), 3));
for (const QChar &c: input) {
const ushort u = c.unicode();
if (u >= '0' && u <= '9' && out.size() < 3) {
out.append(c);
}
}
return out;
}
QString MtProxyConfigModel::sanitizeOptionalIpv4FieldText(const QString &input) const {
QString out;
out.reserve(qMin(input.size(), 15));
for (const QChar &c: input) {
if (out.size() >= 15) {
break;
}
const ushort u = c.unicode();
if ((u >= '0' && u <= '9') || u == '.') {
out.append(c);
}
}
return out;
}
QString MtProxyConfigModel::normalizeFakeTlsDomainInput(const QString &input) const {
QString t = input.trimmed();
if (t.startsWith(QLatin1String("https://"), Qt::CaseInsensitive)) {
t = t.mid(8);
} else if (t.startsWith(QLatin1String("http://"), Qt::CaseInsensitive)) {
t = t.mid(7);
}
if (const int slash = t.indexOf(QLatin1Char('/')); slash >= 0) {
t = t.left(slash);
}
if (const int at = t.indexOf(QLatin1Char('@')); at >= 0) {
t = t.mid(at + 1);
}
if (const int colon = t.indexOf(QLatin1Char(':')); colon >= 0) {
t = t.left(colon);
}
if (t.startsWith(QLatin1String("www."), Qt::CaseInsensitive)) {
const QString rest = t.mid(4);
if (rest.contains(QLatin1Char('.'))) {
t = rest;
}
}
return t.trimmed();
}
bool MtProxyConfigModel::isFakeTlsDomainTypingIncomplete(const QString &text) const {
const QString t = text.trimmed();
if (t.isEmpty()) {
return true;
}
if (isValidFakeTlsDomain(t)) {
return false;
}
if (t.contains(QLatin1Char('/')) || t.contains(QLatin1Char(':')) || t.contains(QLatin1Char('@'))
|| t.contains(QLatin1Char(' '))) {
return false;
}
if (t.contains(QLatin1String(".."))) {
return false;
}
if (!t.contains(QLatin1Char('.'))) {
return true;
}
if (t.endsWith(QLatin1Char('.'))) {
return true;
}
static const QRegularExpression legalPartial(QStringLiteral(R"(^[a-zA-Z0-9.-]*$)"));
if (!legalPartial.match(t).hasMatch()) {
return false;
}
return true;
}
bool MtProxyConfigModel::isValidOptionalIpv4(const QString &ip) const {
const QString t = ip.trimmed();
if (t.isEmpty()) {
return true;
}
return NetworkUtilities::checkIPv4Format(t);
}
QHash<int, QByteArray> MtProxyConfigModel::roleNames() const {
QHash<int, QByteArray> roles;
roles[PortRole] = "port";
roles[SecretRole] = "secret";
roles[TagRole] = "tag";
roles[TgLinkRole] = "tgLink";
roles[TmeLinkRole] = "tmeLink";
roles[IsEnabledRole] = "isEnabled";
roles[PublicHostRole] = "publicHost";
roles[TransportModeRole] = "transportMode";
roles[TlsDomainRole] = "tlsDomain";
roles[AdditionalSecretsRole] = "additionalSecrets";
roles[WorkersModeRole] = "workersMode";
roles[WorkersRole] = "workers";
roles[NatEnabledRole] = "natEnabled";
roles[NatInternalIpRole] = "natInternalIp";
roles[NatExternalIpRole] = "natExternalIp";
return roles;
}
amnezia::MtProxyProtocolConfig MtProxyConfigModel::getProtocolConfig() {
return m_protocolConfig;
}

View File

@@ -0,0 +1,156 @@
#ifndef MTPROXYCONFIGMODEL_H
#define MTPROXYCONFIGMODEL_H
#include <QAbstractListModel>
#include <QJsonArray>
#include <QJsonObject>
#include <QRandomGenerator>
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
#include "core/utils/protocolEnum.h"
#include "core/models/protocols/mtProxyProtocolConfig.h"
class MtProxyConfigModel : public QAbstractListModel {
Q_OBJECT
public:
enum Roles {
PortRole = Qt::UserRole + 1,
SecretRole,
TagRole,
TgLinkRole,
TmeLinkRole,
IsEnabledRole,
PublicHostRole,
TransportModeRole,
TlsDomainRole,
AdditionalSecretsRole,
WorkersModeRole,
WorkersRole,
NatEnabledRole,
NatInternalIpRole,
NatExternalIpRole
};
explicit MtProxyConfigModel(QObject *parent = nullptr);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
bool setData(const QModelIndex &index, const QVariant &value, int role) override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
public slots:
void updateModel(amnezia::DockerContainer container, const amnezia::MtProxyProtocolConfig &protocolConfig);
void updateModel(const QJsonObject &config);
QJsonObject getConfig();
amnezia::MtProxyProtocolConfig getProtocolConfig();
Q_INVOKABLE void generateSecret();
Q_INVOKABLE void setSecret(const QString &secret);
Q_INVOKABLE bool validateAndSetSecret(const QString &rawSecret);
Q_INVOKABLE void setPort(const QString &port);
Q_INVOKABLE void setTag(const QString &tag);
Q_INVOKABLE void setPublicHost(const QString &host);
Q_INVOKABLE void setTransportMode(const QString &mode);
Q_INVOKABLE QString getTransportMode() const;
Q_INVOKABLE QString getTlsDomain() const;
Q_INVOKABLE QString getPublicHost() const;
Q_INVOKABLE void setTlsDomain(const QString &domain);
Q_INVOKABLE void setWorkersMode(const QString &mode);
Q_INVOKABLE void setWorkers(const QString &workers);
Q_INVOKABLE void setNatEnabled(bool enabled);
Q_INVOKABLE void setNatInternalIp(const QString &ip);
Q_INVOKABLE void setNatExternalIp(const QString &ip);
Q_INVOKABLE void addAdditionalSecret();
Q_INVOKABLE void removeAdditionalSecret(int idx);
/// Current `mtproxy_additional_secrets` list from in-memory config (for QML snapshot vs. unsaved adds).
Q_INVOKABLE QVariantList additionalSecretsList() const;
Q_INVOKABLE QString generateQrCode(const QString &text);
Q_INVOKABLE void setEnabled(bool enabled);
Q_INVOKABLE QString defaultTlsDomain() const;
Q_INVOKABLE QString defaultPort() const;
Q_INVOKABLE QString defaultWorkers() const;
Q_INVOKABLE int maxWorkers() const;
Q_INVOKABLE QString transportModeStandard() const;
Q_INVOKABLE QString transportModeFakeTLS() const;
Q_INVOKABLE QString workersModeAuto() const;
Q_INVOKABLE QString workersModeManual() const;
Q_INVOKABLE bool isValidPublicHost(const QString &host) const;
Q_INVOKABLE bool isPublicHostInputAllowed(const QString &text) const;
Q_INVOKABLE bool isPublicHostTypingIncomplete(const QString &text) const;
Q_INVOKABLE bool isValidMtProxyTag(const QString &tag) const;
Q_INVOKABLE bool isMtProxyTagTypingIncomplete(const QString &text) const;
Q_INVOKABLE int mtProxyBotTagHexLength() const;
Q_INVOKABLE bool isValidFakeTlsDomain(const QString &domain) const;
Q_INVOKABLE QString normalizeFakeTlsDomainInput(const QString &input) const;
Q_INVOKABLE QString sanitizeFakeTlsDomainFieldText(const QString &input) const;
Q_INVOKABLE bool isFakeTlsDomainInputAllowed(const QString &text) const;
Q_INVOKABLE QString clipboardText() const;
Q_INVOKABLE QString sanitizePublicHostFieldText(const QString &input) const;
Q_INVOKABLE QString sanitizePortFieldText(const QString &input) const;
Q_INVOKABLE QString sanitizeMtProxyTagFieldText(const QString &input) const;
Q_INVOKABLE QString sanitizeWorkersFieldText(const QString &input) const;
Q_INVOKABLE QString sanitizeOptionalIpv4FieldText(const QString &input) const;
Q_INVOKABLE bool isFakeTlsDomainTypingIncomplete(const QString &text) const;
Q_INVOKABLE bool isValidOptionalIpv4(const QString &ip) const;
protected:
QHash<int, QByteArray> roleNames() const override;
private:
amnezia::DockerContainer m_container;
QJsonObject m_fullConfig;
amnezia::MtProxyProtocolConfig m_protocolConfig;
};
#endif // MTPROXYCONFIGMODEL_H

View File

@@ -0,0 +1,406 @@
#include "telemtConfigModel.h"
#include <QRegularExpression>
#include "core/utils/qrCodeUtils.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/constants/protocolConstants.h"
#include "qrcodegen.hpp"
using namespace amnezia;
TelemtConfigModel::TelemtConfigModel(QObject *parent) : QAbstractListModel(parent) {}
void TelemtConfigModel::applyDefaults(TelemtProtocolConfig &c) {
if (c.port.isEmpty()) {
c.port = QString::fromUtf8(protocols::telemt::defaultPort);
}
if (c.transportMode.isEmpty()) {
c.transportMode = QString::fromUtf8(protocols::telemt::transportModeStandard);
}
if (c.workersMode.isEmpty()) {
c.workersMode = QString::fromUtf8(protocols::telemt::workersModeAuto);
}
if (c.workers.isEmpty()) {
c.workers = QString::fromUtf8(protocols::telemt::defaultWorkers);
}
if (c.userName.isEmpty()) {
c.userName = QString::fromUtf8(protocols::telemt::defaultUserName);
}
}
int TelemtConfigModel::rowCount(const QModelIndex &parent) const {
Q_UNUSED(parent);
return 1;
}
bool TelemtConfigModel::setData(const QModelIndex &index, const QVariant &value, int role) {
if (!index.isValid() || index.row() != 0) {
return false;
}
switch (role) {
case Roles::PortRole: {
m_protocolConfig.port = value.toString();
break;
}
case Roles::SecretRole: {
m_protocolConfig.secret = value.toString();
break;
}
case Roles::TagRole: {
m_protocolConfig.tag = value.toString();
break;
}
case Roles::IsEnabledRole: {
m_protocolConfig.isEnabled = value.toBool();
break;
}
case Roles::PublicHostRole: {
m_protocolConfig.publicHost = value.toString();
break;
}
case Roles::TransportModeRole: {
m_protocolConfig.transportMode = value.toString();
break;
}
case Roles::TlsDomainRole: {
m_protocolConfig.tlsDomain = value.toString();
break;
}
case Roles::AdditionalSecretsRole: {
m_protocolConfig.additionalSecrets = value.toStringList();
break;
}
case Roles::WorkersModeRole: {
m_protocolConfig.workersMode = value.toString();
break;
}
case Roles::WorkersRole: {
m_protocolConfig.workers = value.toString();
break;
}
case Roles::NatEnabledRole: {
m_protocolConfig.natEnabled = value.toBool();
break;
}
case Roles::NatInternalIpRole: {
m_protocolConfig.natInternalIp = value.toString();
break;
}
case Roles::NatExternalIpRole: {
m_protocolConfig.natExternalIp = value.toString();
break;
}
case Roles::MaskEnabledRole: {
m_protocolConfig.maskEnabled = value.toBool();
break;
}
case Roles::UseMiddleProxyRole: {
m_protocolConfig.useMiddleProxy = value.toBool();
break;
}
case Roles::TlsEmulationRole: {
m_protocolConfig.tlsEmulation = value.toBool();
break;
}
case Roles::UserNameRole: {
m_protocolConfig.userName = value.toString();
break;
}
default: {
return false;
}
}
emit dataChanged(index, index, QList{role});
return true;
}
QVariant TelemtConfigModel::data(const QModelIndex &index, int role) const {
if (!index.isValid() || index.row() != 0) {
return QVariant();
}
switch (role) {
case Roles::PortRole: {
return m_protocolConfig.port.isEmpty() ? QString::fromUtf8(protocols::telemt::defaultPort)
: m_protocolConfig.port;
}
case Roles::SecretRole: {
return m_protocolConfig.secret;
}
case Roles::TagRole: {
return m_protocolConfig.tag;
}
case Roles::TgLinkRole: {
return m_protocolConfig.tgLink;
}
case Roles::TmeLinkRole: {
return m_protocolConfig.tmeLink;
}
case Roles::IsEnabledRole: {
return m_protocolConfig.isEnabled;
}
case Roles::PublicHostRole: {
return m_protocolConfig.publicHost.isEmpty() ? m_fullConfig.value(QString(configKey::hostName)).toString()
: m_protocolConfig.publicHost;
}
case Roles::TransportModeRole: {
return m_protocolConfig.transportMode.isEmpty() ? QString::fromUtf8(
protocols::telemt::transportModeStandard)
: m_protocolConfig.transportMode;
}
case Roles::TlsDomainRole: {
return m_protocolConfig.tlsDomain;
}
case Roles::AdditionalSecretsRole: {
return m_protocolConfig.additionalSecrets;
}
case Roles::WorkersModeRole: {
return m_protocolConfig.workersMode.isEmpty() ? QString::fromUtf8(protocols::telemt::workersModeAuto)
: m_protocolConfig.workersMode;
}
case Roles::WorkersRole: {
return m_protocolConfig.workers.isEmpty() ? QString::fromUtf8(protocols::telemt::defaultWorkers)
: m_protocolConfig.workers;
}
case Roles::NatEnabledRole: {
return m_protocolConfig.natEnabled;
}
case Roles::NatInternalIpRole: {
return m_protocolConfig.natInternalIp;
}
case Roles::NatExternalIpRole: {
return m_protocolConfig.natExternalIp;
}
case Roles::MaskEnabledRole: {
return m_protocolConfig.maskEnabled;
}
case Roles::UseMiddleProxyRole: {
return m_protocolConfig.useMiddleProxy;
}
case Roles::TlsEmulationRole: {
return m_protocolConfig.tlsEmulation;
}
case Roles::UserNameRole: {
return m_protocolConfig.userName.isEmpty() ? QString::fromUtf8(protocols::telemt::defaultUserName)
: m_protocolConfig.userName;
}
}
return QVariant();
}
void TelemtConfigModel::updateModel(DockerContainer container, const TelemtProtocolConfig &protocolConfig) {
beginResetModel();
m_container = container;
m_protocolConfig = protocolConfig;
applyDefaults(m_protocolConfig);
endResetModel();
}
void TelemtConfigModel::updateModel(const QJsonObject &config) {
beginResetModel();
m_fullConfig = config;
m_protocolConfig = TelemtProtocolConfig::fromJson(config.value(QString(configKey::telemt)).toObject());
applyDefaults(m_protocolConfig);
endResetModel();
}
QJsonObject TelemtConfigModel::getConfig() {
m_fullConfig.insert(QString(configKey::telemt), m_protocolConfig.toJson());
return m_fullConfig;
}
TelemtProtocolConfig TelemtConfigModel::getProtocolConfig() {
return m_protocolConfig;
}
void TelemtConfigModel::generateSecret() {
QString secret;
for (int i = 0; i < 16; ++i) {
quint32 byte = QRandomGenerator::global()->bounded(256);
secret += QString("%1").arg(byte, 2, 16, QChar('0'));
}
m_protocolConfig.secret = secret;
emit dataChanged(index(0), index(0), QList<int>{SecretRole});
}
void TelemtConfigModel::setSecret(const QString &secret) {
if (secret.isEmpty()) {
return;
}
setData(index(0), secret, SecretRole);
}
bool TelemtConfigModel::validateAndSetSecret(const QString &rawSecret) {
if (!QRegularExpression(QStringLiteral("^[0-9a-fA-F]{32}$")).match(rawSecret).hasMatch()) {
return false;
}
setData(index(0), rawSecret, SecretRole);
return true;
}
void TelemtConfigModel::setPort(const QString &port) {
setData(index(0), port, PortRole);
}
void TelemtConfigModel::setTag(const QString &tag) {
setData(index(0), tag, TagRole);
}
void TelemtConfigModel::setPublicHost(const QString &host) {
setData(index(0), host, PublicHostRole);
}
void TelemtConfigModel::setTransportMode(const QString &mode) {
setData(index(0), mode, TransportModeRole);
}
QString TelemtConfigModel::getTransportMode() const {
return m_protocolConfig.transportMode.isEmpty() ? QString::fromUtf8(protocols::telemt::transportModeStandard)
: m_protocolConfig.transportMode;
}
QString TelemtConfigModel::getTlsDomain() const {
return m_protocolConfig.tlsDomain.isEmpty() ? QString::fromUtf8(protocols::telemt::defaultTlsDomain)
: m_protocolConfig.tlsDomain;
}
QString TelemtConfigModel::getPublicHost() const {
return m_protocolConfig.publicHost;
}
void TelemtConfigModel::setTlsDomain(const QString &domain) {
setData(index(0), domain, TlsDomainRole);
}
void TelemtConfigModel::setWorkersMode(const QString &mode) {
setData(index(0), mode, WorkersModeRole);
}
void TelemtConfigModel::setWorkers(const QString &workers) {
setData(index(0), workers, WorkersRole);
}
void TelemtConfigModel::setNatEnabled(bool enabled) {
setData(index(0), enabled, NatEnabledRole);
}
void TelemtConfigModel::setNatInternalIp(const QString &ip) {
setData(index(0), ip, NatInternalIpRole);
}
void TelemtConfigModel::setNatExternalIp(const QString &ip) {
setData(index(0), ip, NatExternalIpRole);
}
void TelemtConfigModel::setMaskEnabled(bool enabled) {
setData(index(0), enabled, MaskEnabledRole);
}
void TelemtConfigModel::setUseMiddleProxy(bool enabled) {
setData(index(0), enabled, UseMiddleProxyRole);
}
void TelemtConfigModel::setTlsEmulation(bool enabled) {
setData(index(0), enabled, TlsEmulationRole);
}
void TelemtConfigModel::setUserName(const QString &name) {
setData(index(0), name, UserNameRole);
}
void TelemtConfigModel::addAdditionalSecret() {
QString newSecret;
for (int i = 0; i < 16; ++i) {
quint32 byte = QRandomGenerator::global()->bounded(256);
newSecret += QString("%1").arg(byte, 2, 16, QChar('0'));
}
m_protocolConfig.additionalSecrets.append(newSecret);
emit dataChanged(index(0), index(0), QList<int>{AdditionalSecretsRole});
}
void TelemtConfigModel::removeAdditionalSecret(int idx) {
if (idx < 0 || idx >= m_protocolConfig.additionalSecrets.size()) {
return;
}
m_protocolConfig.additionalSecrets.removeAt(idx);
emit dataChanged(index(0), index(0), QList<int>{AdditionalSecretsRole});
}
void TelemtConfigModel::setEnabled(bool enabled) {
m_protocolConfig.isEnabled = enabled;
emit dataChanged(index(0), index(0), QList<int>{IsEnabledRole});
}
QString TelemtConfigModel::generateQrCode(const QString &text) {
if (text.isEmpty()) {
return "";
}
auto qr = qrCodeUtils::generateQrCode(text.toUtf8());
return qrCodeUtils::svgToBase64(QString::fromStdString(toSvgString(qr, 1)));
}
QString TelemtConfigModel::defaultTlsDomain() const {
return QString::fromUtf8(protocols::telemt::defaultTlsDomain);
}
QString TelemtConfigModel::defaultPort() const {
return QString::fromUtf8(protocols::telemt::defaultPort);
}
QString TelemtConfigModel::defaultWorkers() const {
return QString::fromUtf8(protocols::telemt::defaultWorkers);
}
int TelemtConfigModel::maxWorkers() const {
return protocols::telemt::maxWorkers;
}
QString TelemtConfigModel::transportModeStandard() const {
return QString::fromUtf8(protocols::telemt::transportModeStandard);
}
QString TelemtConfigModel::transportModeFakeTLS() const {
return QString::fromUtf8(protocols::telemt::transportModeFakeTLS);
}
QString TelemtConfigModel::workersModeAuto() const {
return QString::fromUtf8(protocols::telemt::workersModeAuto);
}
QString TelemtConfigModel::workersModeManual() const {
return QString::fromUtf8(protocols::telemt::workersModeManual);
}
QHash<int, QByteArray> TelemtConfigModel::roleNames() const {
QHash<int, QByteArray> roles;
roles[PortRole] = "port";
roles[SecretRole] = "secret";
roles[TagRole] = "tag";
roles[TgLinkRole] = "tgLink";
roles[TmeLinkRole] = "tmeLink";
roles[IsEnabledRole] = "isEnabled";
roles[PublicHostRole] = "publicHost";
roles[TransportModeRole] = "transportMode";
roles[TlsDomainRole] = "tlsDomain";
roles[AdditionalSecretsRole] = "additionalSecrets";
roles[WorkersModeRole] = "workersMode";
roles[WorkersRole] = "workers";
roles[NatEnabledRole] = "natEnabled";
roles[NatInternalIpRole] = "natInternalIp";
roles[NatExternalIpRole] = "natExternalIp";
roles[MaskEnabledRole] = "maskEnabled";
roles[UseMiddleProxyRole] = "useMiddleProxy";
roles[TlsEmulationRole] = "tlsEmulation";
roles[UserNameRole] = "userName";
return roles;
}

View File

@@ -0,0 +1,130 @@
#ifndef TELEMTCONFIGMODEL_H
#define TELEMTCONFIGMODEL_H
#include <QAbstractListModel>
#include <QJsonObject>
#include <QRandomGenerator>
#include "core/models/protocols/telemtProtocolConfig.h"
#include "core/utils/containerEnum.h"
class TelemtConfigModel : public QAbstractListModel {
Q_OBJECT
public:
enum Roles {
PortRole = Qt::UserRole + 1,
SecretRole,
TagRole,
TgLinkRole,
TmeLinkRole,
IsEnabledRole,
PublicHostRole,
TransportModeRole,
TlsDomainRole,
AdditionalSecretsRole,
WorkersModeRole,
WorkersRole,
NatEnabledRole,
NatInternalIpRole,
NatExternalIpRole,
MaskEnabledRole,
UseMiddleProxyRole,
TlsEmulationRole,
UserNameRole
};
explicit TelemtConfigModel(QObject *parent = nullptr);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
bool setData(const QModelIndex &index, const QVariant &value, int role) override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
public slots:
void updateModel(amnezia::DockerContainer container, const amnezia::TelemtProtocolConfig &protocolConfig);
void updateModel(const QJsonObject &config);
QJsonObject getConfig();
amnezia::TelemtProtocolConfig getProtocolConfig();
Q_INVOKABLE void generateSecret();
Q_INVOKABLE void setSecret(const QString &secret);
Q_INVOKABLE bool validateAndSetSecret(const QString &rawSecret);
Q_INVOKABLE void setPort(const QString &port);
Q_INVOKABLE void setTag(const QString &tag);
Q_INVOKABLE void setPublicHost(const QString &host);
Q_INVOKABLE void setTransportMode(const QString &mode);
Q_INVOKABLE QString getTransportMode() const;
Q_INVOKABLE QString getTlsDomain() const;
Q_INVOKABLE QString getPublicHost() const;
Q_INVOKABLE void setTlsDomain(const QString &domain);
Q_INVOKABLE void setWorkersMode(const QString &mode);
Q_INVOKABLE void setWorkers(const QString &workers);
Q_INVOKABLE void setNatEnabled(bool enabled);
Q_INVOKABLE void setNatInternalIp(const QString &ip);
Q_INVOKABLE void setNatExternalIp(const QString &ip);
Q_INVOKABLE void addAdditionalSecret();
Q_INVOKABLE void removeAdditionalSecret(int idx);
Q_INVOKABLE QString generateQrCode(const QString &text);
Q_INVOKABLE void setEnabled(bool enabled);
Q_INVOKABLE void setMaskEnabled(bool enabled);
Q_INVOKABLE void setUseMiddleProxy(bool enabled);
Q_INVOKABLE void setTlsEmulation(bool enabled);
Q_INVOKABLE void setUserName(const QString &name);
Q_INVOKABLE QString defaultTlsDomain() const;
Q_INVOKABLE QString defaultPort() const;
Q_INVOKABLE QString defaultWorkers() const;
Q_INVOKABLE int maxWorkers() const;
Q_INVOKABLE QString transportModeStandard() const;
Q_INVOKABLE QString transportModeFakeTLS() const;
Q_INVOKABLE QString workersModeAuto() const;
Q_INVOKABLE QString workersModeManual() const;
protected:
QHash<int, QByteArray> roleNames() const override;
private:
static void applyDefaults(amnezia::TelemtProtocolConfig &c);
amnezia::DockerContainer m_container = amnezia::DockerContainer::None;
QJsonObject m_fullConfig;
amnezia::TelemtProtocolConfig m_protocolConfig;
};
#endif // TELEMTCONFIGMODEL_H

View File

@@ -0,0 +1,127 @@
#include "mtproxy_public_host_input.h"
#include <QRegularExpression>
namespace {
bool ipv4OctetTokenOk(const QString &s) {
static const QRegularExpression re(QStringLiteral(R"(^\d{1,3}$)"));
if (!re.match(s).hasMatch()) {
return false;
}
bool ok = false;
const int n = s.toInt(&ok);
return ok && n >= 0 && n <= 255;
}
// Reject labels like "312edweqwe" (digits >255 then letters).
bool labelHasInvalidOctetLikePrefixBeforeLetters(const QString &label) {
static const QRegularExpression re(QStringLiteral(R"(^(\d+)([a-zA-Z].*)$)"));
const QRegularExpressionMatch m = re.match(label);
if (!m.hasMatch()) {
return false;
}
const QString digits = m.captured(1);
if (digits.length() > 3) {
return true;
}
bool ok = false;
const int n = digits.toInt(&ok);
if (!ok) {
return true;
}
if (n > 255) {
return true;
}
// Do not restrict n≤255 + letters here (e.g. "123mlkjh.example.com"); four-segment IPv4+junk is handled below.
return false;
}
// "123.123wqqweqweqweqwe" — first label is a real octet, second looks like an octet glued to letters (not "123.45").
bool looksLikeTwoSegmentOctetThenDigitLetterGlue(const QString &text) {
const QStringList parts = text.split(QLatin1Char('.'), Qt::KeepEmptyParts);
if (parts.size() != 2) {
return false;
}
if (!ipv4OctetTokenOk(parts.at(0))) {
return false;
}
const QString &p1 = parts.at(1);
static const QRegularExpression digitThenLetter(QStringLiteral(R"(^\d+[a-zA-Z])"));
if (!digitThenLetter.match(p1).hasMatch()) {
return false;
}
return !ipv4OctetTokenOk(p1);
}
// "a.b.c.djunk" where first three parts are pure octets and last part has digits then letters (e.g. "123wdqweqweqwe").
bool looksLikeFourOctetIpv4WithGarbageInLastSegment(const QString &text) {
const QStringList parts = text.split(QLatin1Char('.'), Qt::KeepEmptyParts);
if (parts.size() != 4) {
return false;
}
for (int i = 0; i < 3; ++i) {
if (!ipv4OctetTokenOk(parts.at(i))) {
return false;
}
}
static const QRegularExpression digitThenLetter(QStringLiteral(R"(^\d+[a-zA-Z])"));
return digitThenLetter.match(parts.at(3)).hasMatch();
}
bool hostLabelsRejectBrokenDigitLetterMix(const QString &text) {
if (looksLikeTwoSegmentOctetThenDigitLetterGlue(text)) {
return false;
}
if (looksLikeFourOctetIpv4WithGarbageInLastSegment(text)) {
return false;
}
const QStringList parts = text.split(QLatin1Char('.'), Qt::KeepEmptyParts);
for (const QString &part: parts) {
if (labelHasInvalidOctetLikePrefixBeforeLetters(part)) {
return false;
}
}
return true;
}
} // namespace
bool mtproxyPublicHostInputAllowed(const QString &text) {
if (text.length() > 253) {
return false;
}
static const QRegularExpression allowed(QStringLiteral(R"(^[a-zA-Z0-9.:\-]*$)"));
if (!allowed.match(text).hasMatch()) {
return false;
}
static const QRegularExpression onlyDigits(QStringLiteral(R"(^\d+$)"));
if (onlyDigits.match(text).hasMatch() && text.length() > 3) {
return false;
}
static const QRegularExpression onlyDigitDot(QStringLiteral(R"(^[0-9.]+$)"));
if (!text.isEmpty() && onlyDigitDot.match(text).hasMatch()) {
static const QRegularExpression ipv4Partial(QStringLiteral(R"(^(\d{1,3}\.){0,3}\d{0,3}$)"));
return ipv4Partial.match(text).hasMatch();
}
if (text.contains(QLatin1Char(':'))) {
static const QRegularExpression ipv6Chars(QStringLiteral(R"(^[0-9a-fA-F:.]*$)"));
if (!ipv6Chars.match(text).hasMatch()) {
return false;
}
if (text.size() > 45) {
return false;
}
}
if (!hostLabelsRejectBrokenDigitLetterMix(text)) {
return false;
}
return true;
}
PublicHostInputValidator::PublicHostInputValidator(QObject *parent) : QValidator(parent) {}
QValidator::State PublicHostInputValidator::validate(QString &input, int &pos) const {
Q_UNUSED(pos)
return mtproxyPublicHostInputAllowed(input) ? Acceptable : Invalid;
}

View File

@@ -0,0 +1,20 @@
#ifndef MTPROXY_PUBLIC_HOST_INPUT_H
#define MTPROXY_PUBLIC_HOST_INPUT_H
#include <QString>
#include <QValidator>
/// Shared rules for public host field (IPv4 dotted partial, IPv6 hex, FQDN ASCII).
bool mtproxyPublicHostInputAllowed(const QString &text);
class PublicHostInputValidator : public QValidator {
Q_OBJECT
public:
explicit PublicHostInputValidator(QObject *parent = nullptr);
QValidator::State validate(QString &input, int &pos) const override;
};
#endif

View File

@@ -45,6 +45,12 @@ ListViewType {
PageController.goToPage(PageEnum.PageProtocolRaw)
} else if (isDns) {
PageController.goToPage(PageEnum.PageServiceDnsSettings)
} else if (isMtProxy) {
MtProxyConfigModel.updateModel(config)
PageController.goToPage(PageEnum.PageServiceMtProxySettings)
} else if (isTelemt) {
TelemtConfigModel.updateModel(config)
PageController.goToPage(PageEnum.PageServiceTelemtSettings)
} else {
InstallController.updateProtocols(ServersUiController.getServerId(ServersUiController.processedServerIndex), containerIndex)
PageController.goToPage(PageEnum.PageSettingsServerProtocol)

View File

@@ -31,6 +31,9 @@ ListViewType {
function triggerCurrentItem() {
var item = root.itemAtIndex(selectedIndex)
if (!item) {
return
}
item.selectable.clicked()
}

View File

@@ -0,0 +1,61 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Style 1.0
import "../Controls2"
import "../Controls2/TextTypes"
// MinMaxRowType — two side-by-side labeled text fields: Min / Max
// Usage:
// MinMaxRowType {
// minValue: "0"
// maxValue: "0"
// onMinChanged: someProperty = val
// onMaxChanged: someProperty = val
// }
Item {
id: root
property string minValue: "0"
property string maxValue: "0"
signal minChanged(string val)
signal maxChanged(string val)
implicitHeight: row.implicitHeight
implicitWidth: row.implicitWidth
RowLayout {
id: row
anchors.fill: parent
spacing: 10
// Min field
TextFieldWithHeaderType {
Layout.fillWidth: true
headerText: qsTr("Min")
textField.text: root.minValue
textField.validator: IntValidator { bottom: 0 }
textField.onEditingFinished: {
if (textField.text !== root.minValue) {
root.minChanged(textField.text)
}
}
}
// Max field
TextFieldWithHeaderType {
Layout.fillWidth: true
headerText: qsTr("Max")
textField.text: root.maxValue
textField.validator: IntValidator { bottom: 0 }
textField.onEditingFinished: {
if (textField.text !== root.maxValue) {
root.maxChanged(textField.text)
}
}
}
}
}

View File

@@ -10,6 +10,7 @@ Item {
id: root
property string headerText
property string subtitleText // optional line under header (e.g. default value hint)
property string headerTextDisabledColor: AmneziaStyle.color.charcoalGray
property string headerTextColor: AmneziaStyle.color.mutedGray
@@ -84,6 +85,15 @@ Item {
Layout.fillWidth: true
}
SmallTextType {
text: root.subtitleText
visible: root.subtitleText !== ""
color: AmneziaStyle.color.charcoalGray
font.pixelSize: 13
Layout.fillWidth: true
Layout.topMargin: visible ? 2 : 0
}
TextField {
id: textField

View File

@@ -0,0 +1,125 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import PageEnum 1.0
import ProtocolEnum 1.0
import Style 1.0
import "./"
import "../Controls2"
import "../Controls2/TextTypes"
import "../Config"
import "../Components"
PageType {
id: root
BackButtonType {
id: backButton
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 20 + PageController.safeAreaTopMargin
}
ListViewType {
id: listView
anchors.top: backButton.bottom
anchors.bottom: saveButton.visible ? saveButton.top : parent.bottom
anchors.left: parent.left
anchors.right: parent.right
enabled: ServersUiController.isProcessedServerHasWriteAccess()
model: XrayConfigModel
delegate: ColumnLayout {
width: listView.width
spacing: 0
BaseHeaderType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 0
Layout.bottomMargin: 24
headerText: qsTr("Flow")
}
VerticalRadioButton {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
text: qsTr("Empty")
checked: flow === ""
onClicked: flow = ""
}
DividerType {
}
VerticalRadioButton {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
text: "xtls-rprx-vision"
checked: flow === "xtls-rprx-vision"
onClicked: flow = "xtls-rprx-vision"
}
DividerType {
}
VerticalRadioButton {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
text: "xtls-rprx-vision-udp443"
checked: flow === "xtls-rprx-vision-udp443"
onClicked: flow = "xtls-rprx-vision-udp443"
}
DividerType {
}
Item {
Layout.preferredHeight: 16
}
}
}
BasicButtonType {
id: saveButton
anchors.left: root.left
anchors.right: root.right
anchors.bottom: root.bottom
anchors.leftMargin: 16
anchors.rightMargin: 16
anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin
visible: listView.enabled && XrayConfigModel.hasUnsavedChanges
enabled: visible
text: qsTr("Save")
clickedFunc: function () {
var headerText = qsTr("Save settings?")
var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.")
var yesButtonText = qsTr("Continue")
var noButtonText = qsTr("Cancel")
var yesButtonFunction = function () {
if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ServersUiController.processedContainerIndex) {
PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection"))
return
}
PageController.goToPage(PageEnum.PageSetupWizardInstalling)
InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.Xray)
}
var noButtonFunction = function () {
if (typeof GC !== "undefined" && !GC.isMobile()) {
saveButton.forceActiveFocus()
}
}
showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
}
}
}

View File

@@ -0,0 +1,292 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import PageEnum 1.0
import ProtocolEnum 1.0
import Style 1.0
import "./"
import "../Controls2"
import "../Controls2/TextTypes"
import "../Config"
import "../Components"
PageType {
id: root
BackButtonType {
id: backButton
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 20 + PageController.safeAreaTopMargin
}
ListViewType {
id: listView
anchors.top: backButton.bottom
anchors.bottom: saveButton.visible ? saveButton.top : parent.bottom
anchors.left: parent.left
anchors.right: parent.right
enabled: ServersUiController.isProcessedServerHasWriteAccess()
model: XrayConfigModel
delegate: ColumnLayout {
width: listView.width
spacing: 0
BaseHeaderType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 0
Layout.bottomMargin: 24
headerText: qsTr("Security")
}
VerticalRadioButton {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
text: qsTr("None")
checked: security === "none"
onClicked: security = "none"
}
DividerType {
}
VerticalRadioButton {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
text: qsTr("TLS")
checked: security === "tls"
onClicked: security = "tls"
}
DividerType {
}
VerticalRadioButton {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
text: qsTr("Reality")
checked: security === "reality"
onClicked: security = "reality"
}
DividerType {
}
// ── TLS fields ────────────────────────────────────────────
ColumnLayout {
visible: security === "tls"
Layout.fillWidth: true
spacing: 0
DropDownType {
id: tlsAlpnDropDown
Layout.fillWidth: true
Layout.topMargin: 16
Layout.leftMargin: 16
Layout.rightMargin: 16
text: alpn
descriptionText: qsTr("ALPN")
headerText: qsTr("ALPN")
drawerParent: root
listView: ListViewWithRadioButtonType {
rootWidth: root.width
model: ListModel {
Component.onCompleted: {
var opts = XrayConfigModel.alpnOptions()
for (var i = 0; i < opts.length; i++) {
append({name: opts[i]})
}
}
}
clickedFunction: function () {
alpn = selectedText
tlsAlpnDropDown.text = selectedText
tlsAlpnDropDown.closeTriggered()
}
Component.onCompleted: {
for (var i = 0; i < model.count; i++) {
if (model.get(i).name === alpn) {
selectedIndex = i;
break
}
}
}
}
Connections {
target: XrayConfigModel
function onDataChanged() {
tlsAlpnDropDown.text = alpn
}
}
}
DropDownType {
id: tlsFingerprintDropDown
Layout.fillWidth: true
Layout.topMargin: 8
Layout.leftMargin: 16
Layout.rightMargin: 16
text: fingerprint
descriptionText: qsTr("Fingerprint")
headerText: qsTr("Fingerprint")
drawerParent: root
listView: ListViewWithRadioButtonType {
rootWidth: root.width
model: ListModel {
Component.onCompleted: {
var opts = XrayConfigModel.fingerprintOptions()
for (var i = 0; i < opts.length; i++) {
append({name: opts[i]})
}
}
}
clickedFunction: function () {
fingerprint = selectedText
tlsFingerprintDropDown.text = selectedText
tlsFingerprintDropDown.closeTriggered()
}
Component.onCompleted: {
for (var i = 0; i < model.count; i++) {
if (model.get(i).name === fingerprint) {
selectedIndex = i;
break
}
}
}
}
Connections {
target: XrayConfigModel
function onDataChanged() {
tlsFingerprintDropDown.text = fingerprint
}
}
}
TextFieldWithHeaderType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 8
headerText: qsTr("Server Name (SNI)")
textField.text: sni
textField.onEditingFinished: {
if (textField.text !== sni) sni = textField.text
}
}
}
// ── Reality fields ────────────────────────────────────────
ColumnLayout {
visible: security === "reality"
Layout.fillWidth: true
spacing: 0
DropDownType {
id: realityFingerprintDropDown
Layout.fillWidth: true
Layout.topMargin: 16
Layout.leftMargin: 16
Layout.rightMargin: 16
text: fingerprint
descriptionText: qsTr("Fingerprint")
headerText: qsTr("Fingerprint")
drawerParent: root
listView: ListViewWithRadioButtonType {
rootWidth: root.width
model: ListModel {
Component.onCompleted: {
var opts = XrayConfigModel.fingerprintOptions()
for (var i = 0; i < opts.length; i++) {
append({name: opts[i]})
}
}
}
clickedFunction: function () {
fingerprint = selectedText
realityFingerprintDropDown.text = selectedText
realityFingerprintDropDown.closeTriggered()
}
Component.onCompleted: {
for (var i = 0; i < model.count; i++) {
if (model.get(i).name === fingerprint) {
selectedIndex = i;
break
}
}
}
}
Connections {
target: XrayConfigModel
function onDataChanged() {
realityFingerprintDropDown.text = fingerprint
}
}
}
TextFieldWithHeaderType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 8
headerText: qsTr("Server Name (SNI)")
textField.text: sni
textField.onEditingFinished: {
if (textField.text !== sni) sni = textField.text
}
}
}
Item {
Layout.preferredHeight: 16
}
}
}
BasicButtonType {
id: saveButton
anchors.left: root.left
anchors.right: root.right
anchors.bottom: root.bottom
anchors.leftMargin: 16
anchors.rightMargin: 16
anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin
visible: listView.enabled && XrayConfigModel.hasUnsavedChanges
enabled: visible
text: qsTr("Save")
clickedFunc: function () {
var headerText = qsTr("Save settings?")
var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.")
var yesButtonText = qsTr("Continue")
var noButtonText = qsTr("Cancel")
var yesButtonFunction = function () {
if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ServersUiController.processedContainerIndex) {
PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection"))
return
}
PageController.goToPage(PageEnum.PageSetupWizardInstalling)
InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.Xray)
}
var noButtonFunction = function () {
if (typeof GC !== "undefined" && !GC.isMobile()) {
saveButton.forceActiveFocus()
}
}
showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
}
}
}

View File

@@ -17,6 +17,20 @@ import "../Components"
PageType {
id: root
function formatTransport(value) {
if (value === "raw") return "RAW (TCP)"
if (value === "xhttp") return "XHTTP"
if (value === "mkcp") return "mKCP"
return value
}
function formatSecurity(value) {
if (value === "none") return "None"
if (value === "tls") return "TLS"
if (value === "reality") return "Reality"
return value
}
BackButtonType {
id: backButton
@@ -50,88 +64,125 @@ PageType {
spacing: 0
BaseHeaderType {
RowLayout {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
headerText: qsTr("XRay settings")
Layout.topMargin: 0
BaseHeaderType {
Layout.fillWidth: true
headerText: qsTr("XRay VLESS settings")
}
ImageButtonType {
Layout.alignment: Qt.AlignTop | Qt.AlignRight
implicitWidth: 40
implicitHeight: 40
image: "qrc:/images/controls/more-vertical.svg"
imageColor: AmneziaStyle.color.paleGray
onClicked: PageController.goToPage(PageEnum.PageProtocolXraySnapshots)
}
}
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
Layout.topMargin: 32
Layout.leftMargin: 16
Layout.rightMargin: 16
enabled: listView.enabled
headerText: qsTr("Disguised as traffic from")
textField.text: site
textField.onEditingFinished: {
if (textField.text !== site) {
var tmpText = textField.text
tmpText = tmpText.toLocaleLowerCase()
if (tmpText.startsWith("https://")) {
tmpText = textField.text.substring(8)
site = tmpText
} else {
site = textField.text
}
}
}
checkEmptyText: true
}
TextFieldWithHeaderType {
id: portTextField
Layout.fillWidth: true
Layout.topMargin: 16
Layout.leftMargin: 16
Layout.rightMargin: 16
enabled: listView.enabled
headerText: qsTr("Port")
textField.text: port
textField.maximumLength: 5
textField.validator: IntValidator { bottom: 1; top: 65535 }
textField.onEditingFinished: {
if (textField.text !== port) {
port = textField.text
}
textField.validator: IntValidator {
bottom: 1; top: 65535
}
textField.onEditingFinished: {
if (textField.text !== port) port = textField.text
}
checkEmptyText: true
}
LabelWithButtonType {
Layout.fillWidth: true
Layout.topMargin: 16
text: qsTr("Transport")
descriptionText: root.formatTransport(transport)
rightImageSource: "qrc:/images/controls/chevron-right.svg"
enabled: listView.enabled
clickedFunction: function() {
PageController.goToPage(PageEnum.PageProtocolXrayTransportSettings)
}
}
DividerType {
}
LabelWithButtonType {
Layout.fillWidth: true
text: qsTr("Security")
descriptionText: root.formatSecurity(security)
rightImageSource: "qrc:/images/controls/chevron-right.svg"
enabled: listView.enabled
clickedFunction: function() {
PageController.goToPage(PageEnum.PageProtocolXraySecuritySettings)
}
}
DividerType {
}
LabelWithButtonType {
Layout.fillWidth: true
text: qsTr("Flow")
descriptionText: flow
rightImageSource: "qrc:/images/controls/chevron-right.svg"
enabled: listView.enabled
clickedFunction: function() {
PageController.goToPage(PageEnum.PageProtocolXrayFlowSettings)
}
}
DividerType {
}
Item {
Layout.fillWidth: true; Layout.preferredHeight: 24
}
BasicButtonType {
id: saveButton
Layout.fillWidth: true
Layout.topMargin: 24
Layout.bottomMargin: 24
Layout.bottomMargin: 8
Layout.leftMargin: 16
Layout.rightMargin: 16
enabled: portTextField.errorText === ""
// Show Save immediately while user edits port, even before focus loss.
visible: listView.enabled && (XrayConfigModel.hasUnsavedChanges || textFieldWithHeaderType.textField.text !== port)
enabled: visible && textFieldWithHeaderType.errorText === ""
text: qsTr("Save")
onClicked: function() {
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 yesButtonText = qsTr("Continue")
var noButtonText = qsTr("Cancel")
var yesButtonFunction = function() {
if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ServersUiController.processedContainerIndex) {
PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection"))
@@ -142,16 +193,32 @@ PageType {
InstallController.updateContainer(ServersUiController.getServerId(ServersUiController.processedServerIndex), ServersUiController.processedContainerIndex, ProtocolEnum.Xray)
}
var noButtonFunction = function() {
if (!GC.isMobile()) {
saveButton.forceActiveFocus()
}
if (!GC.isMobile()) saveButton.forceActiveFocus()
}
showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
}
Keys.onEnterPressed: saveButton.clicked()
Keys.onReturnPressed: saveButton.clicked()
}
LabelWithButtonType {
Layout.fillWidth: true
text: qsTr("Reset settings")
textColor: AmneziaStyle.color.vibrantRed
visible: listView.enabled
clickedFunction: function() {
var yesButtonFunction = function() {
XrayConfigModel.resetToDefaults()
}
showQuestionDrawer(qsTr("Reset settings?"), qsTr("All XRay settings will be restored to defaults."),
qsTr("Reset"), qsTr("Cancel"), yesButtonFunction, function() {
})
}
}
Item {
Layout.fillWidth: true; Layout.preferredHeight: 32
}
}
}
}

View File

@@ -0,0 +1,291 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import PageEnum 1.0
import Style 1.0
import "./"
import "../Controls2"
import "../Controls2/TextTypes"
import "../Config"
import "../Components"
import Qt.labs.platform 1.1
PageType {
id: root
property string selectedConfigName: ""
property int selectedConfigIndex: -1
// Reload the list every time we open this page
Component.onCompleted: XrayConfigSnapshotsModel.reload()
// ── Save xray config snapshot to file ────────────────────────────
function saveConfigToFile(json) {
var fileName = ""
if (GC.isMobile()) {
fileName = "amnezia_xray_config.json"
} else {
fileName = SystemController.getFileName(
qsTr("Save XRay configuration"),
qsTr("JSON files (*.json)"),
StandardPaths.standardLocations(StandardPaths.DocumentsLocation) + "/amnezia_xray_config",
true,
".json")
}
if (fileName !== "") {
PageController.showBusyIndicator(true)
ExportController.setConfigFromString(json, fileName)
PageController.showBusyIndicator(false)
PageController.showNotificationMessage(qsTr("Configuration saved"))
}
}
BackButtonType {
id: backButton
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 20 + PageController.safeAreaTopMargin
}
ListViewType {
id: listView
anchors.top: backButton.bottom
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
model: XrayConfigSnapshotsModel
header: ColumnLayout {
width: listView.width
spacing: 0
BaseHeaderType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 0
Layout.bottomMargin: 24
headerText: qsTr("XRay Configurations")
}
// ── Create from current settings ──────────────────────────
LabelWithButtonType {
Layout.fillWidth: true
text: qsTr("Create configuration based on current settings")
textMaximumLineCount: 2
rightImageSource: "qrc:/images/controls/chevron-right.svg"
clickedFunction: function () {
XrayConfigSnapshotsModel.createFromCurrentModel()
}
}
DividerType {
}
// ── Export ────────────────────────────────────────────────
LabelWithButtonType {
Layout.fillWidth: true
text: qsTr("Export settings")
rightImageSource: "qrc:/images/controls/chevron-right.svg"
clickedFunction: function () {
var idx = root.selectedConfigIndex >= 0 ? root.selectedConfigIndex : 0
if (listView.count > 0) {
var json = XrayConfigSnapshotsModel.exportToJson(idx)
saveConfigToFile(json)
}
}
}
DividerType {
}
// ── Import ────────────────────────────────────────────────
LabelWithButtonType {
Layout.fillWidth: true
text: qsTr("Import settings")
descriptionText: qsTr("In JSON format")
rightImageSource: "qrc:/images/controls/chevron-right.svg"
clickedFunction: function () {
var filePath = SystemController.getFileName(
qsTr("Open XRay configuration"),
qsTr("JSON files (*.json)"))
if (filePath !== "") {
var jsonContent = ImportController.readTextFile(filePath)
if (jsonContent !== "") {
if (!XrayConfigSnapshotsModel.importFromJson(jsonContent)) {
PageController.showNotificationMessage(qsTr("Failed to import configuration"))
} else {
PageController.showNotificationMessage(qsTr("Configuration imported successfully"))
}
}
}
}
}
DividerType {
}
// ── Section label ─────────────────────────────────────────
CaptionTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 24
Layout.bottomMargin: 8
text: qsTr("Configurations")
color: AmneziaStyle.color.mutedGray
visible: listView.count > 0
}
}
// ── Empty state ───────────────────────────────────────────────
footer: ColumnLayout {
width: listView.width
visible: listView.count === 0
spacing: 0
Item {
Layout.preferredHeight: 32
}
ParagraphTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
text: qsTr("No saved configurations yet.\nCreate one from the current settings.")
color: AmneziaStyle.color.mutedGray
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
}
}
// ── Config list items ─────────────────────────────────────────
delegate: ColumnLayout {
width: listView.width
spacing: 0
LabelWithButtonType {
Layout.fillWidth: true
text: configName
descriptionText: configDate
rightImageSource: "qrc:/images/controls/more-vertical.svg"
clickedFunction: function () {
root.selectedConfigName = configName
root.selectedConfigIndex = index
configActionsDrawer.openTriggered()
}
}
DividerType {
}
}
}
// ── Import result handler ─────────────────────────────────────────
Connections {
target: XrayConfigSnapshotsModel
function onImportFailed(errorMessage) {
PageController.showNotificationMessage(errorMessage)
}
}
// ── Per-config actions drawer ─────────────────────────────────────
DrawerType2 {
id: configActionsDrawer
parent: root
anchors.fill: parent
expandedHeight: root.height * 0.35
expandedStateContent: ColumnLayout {
id: drawerContent
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
spacing: 0
onImplicitHeightChanged: {
configActionsDrawer.expandedHeight = drawerContent.implicitHeight + 32
}
BackButtonType {
Layout.fillWidth: true
Layout.topMargin: 16
backButtonFunction: function () {
configActionsDrawer.closeTriggered()
}
}
BaseHeaderType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 8
Layout.bottomMargin: 16
headerText: root.selectedConfigName
}
// Apply
LabelWithButtonType {
Layout.fillWidth: true
text: qsTr("Apply configuration")
rightImageSource: "qrc:/images/controls/chevron-right.svg"
clickedFunction: function () {
configActionsDrawer.closeTriggered()
XrayConfigSnapshotsModel.applyConfigToCurrentModel(root.selectedConfigIndex)
PageController.closePage()
}
}
DividerType {
}
// Export this config
LabelWithButtonType {
Layout.fillWidth: true
text: qsTr("Export configuration")
rightImageSource: "qrc:/images/controls/chevron-right.svg"
clickedFunction: function () {
configActionsDrawer.closeTriggered()
var json = XrayConfigSnapshotsModel.exportToJson(root.selectedConfigIndex)
saveConfigToFile(json)
}
}
DividerType {
}
// Delete
LabelWithButtonType {
Layout.fillWidth: true
text: qsTr("Delete configuration")
textColor: AmneziaStyle.color.vibrantRed
clickedFunction: function () {
configActionsDrawer.closeTriggered()
var yesButtonFunction = function () {
XrayConfigSnapshotsModel.removeConfig(root.selectedConfigIndex)
root.selectedConfigIndex = -1
root.selectedConfigName = ""
}
showQuestionDrawer(
qsTr("Delete configuration?"),
qsTr("This action cannot be undone."),
qsTr("Delete"), qsTr("Cancel"),
yesButtonFunction, function () {
})
}
}
DividerType {
}
Item {
Layout.preferredHeight: 16
}
}
}
}

View File

@@ -0,0 +1,755 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import PageEnum 1.0
import ProtocolEnum 1.0
import Style 1.0
import "./"
import "../Controls2"
import "../Controls2/TextTypes"
import "../Config"
import "../Components"
PageType {
id: root
BackButtonType {
id: backButton
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 20 + PageController.safeAreaTopMargin
}
ListViewType {
id: listView
anchors.top: backButton.bottom
anchors.bottom: saveButton.visible ? saveButton.top : parent.bottom
anchors.left: parent.left
anchors.right: parent.right
enabled: ServersUiController.isProcessedServerHasWriteAccess()
model: XrayConfigModel
delegate: ColumnLayout {
width: listView.width
spacing: 0
BaseHeaderType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 0
Layout.bottomMargin: 24
headerText: qsTr("Transport")
}
// ── Radio buttons ─────────────────────────────────────────
VerticalRadioButton {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
text: qsTr("RAW (TCP)")
checked: transport === "raw"
onToggled: if (checked && transport !== "raw") transport = "raw"
}
DividerType {
}
VerticalRadioButton {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
text: qsTr("XHTTP")
descriptionText: qsTr("Advanced users")
checked: transport === "xhttp"
onToggled: if (checked && transport !== "xhttp") transport = "xhttp"
}
DividerType {
}
VerticalRadioButton {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
text: qsTr("mKCP")
checked: transport === "mkcp"
onToggled: if (checked && transport !== "mkcp") transport = "mkcp"
}
DividerType {
}
// ══════════════════════════════════════════════════════════
// mKCP Settings
// ══════════════════════════════════════════════════════════
ColumnLayout {
visible: transport === "mkcp"
Layout.fillWidth: true
spacing: 0
CaptionTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 24
Layout.bottomMargin: 8
text: qsTr("mKCP Settings")
color: AmneziaStyle.color.mutedGray
}
TextFieldWithHeaderType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 8
headerText: qsTr("TTI")
subtitleText: qsTr("Default: %1 ms", "mKCP TTI").arg(XrayConfigModel.mkcpDefaultTti())
textField.text: mkcpTti
textField.onEditingFinished: {
if (textField.text !== mkcpTti) mkcpTti = textField.text
}
}
TextFieldWithHeaderType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 8
headerText: qsTr("uplinkCapacity")
subtitleText: qsTr("Default: %1 Mbit/s", "mKCP uplink").arg(XrayConfigModel.mkcpDefaultUplinkCapacity())
textField.text: mkcpUplinkCapacity
textField.onEditingFinished: {
if (textField.text !== mkcpUplinkCapacity) mkcpUplinkCapacity = textField.text
}
}
TextFieldWithHeaderType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 8
headerText: qsTr("downlinkCapacity")
subtitleText: qsTr("Default: %1 Mbit/s", "mKCP downlink").arg(XrayConfigModel.mkcpDefaultDownlinkCapacity())
textField.text: mkcpDownlinkCapacity
textField.onEditingFinished: {
if (textField.text !== mkcpDownlinkCapacity) mkcpDownlinkCapacity = textField.text
}
}
TextFieldWithHeaderType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 8
headerText: qsTr("readBufferSize")
subtitleText: qsTr("Default: %1 MiB").arg(XrayConfigModel.mkcpDefaultReadBufferSize())
textField.text: mkcpReadBufferSize
textField.onEditingFinished: {
if (textField.text !== mkcpReadBufferSize) mkcpReadBufferSize = textField.text
}
}
TextFieldWithHeaderType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 8
headerText: qsTr("writeBufferSize")
subtitleText: qsTr("Default: %1 MiB").arg(XrayConfigModel.mkcpDefaultWriteBufferSize())
textField.text: mkcpWriteBufferSize
textField.onEditingFinished: {
if (textField.text !== mkcpWriteBufferSize) mkcpWriteBufferSize = textField.text
}
}
SwitcherType {
Layout.fillWidth: true
Layout.margins: 16
Layout.topMargin: 8
text: qsTr("Congestion")
checked: mkcpCongestion
onToggled: mkcpCongestion = checked
}
}
// ══════════════════════════════════════════════════════════
// XHTTP Settings
// ══════════════════════════════════════════════════════════
ColumnLayout {
visible: transport === "xhttp"
Layout.fillWidth: true
spacing: 0
DropDownType {
id: modeDropDown
Layout.fillWidth: true
Layout.topMargin: 16
Layout.leftMargin: 16
Layout.rightMargin: 16
text: xhttpMode
descriptionText: qsTr("Mode")
headerText: qsTr("Mode")
drawerParent: root
listView: ListViewWithRadioButtonType {
rootWidth: root.width
model: ListModel {
Component.onCompleted: {
var opts = XrayConfigModel.xhttpModeOptions()
for (var i = 0; i < opts.length; i++) {
append({name: opts[i]})
}
}
}
clickedFunction: function () {
xhttpMode = selectedText
modeDropDown.text = selectedText
modeDropDown.closeTriggered()
}
Component.onCompleted: {
for (var i = 0; i < model.count; i++) {
if (model.get(i).name === xhttpMode) {
selectedIndex = i;
break
}
}
}
}
Connections {
target: XrayConfigModel
function onDataChanged() {
modeDropDown.text = xhttpMode
}
}
}
CaptionTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 24
Layout.bottomMargin: 8
text: qsTr("HTTP Profile")
color: AmneziaStyle.color.mutedGray
}
TextFieldWithHeaderType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 8
headerText: qsTr("Host")
textField.text: xhttpHost
textField.onEditingFinished: {
if (textField.text !== xhttpHost) xhttpHost = textField.text
}
}
TextFieldWithHeaderType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 8
headerText: qsTr("Path")
textField.text: xhttpPath
textField.onEditingFinished: {
if (textField.text !== xhttpPath) xhttpPath = textField.text
}
}
DropDownType {
id: headersDropDown
Layout.fillWidth: true
Layout.topMargin: 8
Layout.leftMargin: 16
Layout.rightMargin: 16
text: xhttpHeadersTemplate
descriptionText: qsTr("Headers template")
headerText: qsTr("Headers template")
drawerParent: root
listView: ListViewWithRadioButtonType {
rootWidth: root.width
model: ListModel {
Component.onCompleted: {
var opts = XrayConfigModel.xhttpHeadersTemplateOptions()
for (var i = 0; i < opts.length; i++) {
append({name: opts[i]})
}
}
}
clickedFunction: function () {
xhttpHeadersTemplate = selectedText
headersDropDown.text = selectedText
headersDropDown.closeTriggered()
}
Component.onCompleted: {
for (var i = 0; i < model.count; i++) {
if (model.get(i).name === xhttpHeadersTemplate) {
selectedIndex = i;
break
}
}
}
}
Connections {
target: XrayConfigModel
function onDataChanged() {
headersDropDown.text = xhttpHeadersTemplate
}
}
}
DropDownType {
id: uplinkMethodDropDown
Layout.fillWidth: true
Layout.topMargin: 8
Layout.leftMargin: 16
Layout.rightMargin: 16
text: xhttpUplinkMethod
descriptionText: qsTr("UplinkHTTPMethod")
headerText: qsTr("UplinkHTTPMethod")
drawerParent: root
listView: ListViewWithRadioButtonType {
rootWidth: root.width
model: ListModel {
Component.onCompleted: {
var opts = XrayConfigModel.xhttpUplinkMethodOptions()
for (var i = 0; i < opts.length; i++) {
append({name: opts[i]})
}
}
}
clickedFunction: function () {
xhttpUplinkMethod = selectedText
uplinkMethodDropDown.text = selectedText
uplinkMethodDropDown.closeTriggered()
}
Component.onCompleted: {
for (var i = 0; i < model.count; i++) {
if (model.get(i).name === xhttpUplinkMethod) {
selectedIndex = i;
break
}
}
}
}
Connections {
target: XrayConfigModel
function onDataChanged() {
uplinkMethodDropDown.text = xhttpUplinkMethod
}
}
}
SwitcherType {
Layout.fillWidth: true
Layout.margins: 16
Layout.topMargin: 16
text: qsTr("Disable gRPC Header")
descriptionText: qsTr("noGRPCHeader")
checked: xhttpDisableGrpc
onToggled: xhttpDisableGrpc = checked
}
DividerType {
}
SwitcherType {
Layout.fillWidth: true
Layout.margins: 16
text: qsTr("Disable SSE Header")
descriptionText: qsTr("noSSEHeader")
checked: xhttpDisableSse
onToggled: xhttpDisableSse = checked
}
DividerType {
}
// ── Session & Sequence ────────────────────────────────
CaptionTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 24
Layout.bottomMargin: 8
text: qsTr("Session & Sequence")
color: AmneziaStyle.color.mutedGray
}
DropDownType {
id: sessionPlacementDropDown
Layout.fillWidth: true
Layout.topMargin: 8
Layout.leftMargin: 16
Layout.rightMargin: 16
text: xhttpSessionPlacement
descriptionText: qsTr("SessionPlacement")
headerText: qsTr("SessionPlacement")
drawerParent: root
listView: ListViewWithRadioButtonType {
rootWidth: root.width
model: ListModel {
Component.onCompleted: {
var opts = XrayConfigModel.xhttpSessionPlacementOptions()
for (var i = 0; i < opts.length; i++) {
append({name: opts[i]})
}
}
}
clickedFunction: function () {
xhttpSessionPlacement = selectedText
sessionPlacementDropDown.text = selectedText
sessionPlacementDropDown.closeTriggered()
}
Component.onCompleted: {
for (var i = 0; i < model.count; i++) {
if (model.get(i).name === xhttpSessionPlacement) {
selectedIndex = i;
break
}
}
}
}
Connections {
target: XrayConfigModel
function onDataChanged() {
sessionPlacementDropDown.text = xhttpSessionPlacement
}
}
}
DropDownType {
id: sessionKeyDropDown
Layout.fillWidth: true
Layout.topMargin: 8
Layout.leftMargin: 16
Layout.rightMargin: 16
text: xhttpSessionKey
descriptionText: qsTr("SessionKey")
headerText: qsTr("SessionKey")
drawerParent: root
listView: ListViewWithRadioButtonType {
rootWidth: root.width
model: ListModel {
Component.onCompleted: {
var opts = XrayConfigModel.xhttpSessionKeyOptions()
for (var i = 0; i < opts.length; i++) {
append({name: opts[i]})
}
}
}
clickedFunction: function () {
xhttpSessionKey = selectedText
sessionKeyDropDown.text = selectedText
sessionKeyDropDown.closeTriggered()
}
Component.onCompleted: {
for (var i = 0; i < model.count; i++) {
if (model.get(i).name === xhttpSessionKey) {
selectedIndex = i;
break
}
}
}
}
Connections {
target: XrayConfigModel
function onDataChanged() {
sessionKeyDropDown.text = xhttpSessionKey
}
}
}
DropDownType {
id: seqPlacementDropDown
Layout.fillWidth: true
Layout.topMargin: 8
Layout.leftMargin: 16
Layout.rightMargin: 16
text: xhttpSeqPlacement
descriptionText: qsTr("SeqPlacement")
headerText: qsTr("SeqPlacement")
drawerParent: root
listView: ListViewWithRadioButtonType {
rootWidth: root.width
model: ListModel {
Component.onCompleted: {
var opts = XrayConfigModel.xhttpSeqPlacementOptions()
for (var i = 0; i < opts.length; i++) {
append({name: opts[i]})
}
}
}
clickedFunction: function () {
xhttpSeqPlacement = selectedText
seqPlacementDropDown.text = selectedText
seqPlacementDropDown.closeTriggered()
}
Component.onCompleted: {
for (var i = 0; i < model.count; i++) {
if (model.get(i).name === xhttpSeqPlacement) {
selectedIndex = i;
break
}
}
}
}
Connections {
target: XrayConfigModel
function onDataChanged() {
seqPlacementDropDown.text = xhttpSeqPlacement
}
}
}
TextFieldWithHeaderType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 8
headerText: qsTr("SeqKey")
textField.text: xhttpSeqKey
textField.onEditingFinished: {
if (textField.text !== xhttpSeqKey) xhttpSeqKey = textField.text
}
}
DropDownType {
id: uplinkDataPlacementDropDown
Layout.fillWidth: true
Layout.topMargin: 8
Layout.leftMargin: 16
Layout.rightMargin: 16
text: xhttpUplinkDataPlacement
descriptionText: qsTr("UplinkDataPlacement")
headerText: qsTr("UplinkDataPlacement")
drawerParent: root
listView: ListViewWithRadioButtonType {
rootWidth: root.width
model: ListModel {
Component.onCompleted: {
var opts = XrayConfigModel.xhttpUplinkDataPlacementOptions()
for (var i = 0; i < opts.length; i++) {
append({name: opts[i]})
}
}
}
clickedFunction: function () {
xhttpUplinkDataPlacement = selectedText
uplinkDataPlacementDropDown.text = selectedText
uplinkDataPlacementDropDown.closeTriggered()
}
Component.onCompleted: {
for (var i = 0; i < model.count; i++) {
if (model.get(i).name === xhttpUplinkDataPlacement) {
selectedIndex = i;
break
}
}
}
}
Connections {
target: XrayConfigModel
function onDataChanged() {
uplinkDataPlacementDropDown.text = xhttpUplinkDataPlacement
}
}
}
TextFieldWithHeaderType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 8
headerText: qsTr("UplinkDataKey")
textField.text: xhttpUplinkDataKey
textField.onEditingFinished: {
if (textField.text !== xhttpUplinkDataKey) xhttpUplinkDataKey = textField.text
}
}
// ── Traffic Shaping ───────────────────────────────────
CaptionTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 24
Layout.bottomMargin: 8
text: qsTr("Traffic Shaping")
color: AmneziaStyle.color.mutedGray
}
TextFieldWithHeaderType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 8
headerText: qsTr("UplinkChunkSize")
textField.text: xhttpUplinkChunkSize
textField.validator: IntValidator {
bottom: 0
}
textField.onEditingFinished: {
if (textField.text !== xhttpUplinkChunkSize) xhttpUplinkChunkSize = textField.text
}
}
TextFieldWithHeaderType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 8
headerText: qsTr("scMaxBufferedPosts")
textField.text: xhttpScMaxBufferedPosts
textField.onEditingFinished: {
if (textField.text !== xhttpScMaxBufferedPosts) xhttpScMaxBufferedPosts = textField.text
}
}
CaptionTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 16
Layout.bottomMargin: 8
text: qsTr("scMaxEachPostBytes")
color: AmneziaStyle.color.mutedGray
}
MinMaxRowType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
minValue: xhttpScMaxEachPostBytesMin
maxValue: xhttpScMaxEachPostBytesMax
onMinChanged: xhttpScMaxEachPostBytesMin = val
onMaxChanged: xhttpScMaxEachPostBytesMax = val
}
CaptionTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 16
Layout.bottomMargin: 8
text: qsTr("scStreamUpServerSecs")
color: AmneziaStyle.color.mutedGray
}
MinMaxRowType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
minValue: xhttpScStreamUpServerSecsMin
maxValue: xhttpScStreamUpServerSecsMax
onMinChanged: xhttpScStreamUpServerSecsMin = val
onMaxChanged: xhttpScStreamUpServerSecsMax = val
}
CaptionTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 16
Layout.bottomMargin: 8
text: qsTr("scMinPostsIntervalMs")
color: AmneziaStyle.color.mutedGray
}
MinMaxRowType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
minValue: xhttpScMinPostsIntervalMsMin
maxValue: xhttpScMinPostsIntervalMsMax
onMinChanged: xhttpScMinPostsIntervalMsMin = val
onMaxChanged: xhttpScMinPostsIntervalMsMax = val
}
// ── Padding and multiplexing ──────────────────────────
CaptionTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 24
Layout.bottomMargin: 8
text: qsTr("Padding and multiplexing")
color: AmneziaStyle.color.mutedGray
}
LabelWithButtonType {
Layout.fillWidth: true
text: qsTr("xPadding")
rightImageSource: "qrc:/images/controls/chevron-right.svg"
clickedFunction: function () {
PageController.goToPage(PageEnum.PageProtocolXrayXPaddingSettings)
}
}
DividerType {
}
LabelWithButtonType {
Layout.fillWidth: true
text: qsTr("XMux")
descriptionText: xmuxEnabled ? qsTr("On") : qsTr("Off")
rightImageSource: "qrc:/images/controls/chevron-right.svg"
clickedFunction: function () {
PageController.goToPage(PageEnum.PageProtocolXrayXmuxSettings)
}
}
DividerType {
}
}
Item {
Layout.preferredHeight: 16
}
}
}
BasicButtonType {
id: saveButton
anchors.left: root.left
anchors.right: root.right
anchors.bottom: root.bottom
anchors.leftMargin: 16
anchors.rightMargin: 16
anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin
visible: listView.enabled && XrayConfigModel.hasUnsavedChanges
enabled: visible
text: qsTr("Save")
clickedFunc: function () {
var headerText = qsTr("Save settings?")
var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.")
var yesButtonText = qsTr("Continue")
var noButtonText = qsTr("Cancel")
var yesButtonFunction = function () {
if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ServersUiController.processedContainerIndex) {
PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection"))
return
}
PageController.goToPage(PageEnum.PageSetupWizardInstalling)
InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.Xray)
}
var noButtonFunction = function () {
if (typeof GC !== "undefined" && !GC.isMobile()) {
saveButton.forceActiveFocus()
}
}
showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
}
}
}

View File

@@ -0,0 +1,108 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import PageEnum 1.0
import ProtocolEnum 1.0
import Style 1.0
import "./"
import "../Controls2"
import "../Controls2/TextTypes"
import "../Config"
import "../Components"
PageType {
id: root
BackButtonType {
id: backButton
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 20 + PageController.safeAreaTopMargin
}
ListViewType {
id: listView
anchors.top: backButton.bottom
anchors.bottom: saveButton.visible ? saveButton.top : parent.bottom
anchors.left: parent.left
anchors.right: parent.right
enabled: ServersUiController.isProcessedServerHasWriteAccess()
model: XrayConfigModel
delegate: ColumnLayout {
width: listView.width
spacing: 0
BaseHeaderType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 0
Layout.bottomMargin: 24
headerText: qsTr("xPaddingBytes")
}
CaptionTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 8
text: qsTr("Range")
color: AmneziaStyle.color.mutedGray
}
MinMaxRowType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
minValue: xPaddingBytesMin
maxValue: xPaddingBytesMax
onMinChanged: xPaddingBytesMin = val
onMaxChanged: xPaddingBytesMax = val
}
Item {
Layout.preferredHeight: 16
}
}
}
BasicButtonType {
id: saveButton
anchors.left: root.left
anchors.right: root.right
anchors.bottom: root.bottom
anchors.leftMargin: 16
anchors.rightMargin: 16
anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin
visible: listView.enabled && XrayConfigModel.hasUnsavedChanges
enabled: visible
text: qsTr("Save")
clickedFunc: function () {
var headerText = qsTr("Save settings?")
var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.")
var yesButtonText = qsTr("Continue")
var noButtonText = qsTr("Cancel")
var yesButtonFunction = function () {
if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ServersUiController.processedContainerIndex) {
PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection"))
return
}
PageController.goToPage(PageEnum.PageSetupWizardInstalling)
InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.Xray)
}
var noButtonFunction = function () {
if (typeof GC !== "undefined" && !GC.isMobile()) {
saveButton.forceActiveFocus()
}
}
showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
}
}
}

View File

@@ -0,0 +1,224 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import PageEnum 1.0
import ProtocolEnum 1.0
import Style 1.0
import "./"
import "../Controls2"
import "../Controls2/TextTypes"
import "../Config"
import "../Components"
PageType {
id: root
BackButtonType {
id: backButton
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 20 + PageController.safeAreaTopMargin
}
ListViewType {
id: listView
anchors.top: backButton.bottom
anchors.bottom: saveButton.visible ? saveButton.top : parent.bottom
anchors.left: parent.left
anchors.right: parent.right
enabled: ServersUiController.isProcessedServerHasWriteAccess()
model: XrayConfigModel
delegate: ColumnLayout {
width: listView.width
spacing: 0
BaseHeaderType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 0
Layout.bottomMargin: 24
headerText: qsTr("xPadding")
}
// xPaddingBytes — min/max display row
LabelWithButtonType {
Layout.fillWidth: true
text: qsTr("xPaddingBytes")
descriptionText: (xPaddingBytesMin !== "" ? xPaddingBytesMin : "0") + "—" + (xPaddingBytesMax !== "" ? xPaddingBytesMax : "0")
rightImageSource: "qrc:/images/controls/chevron-right.svg"
clickedFunction: function () {
PageController.goToPage(PageEnum.PageProtocolXrayXPaddingBytesSettings)
}
}
DividerType {
}
SwitcherType {
Layout.fillWidth: true
Layout.margins: 16
text: qsTr("xPaddingObfsMode")
checked: xPaddingObfsMode
onToggled: xPaddingObfsMode = checked
}
DividerType {
}
TextFieldWithHeaderType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 16
headerText: qsTr("xPaddingKey")
textField.text: xPaddingKey
textField.onEditingFinished: {
if (textField.text !== xPaddingKey) xPaddingKey = textField.text
}
}
TextFieldWithHeaderType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 8
headerText: qsTr("xPaddingHeader")
textField.text: xPaddingHeader
textField.onEditingFinished: {
if (textField.text !== xPaddingHeader) xPaddingHeader = textField.text
}
}
DropDownType {
id: placementDropDown
Layout.fillWidth: true
Layout.topMargin: 8
Layout.leftMargin: 16
Layout.rightMargin: 16
text: xPaddingPlacement
descriptionText: qsTr("xPaddingPlacement")
headerText: qsTr("xPaddingPlacement")
drawerParent: root
listView: ListViewWithRadioButtonType {
rootWidth: root.width
model: ListModel {
Component.onCompleted: {
var opts = XrayConfigModel.xPaddingPlacementOptions()
for (var i = 0; i < opts.length; i++) {
append({name: opts[i]})
}
}
}
clickedFunction: function () {
xPaddingPlacement = selectedText
placementDropDown.text = selectedText
placementDropDown.closeTriggered()
}
Component.onCompleted: {
for (var i = 0; i < model.count; i++) {
if (model.get(i).name === xPaddingPlacement) {
selectedIndex = i;
break
}
}
}
}
Connections {
target: XrayConfigModel
function onDataChanged() {
placementDropDown.text = xPaddingPlacement
}
}
}
DropDownType {
id: methodDropDown
Layout.fillWidth: true
Layout.topMargin: 8
Layout.leftMargin: 16
Layout.rightMargin: 16
text: xPaddingMethod
descriptionText: qsTr("xPaddingMethod")
headerText: qsTr("xPaddingMethod")
drawerParent: root
listView: ListViewWithRadioButtonType {
rootWidth: root.width
model: ListModel {
Component.onCompleted: {
var opts = XrayConfigModel.xPaddingMethodOptions()
for (var i = 0; i < opts.length; i++) {
append({name: opts[i]})
}
}
}
clickedFunction: function () {
xPaddingMethod = selectedText
methodDropDown.text = selectedText
methodDropDown.closeTriggered()
}
Component.onCompleted: {
for (var i = 0; i < model.count; i++) {
if (model.get(i).name === xPaddingMethod) {
selectedIndex = i;
break
}
}
}
}
Connections {
target: XrayConfigModel
function onDataChanged() {
methodDropDown.text = xPaddingMethod
}
}
}
Item {
Layout.preferredHeight: 16
}
}
}
BasicButtonType {
id: saveButton
anchors.left: root.left
anchors.right: root.right
anchors.bottom: root.bottom
anchors.leftMargin: 16
anchors.rightMargin: 16
anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin
visible: listView.enabled && XrayConfigModel.hasUnsavedChanges
enabled: visible
text: qsTr("Save")
clickedFunc: function () {
var headerText = qsTr("Save settings?")
var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.")
var yesButtonText = qsTr("Continue")
var noButtonText = qsTr("Cancel")
var yesButtonFunction = function () {
if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ServersUiController.processedContainerIndex) {
PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection"))
return
}
PageController.goToPage(PageEnum.PageSetupWizardInstalling)
InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.Xray)
}
var noButtonFunction = function () {
if (typeof GC !== "undefined" && !GC.isMobile()) {
saveButton.forceActiveFocus()
}
}
showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
}
}
}

View File

@@ -0,0 +1,222 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import PageEnum 1.0
import ProtocolEnum 1.0
import Style 1.0
import "./"
import "../Controls2"
import "../Controls2/TextTypes"
import "../Config"
import "../Components"
PageType {
id: root
BackButtonType {
id: backButton
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 20 + PageController.safeAreaTopMargin
}
ListViewType {
id: listView
anchors.top: backButton.bottom
anchors.bottom: saveButton.visible ? saveButton.top : parent.bottom
anchors.left: parent.left
anchors.right: parent.right
enabled: ServersUiController.isProcessedServerHasWriteAccess()
model: XrayConfigModel
delegate: ColumnLayout {
width: listView.width
spacing: 0
BaseHeaderType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 0
Layout.bottomMargin: 24
headerText: qsTr("xmux")
}
SwitcherType {
Layout.fillWidth: true
Layout.margins: 16
text: qsTr("xmux")
checked: xmuxEnabled
onToggled: xmuxEnabled = checked
}
DividerType {
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
enabled: xmuxEnabled
// maxConcurrency
CaptionTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 24
Layout.bottomMargin: 8
text: qsTr("maxConcurrency")
color: AmneziaStyle.color.mutedGray
}
MinMaxRowType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
minValue: xmuxMaxConcurrencyMin
maxValue: xmuxMaxConcurrencyMax
onMinChanged: xmuxMaxConcurrencyMin = val
onMaxChanged: xmuxMaxConcurrencyMax = val
}
// maxConnections
CaptionTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 16
Layout.bottomMargin: 8
text: qsTr("maxConnections")
color: AmneziaStyle.color.mutedGray
}
MinMaxRowType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
minValue: xmuxMaxConnectionsMin
maxValue: xmuxMaxConnectionsMax
onMinChanged: xmuxMaxConnectionsMin = val
onMaxChanged: xmuxMaxConnectionsMax = val
}
// cMaxReuseTimes
CaptionTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 16
Layout.bottomMargin: 8
text: qsTr("cMaxReuseTimes")
color: AmneziaStyle.color.mutedGray
}
MinMaxRowType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
minValue: xmuxCMaxReuseTimesMin
maxValue: xmuxCMaxReuseTimesMax
onMinChanged: xmuxCMaxReuseTimesMin = val
onMaxChanged: xmuxCMaxReuseTimesMax = val
}
// hMaxRequestTimes
CaptionTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 16
Layout.bottomMargin: 8
text: qsTr("hMaxRequestTimes")
color: AmneziaStyle.color.mutedGray
}
MinMaxRowType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
minValue: xmuxHMaxRequestTimesMin
maxValue: xmuxHMaxRequestTimesMax
onMinChanged: xmuxHMaxRequestTimesMin = val
onMaxChanged: xmuxHMaxRequestTimesMax = val
}
// hMaxReusableSecs
CaptionTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 16
Layout.bottomMargin: 8
text: qsTr("hMaxReusableSecs")
color: AmneziaStyle.color.mutedGray
}
MinMaxRowType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
minValue: xmuxHMaxReusableSecsMin
maxValue: xmuxHMaxReusableSecsMax
onMinChanged: xmuxHMaxReusableSecsMin = val
onMaxChanged: xmuxHMaxReusableSecsMax = val
}
TextFieldWithHeaderType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 16
headerText: qsTr("hKeepAlivePeriod")
textField.text: xmuxHKeepAlivePeriod
textField.validator: IntValidator {
bottom: 0
}
textField.onEditingFinished: {
if (textField.text !== xmuxHKeepAlivePeriod) xmuxHKeepAlivePeriod = textField.text
}
}
}
Item {
Layout.preferredHeight: 16
}
}
}
BasicButtonType {
id: saveButton
anchors.left: root.left
anchors.right: root.right
anchors.bottom: root.bottom
anchors.leftMargin: 16
anchors.rightMargin: 16
anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin
visible: listView.enabled && XrayConfigModel.hasUnsavedChanges
enabled: visible
text: qsTr("Save")
clickedFunc: function () {
var headerText = qsTr("Save settings?")
var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.")
var yesButtonText = qsTr("Continue")
var noButtonText = qsTr("Cancel")
var yesButtonFunction = function () {
if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ServersUiController.processedContainerIndex) {
PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection"))
return
}
PageController.goToPage(PageEnum.PageSetupWizardInstalling)
InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.Xray)
}
var noButtonFunction = function () {
if (typeof GC !== "undefined" && !GC.isMobile()) {
saveButton.forceActiveFocus()
}
}
showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -132,9 +132,11 @@ PageType {
onInstallationErrorOccurred(message)
}
function onUpdateContainerFinished(message) {
function onUpdateContainerFinished(message, closePage) {
PageController.showNotificationMessage(message)
PageController.closePage()
if (closePage) {
PageController.closePage()
}
}
function onCachedProfileCleared(message) {

View File

@@ -77,7 +77,19 @@
<file>Pages2/PageProtocolRaw.qml</file>
<file>Pages2/PageProtocolWireGuardSettings.qml</file>
<file>Pages2/PageProtocolXraySettings.qml</file>
<file>Pages2/PageProtocolXraySnapshots.qml</file>
<file>Pages2/PageProtocolXrayFlowSettings.qml</file>
<file>Pages2/PageProtocolXraySecuritySettings.qml</file>
<file>Pages2/PageProtocolXrayTransportSettings.qml</file>
<file>Pages2/PageProtocolXrayXmuxSettings.qml</file>
<file>Pages2/PageProtocolXrayXPaddingSettings.qml</file>
<file>Pages2/PageProtocolXrayXPaddingBytesSettings.qml</file>
<file>Controls2/MinMaxRowType.qml</file>
<file>Pages2/PageServiceDnsSettings.qml</file>
<file>Pages2/PageServiceMtProxySettings.qml</file>
<file>Pages2/PageServiceTelemtSettings.qml</file>
<file>Pages2/PageServiceSftpSettings.qml</file>
<file>Pages2/PageServiceSocksProxySettings.qml</file>
<file>Pages2/PageServiceTorWebsiteSettings.qml</file>