Feat: Add MtProxy (Telegram)

This commit is contained in:
dranik
2026-05-04 19:16:48 +03:00
parent 009ca981d5
commit 485d0c848a
50 changed files with 3960 additions and 29 deletions

View File

@@ -36,6 +36,7 @@ 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/controllers/appSplitTunnelingController.h
${CLIENT_ROOT_DIR}/core/controllers/ipSplitTunnelingController.h
${CLIENT_ROOT_DIR}/core/controllers/allowedDnsController.h
@@ -111,6 +112,7 @@ 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/controllers/appSplitTunnelingController.cpp
${CLIENT_ROOT_DIR}/core/controllers/ipSplitTunnelingController.cpp
${CLIENT_ROOT_DIR}/core/controllers/allowedDnsController.cpp
@@ -201,12 +203,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

@@ -101,6 +101,9 @@ void CoreController::initModels()
m_socks5ConfigModel = new Socks5ProxyConfigModel(this);
setQmlContextProperty("Socks5ProxyConfigModel", m_socks5ConfigModel);
m_mtProxyConfigModel = new MtProxyConfigModel(this);
setQmlContextProperty("MtProxyConfigModel", m_mtProxyConfigModel);
m_clientManagementModel = new ClientManagementModel(this);
setQmlContextProperty("ClientManagementModel", m_clientManagementModel);
@@ -170,7 +173,7 @@ void CoreController::initControllers()
#ifdef Q_OS_WINDOWS
m_ikev2ConfigModel,
#endif
m_sftpConfigModel, m_socks5ConfigModel, this);
m_sftpConfigModel, m_socks5ConfigModel, m_mtProxyConfigModel, this);
setQmlContextProperty("InstallController", m_installUiController);
m_importController = new ImportUiController(m_importCoreController, this);
@@ -203,6 +206,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"
@@ -69,6 +70,8 @@
#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/ipSplitTunnelingModel.h"
#include "ui/models/newsModel.h"
@@ -158,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;
@@ -210,6 +214,7 @@ private:
#endif
SftpConfigModel* m_sftpConfigModel;
Socks5ProxyConfigModel* m_socks5ConfigModel;
MtProxyConfigModel* m_mtProxyConfigModel;
CoreSignalHandlers* m_signalHandlers;
};

View File

@@ -19,6 +19,7 @@
#include "core/installers/openvpnInstaller.h"
#include "core/installers/sftpInstaller.h"
#include "core/installers/socks5Installer.h"
#include "core/installers/mtProxyInstaller.h"
#include "core/installers/torInstaller.h"
#include "core/installers/wireguardInstaller.h"
#include "core/installers/xrayInstaller.h"
@@ -35,6 +36,7 @@
#include "core/utils/constants/protocolConstants.h"
#include "core/models/serverConfig.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"
@@ -54,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,
@@ -133,6 +150,11 @@ ErrorCode InstallController::updateContainer(int serverIndex, DockerContainer co
ContainerConfig &newConfig)
{
if (!isUpdateDockerContainerRequired(container, oldConfig, newConfig)) {
if (container == DockerContainer::MtProxy) {
ServerCredentials credentials = m_serversRepository->serverCredentials(serverIndex);
SshSession sshSession(this);
MtProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
}
m_serversRepository->setContainerConfig(serverIndex, container, newConfig);
return ErrorCode::NoError;
}
@@ -154,6 +176,9 @@ ErrorCode InstallController::updateContainer(int serverIndex, DockerContainer co
}
if (errorCode == ErrorCode::NoError) {
if (container == DockerContainer::MtProxy) {
MtProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
}
clearCachedProfile(serverIndex, container);
m_serversRepository->setContainerConfig(serverIndex, container, newConfig);
}
@@ -372,9 +397,22 @@ 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);
}
return ErrorCode::NoError;
}
ErrorCode InstallController::startupContainerWorker(const ServerCredentials &credentials, DockerContainer container, const ContainerConfig &config, SshSession &sshSession)
@@ -527,6 +565,32 @@ 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::Socks5Proxy) {
return true;
}
@@ -772,6 +836,7 @@ 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));
default: return QScopedPointer<InstallerBase>(new InstallerBase(this));
}
}
@@ -810,6 +875,13 @@ 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);
}
return true;
@@ -1093,6 +1165,31 @@ 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);
}
}
}
}

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

@@ -14,6 +14,7 @@
#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/ikev2ProtocolConfig.h"
#include "core/models/protocols/torProtocolConfig.h"
@@ -91,6 +92,12 @@ 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::Ikev2: {
Ikev2ProtocolConfig ikev2Config;
config.protocolConfig = ikev2Config;

View File

@@ -0,0 +1,82 @@
#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;
}
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,20 @@
#ifndef MTPROXYINSTALLER_H
#define MTPROXYINSTALLER_H
#include "installerBase.h"
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);
};
#endif // MTPROXYINSTALLER_H

View File

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

View File

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

View File

@@ -9,6 +9,7 @@
#include "core/utils/protocolEnum.h"
#include "core/models/protocols/ikev2ProtocolConfig.h"
#include "core/models/protocols/dnsProtocolConfig.h"
#include "core/models/protocols/mtProxyProtocolConfig.h"
namespace amnezia
{
@@ -38,6 +39,8 @@ 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;
}
return Proto::Unknown;
}, data);
@@ -65,6 +68,8 @@ 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;
}
return QString();
}, data);
@@ -88,6 +93,8 @@ 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");
}
return QString();
}, data);
@@ -299,6 +306,8 @@ 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)};
default:
return ProtocolConfig{AwgProtocolConfig{}};
}

View File

@@ -22,6 +22,7 @@
#include "core/models/protocols/ikev2ProtocolConfig.h"
#include "core/models/protocols/torProtocolConfig.h"
#include "core/models/protocols/dnsProtocolConfig.h"
#include "core/models/protocols/mtProxyProtocolConfig.h"
namespace amnezia
{
@@ -36,6 +37,7 @@ struct ProtocolConfig {
XrayProtocolConfig,
SftpProtocolConfig,
Socks5ProxyProtocolConfig,
MtProxyProtocolConfig,
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

@@ -68,7 +68,9 @@ 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)") },
};
}
QMap<Proto, QString> ProtocolUtils::protocolDescriptions()
@@ -92,6 +94,7 @@ 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;
default: return ServiceType::Other;
}
}
@@ -104,6 +107,7 @@ int ProtocolUtils::getPortForInstall(Proto p)
case OpenVpn:
case Socks5Proxy:
return QRandomGenerator::global()->bounded(30000, 50000);
case MtProxy:
default:
return defaultPort(p);
}
@@ -123,6 +127,7 @@ 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();
default: return -1;
}
}
@@ -141,6 +146,7 @@ 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;
default: return false;
}
}
@@ -161,6 +167,7 @@ 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;
default: return TransportProto::Udp;
}
}
@@ -180,6 +187,7 @@ 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;
default: return false;
}
return false;
@@ -208,4 +216,3 @@ QString ProtocolUtils::getProtocolVersionString(const QJsonObject &protocolConfi
if (version == protocols::awg::awgV1_5) return QObject::tr(" (version 1.5)");
return "";
}

View File

@@ -92,6 +92,7 @@ namespace amnezia
constexpr QLatin1String xray("xray");
constexpr QLatin1String ssxray("ssxray");
constexpr QLatin1String socks5proxy("socks5proxy");
constexpr QLatin1String mtproxy("mtproxy");
constexpr QLatin1String splitTunnelSites("splitTunnelSites");
constexpr QLatin1String splitTunnelType("splitTunnelType");

View File

@@ -3,6 +3,7 @@
namespace amnezia
{
namespace protocols
{
@@ -174,9 +175,37 @@ 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 protocols
}
#endif // PROTOCOLCONSTANTS_H

View File

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

View File

@@ -72,7 +72,9 @@ 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)") },
};
}
QMap<DockerContainer, QString> ContainerUtils::containerDescriptions()
@@ -102,7 +104,10 @@ 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") },
};
}
QMap<DockerContainer, QString> ContainerUtils::containerDetailedDescriptions()
@@ -172,7 +177,12 @@ 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.") },
};
}
@@ -197,6 +207,7 @@ 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;
default: return Proto::Unknown;
}
}
@@ -224,6 +235,7 @@ 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;
default:
return false;
}
@@ -237,7 +249,7 @@ 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;
default:
return false;
}
@@ -256,6 +268,7 @@ 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;
default: return false;
}
@@ -318,6 +331,7 @@ 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;
default: return true;
}
}
@@ -346,8 +360,9 @@ int ContainerUtils::installPageOrder(DockerContainer container)
case DockerContainer::Xray: return 3;
case DockerContainer::Ipsec: return 7;
case DockerContainer::SSXray: return 8;
case DockerContainer::MtProxy:
return 20;
default: return 0;
}
}

View File

@@ -30,7 +30,8 @@ namespace amnezia
TorWebSite,
Dns,
Sftp,
Socks5Proxy
Socks5Proxy,
MtProxy,
};
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,7 @@
#include "core/models/protocols/xrayProtocolConfig.h"
#include "core/models/protocols/sftpProtocolConfig.h"
#include "core/models/protocols/socks5ProxyProtocolConfig.h"
#include "core/models/protocols/mtProxyProtocolConfig.h"
using namespace amnezia;
using namespace ProtocolUtils;
@@ -38,6 +38,7 @@ 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");
default: return QString();
}
}
@@ -285,6 +286,55 @@ 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::genProtocolVarsForContainer(DockerContainer container, const ContainerConfig &containerConfig)
{
ScriptVars vars;
@@ -309,6 +359,9 @@ amnezia::ScriptVars amnezia::genProtocolVarsForContainer(DockerContainer contain
case Proto::Socks5Proxy:
vars.append(genSocks5ProxyVars(containerConfig));
break;
case Proto::MtProxy:
vars.append(genMtProxyVars(containerConfig));
break;
default:
break;
}

View File

@@ -68,6 +68,7 @@ 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 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;
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

@@ -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

@@ -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,7 @@ namespace PageLoader
PageServiceTorWebsiteSettings,
PageServiceDnsSettings,
PageServiceSocksProxySettings,
PageServiceMtProxySettings,
PageSetupWizardStart,
PageSetupWizardCredentials,

View File

@@ -1,6 +1,7 @@
#include "exportUiController.h"
#include "../systemController.h"
#include "core/utils/qrCodeUtils.h"
ExportUiController::ExportUiController(ExportController* exportController, QObject *parent)
: QObject(parent),
@@ -51,6 +52,14 @@ void ExportUiController::generateXrayConfig(int serverIndex, const QString &clie
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;

View File

@@ -23,6 +23,7 @@ public slots:
void generateWireGuardConfig(int serverIndex, const QString &clientName);
void generateAwgConfig(int serverIndex, int containerIndex, const QString &clientName);
void generateXrayConfig(int serverIndex, const QString &clientName);
void generateQrFromString(const QString &text);
QString getConfig();
QString getNativeConfigString();

View File

@@ -5,7 +5,10 @@
#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"
@@ -69,6 +72,7 @@ InstallUiController::InstallUiController(InstallController *installController,
#endif
SftpConfigModel *sftpConfigModel,
Socks5ProxyConfigModel *socks5ConfigModel,
MtProxyConfigModel* mtConfigModel,
QObject *parent)
: QObject(parent),
m_installController(installController),
@@ -85,7 +89,8 @@ InstallUiController::InstallUiController(InstallController *installController,
m_ikev2ConfigModel(ikev2ConfigModel),
#endif
m_sftpConfigModel(sftpConfigModel),
m_socks5ConfigModel(socks5ConfigModel)
m_socks5ConfigModel(socks5ConfigModel),
m_mtProxyConfigModel(mtConfigModel)
{
connect(m_installController, &InstallController::configValidated, this, &InstallUiController::configValidated);
connect(m_installController, &InstallController::validationErrorOccurred, this, [this](ErrorCode errorCode) {
@@ -202,7 +207,7 @@ void InstallUiController::scanServerForInstalledContainers(int serverIndex)
emit installationErrorOccurred(errorCode);
}
void InstallUiController::updateContainer(int serverIndex, int containerIndex, int protocolIndex)
void InstallUiController::updateContainer(int serverIndex, int containerIndex, int protocolIndex, bool closePage)
{
DockerContainer container = static_cast<DockerContainer>(containerIndex);
@@ -241,6 +246,10 @@ void InstallUiController::updateContainer(int serverIndex, int containerIndex, i
containerConfig.protocolConfig = m_socks5ConfigModel->getProtocolConfig();
break;
}
case Proto::MtProxy: {
containerConfig.protocolConfig = m_mtProxyConfigModel->getProtocolConfig();
break;
}
#ifdef Q_OS_WINDOWS
case Proto::Ikev2: {
containerConfig.protocolConfig = m_ikev2ConfigModel->getProtocolConfig();
@@ -252,6 +261,45 @@ void InstallUiController::updateContainer(int serverIndex, int containerIndex, i
}
ContainerConfig oldContainerConfig = m_serversController->getContainerConfig(serverIndex, container);
if (container == DockerContainer::MtProxy) {
emit serverIsBusy(true);
auto *watcher = new QFutureWatcher<ErrorCode>(this);
QObject::connect(watcher, &QFutureWatcher<ErrorCode>::finished, this,
[this, watcher, serverIndex, container, closePage]() {
const ErrorCode errorCode = watcher->result();
watcher->deleteLater();
emit serverIsBusy(false);
if (errorCode == ErrorCode::NoError) {
const ContainerConfig updatedConfig =
m_serversController->getContainerConfig(serverIndex, container);
m_protocolModel->updateModel(updatedConfig);
const auto defaultContainer =
m_serversController->getServerConfig(serverIndex).defaultContainer();
if ((serverIndex == m_serversController->getDefaultServerIndex())
&& (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, serverIndex, container, oldConfigCopy,
newConfigCopy]() mutable -> ErrorCode {
return installController->updateContainer(serverIndex, container, oldConfigCopy, newConfigCopy);
});
watcher->setFuture(future);
return;
}
ErrorCode errorCode = m_installController->updateContainer(serverIndex, container, oldContainerConfig, containerConfig);
if (errorCode == ErrorCode::NoError) {
@@ -262,7 +310,7 @@ void InstallUiController::updateContainer(int serverIndex, int containerIndex, i
if ((serverIndex == m_serversController->getDefaultServerIndex()) && (container == defaultContainer)) {
emit currentContainerUpdated();
} else {
emit updateContainerFinished(tr("Settings updated successfully"));
emit updateContainerFinished(tr("Settings updated successfully"), closePage);
}
return;
@@ -271,6 +319,148 @@ void InstallUiController::updateContainer(int serverIndex, int containerIndex, i
emit installationErrorOccurred(errorCode);
}
void InstallUiController::setContainerEnabled(int serverIndex, int containerIndex, bool enabled) {
const DockerContainer container = static_cast<DockerContainer>(containerIndex);
const ServerCredentials credentials = m_serversController->getServerCredentials(serverIndex);
const QString containerName = ContainerUtils::containerToString(container);
emit serverIsBusy(true);
SshSession sshSession(this);
const QString script = enabled
? QString("sudo docker start %1").arg(containerName)
: QString("sudo docker stop %1").arg(containerName);
const ErrorCode errorCode = sshSession.runScript(credentials, script);
emit serverIsBusy(false);
if (errorCode == ErrorCode::NoError) {
ContainerConfig currentConfig = m_serversController->getContainerConfig(serverIndex, container);
if (auto *mtConfig = currentConfig.getMtProxyProtocolConfig()) {
mtConfig->isEnabled = enabled;
m_serversController->updateContainerConfig(serverIndex, container, currentConfig);
m_protocolModel->updateModel(currentConfig);
}
emit setContainerEnabledFinished(enabled);
return;
}
emit installationErrorOccurred(errorCode);
}
void InstallUiController::refreshContainerStatus(int serverIndex, int containerIndex) {
const DockerContainer container = static_cast<DockerContainer>(containerIndex);
const ServerCredentials credentials = m_serversController->getServerCredentials(serverIndex);
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 = QString(
"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) {
emit containerStatusRefreshed(3);
return;
}
const QString status = stdOut.trimmed();
if (status == "running") {
emit containerStatusRefreshed(1);
} else if (status == "not_found" || status.isEmpty()) {
emit containerStatusRefreshed(0);
} else if (status == "exited" || status == "created" || status == "paused") {
emit containerStatusRefreshed(2);
} else {
emit containerStatusRefreshed(3);
}
}
void InstallUiController::refreshContainerDiagnostics(int serverIndex, int containerIndex, int port) {
const ServerCredentials credentials = m_serversController->getServerCredentials(serverIndex);
const DockerContainer container = static_cast<DockerContainer>(containerIndex);
const QString containerName = ContainerUtils::containerToString(container);
const QString script =
QString(
"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(port);
QString stdOut;
auto cbReadStdOut = [&](const QString &data, libssh::Client &) {
stdOut += data;
return ErrorCode::NoError;
};
SshSession sshSession(this);
const ErrorCode errorCode = sshSession.runScript(credentials, script, cbReadStdOut);
if (errorCode != ErrorCode::NoError) {
emit containerDiagnosticsRefreshed(false, false, -1, QString(), QString());
return;
}
bool portReachable = false;
bool upstreamReachable = false;
int clientsConnected = -1;
QString lastConfigRefresh;
QString statsEndpoint;
for (const QString &line: stdOut.split('\n', Qt::SkipEmptyParts)) {
if (line.startsWith("PORT_OK=")) {
portReachable = line.mid(8).trimmed() == "yes";
} else if (line.startsWith("TG_OK=")) {
upstreamReachable = line.mid(6).trimmed() == "yes";
} else if (line.startsWith("CLIENTS=")) {
clientsConnected = line.mid(8).trimmed().toInt();
} else if (line.startsWith("CONF_TIME=")) {
lastConfigRefresh = line.mid(10).trimmed();
} else if (line.startsWith("STATS=")) {
statsEndpoint = line.mid(6).trimmed();
}
}
emit containerDiagnosticsRefreshed(portReachable, upstreamReachable, clientsConnected, lastConfigRefresh,
statsEndpoint);
}
void InstallUiController::fetchContainerSecret(int serverIndex, int containerIndex) {
const ServerCredentials credentials = m_serversController->getServerCredentials(serverIndex);
const DockerContainer container = static_cast<DockerContainer>(containerIndex);
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) {
emit containerSecretFetched(QString());
return;
}
const QString secret = stdOut.trimmed();
static const QRegularExpression hex32(QStringLiteral("^[0-9a-fA-F]{32}$"));
emit containerSecretFetched(hex32.match(secret).hasMatch() ? secret : QString());
}
void InstallUiController::rebootServer(int serverIndex)
{
QString serverName = m_serversController->getServerConfig(serverIndex).displayName();
@@ -484,10 +674,10 @@ void InstallUiController::updateProtocolConfigModel(int serverIndex, int contain
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;
#ifdef Q_OS_WINDOWS
case Proto::Ikev2: updateIfPresent(m_ikev2ConfigModel, containerConfig.getIkev2ProtocolConfig()); break;
#endif
default: break;
}
}

View File

@@ -28,6 +28,7 @@
#include "ui/models/services/torConfigModel.h"
#include "core/models/protocols/sftpProtocolConfig.h"
#include "core/models/protocols/socks5ProxyProtocolConfig.h"
#include "ui/models/services/mtProxyConfigModel.h"
class InstallUiController : public QObject
{
@@ -48,6 +49,7 @@ public:
#endif
SftpConfigModel* sftpConfigModel,
Socks5ProxyConfigModel* socks5ConfigModel,
MtProxyConfigModel* mtConfigModel,
QObject *parent = nullptr);
~InstallUiController();
@@ -58,12 +60,16 @@ public slots:
void scanServerForInstalledContainers(int serverIndex);
void updateContainer(int serverIndex, int containerIndex, int protocolIndex);
void updateContainer(int serverIndex, int containerIndex, int protocolIndex, bool closePage = true);
void removeServer(int serverIndex);
void rebootServer(int serverIndex);
void removeAllContainers(int serverIndex);
void removeContainer(int serverIndex, int containerIndex);
void setContainerEnabled(int serverIndex, int containerIndex, bool enabled);
void refreshContainerStatus(int serverIndex, int containerIndex);
void refreshContainerDiagnostics(int serverIndex, int containerIndex, int port);
void fetchContainerSecret(int serverIndex, int containerIndex);
void clearCachedProfile(int serverIndex, int containerIndex);
@@ -94,7 +100,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 +108,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);
@@ -140,6 +151,7 @@ private:
#endif
SftpConfigModel* m_sftpConfigModel;
Socks5ProxyConfigModel* m_socks5ConfigModel;
MtProxyConfigModel* m_mtProxyConfigModel;
ServerCredentials m_processedServerCredentials;

View File

@@ -482,6 +482,8 @@ 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");
}
}
}

View File

@@ -74,6 +74,7 @@ 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 InstallPageOrderRole: return ContainerUtils::installPageOrder(container);
}
@@ -184,5 +185,6 @@ QHash<int, QByteArray> ContainersModel::roleNames() const
roles[IsSftpRole] = "isSftp";
roles[IsTorWebsiteRole] = "isTorWebsite";
roles[IsSocks5ProxyRole] = "isSocks5Proxy";
roles[IsMtProxyRole] = "isMtProxy";
return roles;
}

View File

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

View File

@@ -42,6 +42,7 @@ QHash<int, QByteArray> ProtocolsModel::roleNames() const
roles[IsSftpRole] = "isSftp";
roles[IsIpsecRole] = "isIpsec";
roles[IsSocks5ProxyRole] = "isSocks5Proxy";
roles[IsMtProxyRole] = "isMtProxy";
return roles;
}
@@ -71,6 +72,7 @@ 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 RawConfigRole:
return getRawConfig();
case IsClientProtocolExistsRole:
@@ -124,6 +126,7 @@ 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;
default: return PageLoader::PageEnum::PageProtocolOpenVpnSettings;
}
}

View File

@@ -25,7 +25,8 @@ public:
IsXrayRole,
IsSftpRole,
IsIpsecRole,
IsSocks5ProxyRole
IsSocks5ProxyRole,
IsMtProxyRole,
};
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,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,9 @@ ListViewType {
PageController.goToPage(PageEnum.PageProtocolRaw)
} else if (isDns) {
PageController.goToPage(PageEnum.PageServiceDnsSettings)
} else if (isMtProxy) {
MtProxyConfigModel.updateModel(config)
PageController.goToPage(PageEnum.PageServiceMtProxySettings)
} else {
InstallController.updateProtocols(ServersUiController.processedIndex, 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()
}

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

@@ -78,6 +78,7 @@
<file>Pages2/PageProtocolWireGuardSettings.qml</file>
<file>Pages2/PageProtocolXraySettings.qml</file>
<file>Pages2/PageServiceDnsSettings.qml</file>
<file>Pages2/PageServiceMtProxySettings.qml</file>
<file>Pages2/PageServiceSftpSettings.qml</file>
<file>Pages2/PageServiceSocksProxySettings.qml</file>
<file>Pages2/PageServiceTorWebsiteSettings.qml</file>