diff --git a/client/cmake/sources.cmake b/client/cmake/sources.cmake index be96468d4..97ad0fd3a 100644 --- a/client/cmake/sources.cmake +++ b/client/cmake/sources.cmake @@ -37,6 +37,7 @@ set(HEADERS ${HEADERS} ${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 @@ -113,6 +114,7 @@ set(SOURCES ${SOURCES} ${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 diff --git a/client/core/controllers/coreController.cpp b/client/core/controllers/coreController.cpp index 33920196e..608cecaf5 100644 --- a/client/core/controllers/coreController.cpp +++ b/client/core/controllers/coreController.cpp @@ -104,6 +104,9 @@ void CoreController::initModels() 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); @@ -173,7 +176,7 @@ void CoreController::initControllers() #ifdef Q_OS_WINDOWS m_ikev2ConfigModel, #endif - m_sftpConfigModel, m_socks5ConfigModel, m_mtProxyConfigModel, this); + m_sftpConfigModel, m_socks5ConfigModel, m_mtProxyConfigModel, m_telemtConfigModel, this); setQmlContextProperty("InstallController", m_installUiController); m_importController = new ImportUiController(m_importCoreController, this); diff --git a/client/core/controllers/coreController.h b/client/core/controllers/coreController.h index 8100379e5..ac597b81f 100644 --- a/client/core/controllers/coreController.h +++ b/client/core/controllers/coreController.h @@ -71,6 +71,7 @@ #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" @@ -215,6 +216,7 @@ private: SftpConfigModel* m_sftpConfigModel; Socks5ProxyConfigModel* m_socks5ConfigModel; MtProxyConfigModel* m_mtProxyConfigModel; + TelemtConfigModel* m_telemtConfigModel; CoreSignalHandlers* m_signalHandlers; }; diff --git a/client/core/controllers/selfhosted/installController.cpp b/client/core/controllers/selfhosted/installController.cpp index a34e55709..a3fc0a8b5 100644 --- a/client/core/controllers/selfhosted/installController.cpp +++ b/client/core/controllers/selfhosted/installController.cpp @@ -20,6 +20,7 @@ #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" @@ -154,6 +155,10 @@ ErrorCode InstallController::updateContainer(int serverIndex, DockerContainer co ServerCredentials credentials = m_serversRepository->serverCredentials(serverIndex); SshSession sshSession(this); MtProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig); + } else if (container == DockerContainer::Telemt) { + ServerCredentials credentials = m_serversRepository->serverCredentials(serverIndex); + SshSession sshSession(this); + TelemtInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig); } m_serversRepository->setContainerConfig(serverIndex, container, newConfig); return ErrorCode::NoError; @@ -178,6 +183,8 @@ ErrorCode InstallController::updateContainer(int serverIndex, DockerContainer co 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(serverIndex, container); m_serversRepository->setContainerConfig(serverIndex, container, newConfig); @@ -410,6 +417,8 @@ ErrorCode InstallController::configureContainerWorker(const ServerCredentials &c if (container == DockerContainer::MtProxy) { MtProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, config); + } else if (container == DockerContainer::Telemt) { + TelemtInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, config); } return ErrorCode::NoError; @@ -591,6 +600,53 @@ bool InstallController::isReinstallContainerRequired(DockerContainer container, } } + 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; } @@ -837,6 +893,7 @@ QScopedPointer InstallController::createInstaller(DockerContainer case DockerContainer::Sftp: return QScopedPointer(new SftpInstaller(this)); case DockerContainer::Socks5Proxy: return QScopedPointer(new Socks5Installer(this)); case DockerContainer::MtProxy: return QScopedPointer(new MtProxyInstaller(this)); + case DockerContainer::Telemt: return QScopedPointer(new TelemtInstaller(this)); default: return QScopedPointer(new InstallerBase(this)); } } @@ -882,6 +939,13 @@ bool InstallController::isUpdateDockerContainerRequired(DockerContainer containe 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; @@ -1190,6 +1254,31 @@ void InstallController::updateContainerConfigAfterInstallation(DockerContainer c 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); + } + } } } diff --git a/client/core/diagnostics/telemtDiagnostics.h b/client/core/diagnostics/telemtDiagnostics.h new file mode 100644 index 000000000..d2860d299 --- /dev/null +++ b/client/core/diagnostics/telemtDiagnostics.h @@ -0,0 +1,20 @@ +#ifndef TELEMTDIAGNOSTICS_H +#define TELEMTDIAGNOSTICS_H + +#include "containerDiagnostics.h" + +#include + +namespace amnezia +{ + struct TelemtDiagnostics : ContainerDiagnostics + { + bool upstreamReachable = false; + int clientsConnected = -1; + QString lastConfigRefresh; + QString statsEndpoint; + }; + +} // namespace amnezia + +#endif // TELEMTDIAGNOSTICS_H diff --git a/client/core/installers/installerBase.cpp b/client/core/installers/installerBase.cpp index d4243a5f4..2dc08e85a 100644 --- a/client/core/installers/installerBase.cpp +++ b/client/core/installers/installerBase.cpp @@ -15,6 +15,7 @@ #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" @@ -98,6 +99,12 @@ ContainerConfig InstallerBase::createBaseConfig(DockerContainer container, int p config.protocolConfig = mtConfig; break; } + case Proto::Telemt: { + TelemtProtocolConfig telemtConfig; + telemtConfig.port = portStr; + config.protocolConfig = telemtConfig; + break; + } case Proto::Ikev2: { Ikev2ProtocolConfig ikev2Config; config.protocolConfig = ikev2Config; diff --git a/client/core/installers/telemtInstaller.cpp b/client/core/installers/telemtInstaller.cpp new file mode 100644 index 000000000..ba5173a6a --- /dev/null +++ b/client/core/installers/telemtInstaller.cpp @@ -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 +#include +#include +#include + +#include + +using namespace amnezia; + +namespace { + constexpr QLatin1String kTelemtClientJsonPath("/data/amnezia-telemt-client.json"); + constexpr QLatin1String kTelemtClientJsonUploadPath("data/amnezia-telemt-client.json"); + constexpr QLatin1String kTelemtSecretPath("/data/.amnezia-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; + } +} diff --git a/client/core/installers/telemtInstaller.h b/client/core/installers/telemtInstaller.h new file mode 100644 index 000000000..13323e8a7 --- /dev/null +++ b/client/core/installers/telemtInstaller.h @@ -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 diff --git a/client/core/models/containerConfig.cpp b/client/core/models/containerConfig.cpp index deb123d45..6a008a13d 100644 --- a/client/core/models/containerConfig.cpp +++ b/client/core/models/containerConfig.cpp @@ -123,6 +123,16 @@ const MtProxyProtocolConfig* ContainerConfig::getMtProxyProtocolConfig() const return protocolConfig.as(); } +TelemtProtocolConfig* ContainerConfig::getTelemtProtocolConfig() +{ + return protocolConfig.as(); +} + +const TelemtProtocolConfig* ContainerConfig::getTelemtProtocolConfig() const +{ + return protocolConfig.as(); +} + Ikev2ProtocolConfig* ContainerConfig::getIkev2ProtocolConfig() { return protocolConfig.as(); diff --git a/client/core/models/containerConfig.h b/client/core/models/containerConfig.h index 9f116fc08..b07ff6dff 100644 --- a/client/core/models/containerConfig.h +++ b/client/core/models/containerConfig.h @@ -60,6 +60,9 @@ struct ContainerConfig { MtProxyProtocolConfig* getMtProxyProtocolConfig(); const MtProxyProtocolConfig* getMtProxyProtocolConfig() const; + TelemtProtocolConfig* getTelemtProtocolConfig(); + const TelemtProtocolConfig* getTelemtProtocolConfig() const; + Ikev2ProtocolConfig* getIkev2ProtocolConfig(); const Ikev2ProtocolConfig* getIkev2ProtocolConfig() const; diff --git a/client/core/models/protocolConfig.cpp b/client/core/models/protocolConfig.cpp index 2b3d60864..24e879f18 100644 --- a/client/core/models/protocolConfig.cpp +++ b/client/core/models/protocolConfig.cpp @@ -10,6 +10,7 @@ #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 { @@ -41,6 +42,8 @@ Proto ProtocolConfig::type() const return Proto::Dns; } else if constexpr (std::is_same_v) { return Proto::MtProxy; + } else if constexpr (std::is_same_v) { + return Proto::Telemt; } return Proto::Unknown; }, data); @@ -70,6 +73,8 @@ QString ProtocolConfig::port() const return QString(); } else if constexpr (std::is_same_v) { return arg.port.isEmpty() ? QString(protocols::mtProxy::defaultPort) : arg.port; + } else if constexpr (std::is_same_v) { + return arg.port.isEmpty() ? QString(protocols::telemt::defaultPort) : arg.port; } return QString(); }, data); @@ -95,6 +100,8 @@ QString ProtocolConfig::transportProto() const return QString(); } else if constexpr (std::is_same_v) { return QStringLiteral("tcp"); + } else if constexpr (std::is_same_v) { + return QStringLiteral("tcp"); } return QString(); }, data); @@ -308,6 +315,8 @@ ProtocolConfig ProtocolConfig::fromJson(const QJsonObject& json, Proto type) 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{}}; } diff --git a/client/core/models/protocolConfig.h b/client/core/models/protocolConfig.h index 8a8cf91f2..324530087 100644 --- a/client/core/models/protocolConfig.h +++ b/client/core/models/protocolConfig.h @@ -23,6 +23,7 @@ #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 { @@ -38,6 +39,7 @@ struct ProtocolConfig { SftpProtocolConfig, Socks5ProxyProtocolConfig, MtProxyProtocolConfig, + TelemtProtocolConfig, Ikev2ProtocolConfig, TorProtocolConfig, DnsProtocolConfig diff --git a/client/core/models/protocols/telemtProtocolConfig.cpp b/client/core/models/protocols/telemtProtocolConfig.cpp new file mode 100644 index 000000000..5f55d0e10 --- /dev/null +++ b/client/core/models/protocols/telemtProtocolConfig.cpp @@ -0,0 +1,162 @@ +#include "telemtProtocolConfig.h" + +#include "core/utils/constants/configKeys.h" +#include "core/utils/constants/protocolConstants.h" + +#include +#include + +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; +} diff --git a/client/core/models/protocols/telemtProtocolConfig.h b/client/core/models/protocols/telemtProtocolConfig.h new file mode 100644 index 000000000..0a8830e60 --- /dev/null +++ b/client/core/models/protocols/telemtProtocolConfig.h @@ -0,0 +1,38 @@ +#ifndef TELEMTPROTOCOLCONFIG_H +#define TELEMTPROTOCOLCONFIG_H + +#include +#include +#include + +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 diff --git a/client/core/protocols/protocolUtils.cpp b/client/core/protocols/protocolUtils.cpp index 2f8d10c2b..fe8a1454b 100644 --- a/client/core/protocols/protocolUtils.cpp +++ b/client/core/protocols/protocolUtils.cpp @@ -70,6 +70,7 @@ QMap ProtocolUtils::protocolHumanNames() { Proto::Sftp, QObject::tr("SFTP service") }, { Proto::Socks5Proxy, QObject::tr("SOCKS5 proxy server") }, { Proto::MtProxy, QObject::tr("MTProxy (Telegram)") }, + { Proto::Telemt, QObject::tr("Telemt (Telegram)") }, }; } @@ -95,6 +96,7 @@ ServiceType ProtocolUtils::protocolService(Proto p) 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; } } @@ -108,6 +110,7 @@ int ProtocolUtils::getPortForInstall(Proto p) case Socks5Proxy: return QRandomGenerator::global()->bounded(30000, 50000); case MtProxy: + case Telemt: default: return defaultPort(p); } @@ -128,6 +131,7 @@ int ProtocolUtils::defaultPort(Proto p) 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; } } @@ -147,6 +151,7 @@ bool ProtocolUtils::defaultPortChangeable(Proto p) case Proto::Sftp: return true; case Proto::Socks5Proxy: return true; case Proto::MtProxy: return true; + case Proto::Telemt: return true; default: return false; } } @@ -168,6 +173,7 @@ TransportProto ProtocolUtils::defaultTransportProto(Proto p) 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; } } @@ -188,9 +194,9 @@ bool ProtocolUtils::defaultTransportProtoChangeable(Proto p) 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) diff --git a/client/core/utils/constants/configKeys.h b/client/core/utils/constants/configKeys.h index f47e3eb95..ff84d67a1 100644 --- a/client/core/utils/constants/configKeys.h +++ b/client/core/utils/constants/configKeys.h @@ -93,6 +93,7 @@ namespace amnezia constexpr QLatin1String ssxray("ssxray"); constexpr QLatin1String socks5proxy("socks5proxy"); constexpr QLatin1String mtproxy("mtproxy"); + constexpr QLatin1String telemt("telemt"); constexpr QLatin1String splitTunnelSites("splitTunnelSites"); constexpr QLatin1String splitTunnelType("splitTunnelType"); diff --git a/client/core/utils/constants/protocolConstants.h b/client/core/utils/constants/protocolConstants.h index 0cb471d61..ec502669d 100644 --- a/client/core/utils/constants/protocolConstants.h +++ b/client/core/utils/constants/protocolConstants.h @@ -205,6 +205,40 @@ namespace amnezia 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 } diff --git a/client/core/utils/containerEnum.h b/client/core/utils/containerEnum.h index 8e4fc33f8..986aff92f 100644 --- a/client/core/utils/containerEnum.h +++ b/client/core/utils/containerEnum.h @@ -25,6 +25,7 @@ namespace amnezia Sftp, Socks5Proxy, MtProxy, + Telemt, }; Q_ENUM_NS(DockerContainer) } // namespace ContainerEnumNS diff --git a/client/core/utils/containers/containerUtils.cpp b/client/core/utils/containers/containerUtils.cpp index cda3353d5..1efc2cccd 100644 --- a/client/core/utils/containers/containerUtils.cpp +++ b/client/core/utils/containers/containerUtils.cpp @@ -74,6 +74,7 @@ QMap ContainerUtils::containerHumanNames() { DockerContainer::Sftp, QObject::tr("SFTP file sharing service") }, { DockerContainer::Socks5Proxy, QObject::tr("SOCKS5 proxy server") }, { DockerContainer::MtProxy, QObject::tr("MTProxy (Telegram)") }, + { DockerContainer::Telemt, QObject::tr("Telemt (Telegram)") }, }; } @@ -107,6 +108,8 @@ QMap ContainerUtils::containerDescriptions() QObject::tr("") }, { DockerContainer::MtProxy, QObject::tr("Telegram MTProto proxy server") }, + { DockerContainer::Telemt, + QObject::tr("Telegram MTProto proxy (Telemt, Rust)") }, }; } @@ -183,6 +186,9 @@ QMap ContainerUtils::containerDetailedDescriptions() "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.") } }; } @@ -208,6 +214,7 @@ Proto ContainerUtils::defaultProtocol(DockerContainer c) 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; } } @@ -236,6 +243,7 @@ bool ContainerUtils::isSupportedByCurrentPlatform(DockerContainer c) case DockerContainer::Xray: return true; case DockerContainer::SSXray: return true; case DockerContainer::MtProxy: return true; + case DockerContainer::Telemt: return true; default: return false; } @@ -250,6 +258,7 @@ bool ContainerUtils::isSupportedByCurrentPlatform(DockerContainer c) case DockerContainer::Xray: return true; case DockerContainer::SSXray: return true; case DockerContainer::MtProxy: return true; + case DockerContainer::Telemt: return true; default: return false; } @@ -269,6 +278,7 @@ bool ContainerUtils::isSupportedByCurrentPlatform(DockerContainer c) case DockerContainer::Xray: return true; case DockerContainer::SSXray: return true; case DockerContainer::MtProxy: return true; + case DockerContainer::Telemt: return true; default: return false; } @@ -332,6 +342,7 @@ bool ContainerUtils::isShareable(DockerContainer container) case DockerContainer::Sftp: return false; case DockerContainer::Socks5Proxy: return false; case DockerContainer::MtProxy: return false; + case DockerContainer::Telemt: return false; default: return true; } } @@ -361,6 +372,7 @@ int ContainerUtils::installPageOrder(DockerContainer container) case DockerContainer::Ipsec: return 7; case DockerContainer::SSXray: return 8; case DockerContainer::MtProxy: + case DockerContainer::Telemt: return 20; default: return 0; } diff --git a/client/core/utils/protocolEnum.h b/client/core/utils/protocolEnum.h index 5293d3fe2..19fdc67dc 100644 --- a/client/core/utils/protocolEnum.h +++ b/client/core/utils/protocolEnum.h @@ -32,6 +32,7 @@ namespace amnezia Sftp, Socks5Proxy, MtProxy, + Telemt, }; Q_ENUM_NS(Proto) diff --git a/client/core/utils/selfhosted/scriptsRegistry.cpp b/client/core/utils/selfhosted/scriptsRegistry.cpp index a5a54c205..164b58576 100644 --- a/client/core/utils/selfhosted/scriptsRegistry.cpp +++ b/client/core/utils/selfhosted/scriptsRegistry.cpp @@ -20,6 +20,7 @@ #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; @@ -39,6 +40,7 @@ QString amnezia::scriptFolder(amnezia::DockerContainer container) 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(); } } @@ -335,6 +337,37 @@ amnezia::ScriptVars amnezia::genMtProxyVars(const ContainerConfig &containerConf 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; @@ -362,6 +395,9 @@ amnezia::ScriptVars amnezia::genProtocolVarsForContainer(DockerContainer contain case Proto::MtProxy: vars.append(genMtProxyVars(containerConfig)); break; + case Proto::Telemt: + vars.append(genTelemtVars(containerConfig)); + break; default: break; } diff --git a/client/core/utils/selfhosted/scriptsRegistry.h b/client/core/utils/selfhosted/scriptsRegistry.h index 5789a8c20..ee94a9219 100644 --- a/client/core/utils/selfhosted/scriptsRegistry.h +++ b/client/core/utils/selfhosted/scriptsRegistry.h @@ -69,6 +69,7 @@ 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); } diff --git a/client/core/utils/selfhosted/sshSession.cpp b/client/core/utils/selfhosted/sshSession.cpp index c2360c6d1..363745fd5 100644 --- a/client/core/utils/selfhosted/sshSession.cpp +++ b/client/core/utils/selfhosted/sshSession.cpp @@ -103,7 +103,7 @@ ErrorCode SshSession::runContainerScript(const ServerCredentials &credentials, D if (e) return e; - const bool useSh = container == DockerContainer::Socks5Proxy || container == DockerContainer::MtProxy; + 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); diff --git a/client/server_scripts/serverScripts.qrc b/client/server_scripts/serverScripts.qrc index 35a2be2a3..278e16953 100644 --- a/client/server_scripts/serverScripts.qrc +++ b/client/server_scripts/serverScripts.qrc @@ -28,6 +28,10 @@ mtproxy/Dockerfile mtproxy/run_container.sh mtproxy/start.sh + telemt/configure_container.sh + telemt/Dockerfile + telemt/run_container.sh + telemt/start.sh openvpn/configure_container.sh openvpn/Dockerfile openvpn/run_container.sh diff --git a/client/server_scripts/telemt/Dockerfile b/client/server_scripts/telemt/Dockerfile new file mode 100644 index 000000000..ad3f27365 --- /dev/null +++ b/client/server_scripts/telemt/Dockerfile @@ -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 [""] diff --git a/client/server_scripts/telemt/configure_container.sh b/client/server_scripts/telemt/configure_container.sh new file mode 100644 index 000000000..b3090eeb6 --- /dev/null +++ b/client/server_scripts/telemt/configure_container.sh @@ -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/.amnezia-secret ]; then + SECRET=$(cat /data/.amnezia-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/.amnezia-secret +chmod 600 /data/.amnezia-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: " diff --git a/client/server_scripts/telemt/run_container.sh b/client/server_scripts/telemt/run_container.sh new file mode 100644 index 000000000..24d3516e3 --- /dev/null +++ b/client/server_scripts/telemt/run_container.sh @@ -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 diff --git a/client/server_scripts/telemt/start.sh b/client/server_scripts/telemt/start.sh new file mode 100644 index 000000000..c7799aa4d --- /dev/null +++ b/client/server_scripts/telemt/start.sh @@ -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 diff --git a/client/ui/controllers/qml/pageController.h b/client/ui/controllers/qml/pageController.h index c6349dc9b..7dcd3d3ca 100644 --- a/client/ui/controllers/qml/pageController.h +++ b/client/ui/controllers/qml/pageController.h @@ -51,6 +51,7 @@ namespace PageLoader PageServiceDnsSettings, PageServiceSocksProxySettings, PageServiceMtProxySettings, + PageServiceTelemtSettings, PageSetupWizardStart, PageSetupWizardCredentials, diff --git a/client/ui/controllers/selfhosted/installUiController.cpp b/client/ui/controllers/selfhosted/installUiController.cpp index 2c57e3d2a..75ad40167 100755 --- a/client/ui/controllers/selfhosted/installUiController.cpp +++ b/client/ui/controllers/selfhosted/installUiController.cpp @@ -73,6 +73,7 @@ InstallUiController::InstallUiController(InstallController *installController, SftpConfigModel *sftpConfigModel, Socks5ProxyConfigModel *socks5ConfigModel, MtProxyConfigModel* mtConfigModel, + TelemtConfigModel *telemtConfigModel, QObject *parent) : QObject(parent), m_installController(installController), @@ -90,7 +91,8 @@ InstallUiController::InstallUiController(InstallController *installController, #endif m_sftpConfigModel(sftpConfigModel), m_socks5ConfigModel(socks5ConfigModel), - m_mtProxyConfigModel(mtConfigModel) + m_mtProxyConfigModel(mtConfigModel), + m_telemtConfigModel(telemtConfigModel) { connect(m_installController, &InstallController::configValidated, this, &InstallUiController::configValidated); connect(m_installController, &InstallController::validationErrorOccurred, this, [this](ErrorCode errorCode) { @@ -250,6 +252,10 @@ void InstallUiController::updateContainer(int serverIndex, int containerIndex, i 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(); @@ -261,7 +267,7 @@ void InstallUiController::updateContainer(int serverIndex, int containerIndex, i } ContainerConfig oldContainerConfig = m_serversController->getContainerConfig(serverIndex, container); - if (container == DockerContainer::MtProxy) { + if (container == DockerContainer::MtProxy || container == DockerContainer::Telemt) { emit serverIsBusy(true); auto *watcher = new QFutureWatcher(this); QObject::connect(watcher, &QFutureWatcher::finished, this, @@ -338,6 +344,10 @@ void InstallUiController::setContainerEnabled(int serverIndex, int containerInde mtConfig->isEnabled = enabled; m_serversController->updateContainerConfig(serverIndex, container, currentConfig); m_protocolModel->updateModel(currentConfig); + } else if (auto *telemtConfig = currentConfig.getTelemtProtocolConfig()) { + telemtConfig->isEnabled = enabled; + m_serversController->updateContainerConfig(serverIndex, container, currentConfig); + m_protocolModel->updateModel(currentConfig); } emit setContainerEnabledFinished(enabled); return; @@ -447,7 +457,8 @@ void InstallUiController::fetchContainerSecret(int serverIndex, int containerInd }; SshSession sshSession(this); - const QString path = QStringLiteral("/data/secret"); + const QString path = container == DockerContainer::Telemt ? QStringLiteral("/data/.amnezia-secret") + : QStringLiteral("/data/secret"); const QString cmd = QStringLiteral("sudo docker exec %1 cat %2").arg(containerName, path); const ErrorCode errorCode = sshSession.runScript(credentials, cmd, cbReadStdOut); @@ -675,6 +686,7 @@ void InstallUiController::updateProtocolConfigModel(int serverIndex, int contain 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 diff --git a/client/ui/controllers/selfhosted/installUiController.h b/client/ui/controllers/selfhosted/installUiController.h index 225fb85e5..26bac4ab6 100644 --- a/client/ui/controllers/selfhosted/installUiController.h +++ b/client/ui/controllers/selfhosted/installUiController.h @@ -29,6 +29,7 @@ #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 { @@ -50,6 +51,7 @@ public: SftpConfigModel* sftpConfigModel, Socks5ProxyConfigModel* socks5ConfigModel, MtProxyConfigModel* mtConfigModel, + TelemtConfigModel* telemtConfigModel, QObject *parent = nullptr); ~InstallUiController(); @@ -152,6 +154,7 @@ private: SftpConfigModel* m_sftpConfigModel; Socks5ProxyConfigModel* m_socks5ConfigModel; MtProxyConfigModel* m_mtProxyConfigModel; + TelemtConfigModel* m_telemtConfigModel; ServerCredentials m_processedServerCredentials; diff --git a/client/ui/controllers/serversUiController.cpp b/client/ui/controllers/serversUiController.cpp index 5974e6646..c5c295d00 100644 --- a/client/ui/controllers/serversUiController.cpp +++ b/client/ui/controllers/serversUiController.cpp @@ -484,6 +484,8 @@ QStringList ServersUiController::getAllInstalledServicesName(int serverIndex) co servicesName.append("SOCKS5"); } else if (container == DockerContainer::MtProxy) { servicesName.append("MTProxy"); + } else if (container == DockerContainer::Telemt) { + servicesName.append("Telemt"); } } } diff --git a/client/ui/models/containersModel.cpp b/client/ui/models/containersModel.cpp index ade74f984..e176ac167 100644 --- a/client/ui/models/containersModel.cpp +++ b/client/ui/models/containersModel.cpp @@ -75,6 +75,7 @@ QVariant ContainersModel::data(const QModelIndex &index, int role) const 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); } @@ -186,5 +187,6 @@ QHash ContainersModel::roleNames() const roles[IsTorWebsiteRole] = "isTorWebsite"; roles[IsSocks5ProxyRole] = "isSocks5Proxy"; roles[IsMtProxyRole] = "isMtProxy"; + roles[IsTelemtRole] = "isTelemt"; return roles; } diff --git a/client/ui/models/containersModel.h b/client/ui/models/containersModel.h index d88628d91..eec1be794 100644 --- a/client/ui/models/containersModel.h +++ b/client/ui/models/containersModel.h @@ -50,6 +50,7 @@ public: IsTorWebsiteRole, IsSocks5ProxyRole, IsMtProxyRole, + IsTelemtRole, }; Q_INVOKABLE void openContainerSettings(int containerIndex); diff --git a/client/ui/models/protocolsModel.cpp b/client/ui/models/protocolsModel.cpp index c0cbe99ce..b5d07e3c4 100644 --- a/client/ui/models/protocolsModel.cpp +++ b/client/ui/models/protocolsModel.cpp @@ -43,6 +43,7 @@ QHash ProtocolsModel::roleNames() const roles[IsIpsecRole] = "isIpsec"; roles[IsSocks5ProxyRole] = "isSocks5Proxy"; roles[IsMtProxyRole] = "isMtProxy"; + roles[IsTelemtRole] = "isTelemt"; return roles; } @@ -73,6 +74,7 @@ QVariant ProtocolsModel::data(const QModelIndex &index, int role) const 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: @@ -127,6 +129,7 @@ PageLoader::PageEnum ProtocolsModel::serverProtocolPage(Proto protocol) const 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; } } diff --git a/client/ui/models/protocolsModel.h b/client/ui/models/protocolsModel.h index 01f9b9cd4..e5ed345a8 100644 --- a/client/ui/models/protocolsModel.h +++ b/client/ui/models/protocolsModel.h @@ -27,6 +27,7 @@ public: IsIpsecRole, IsSocks5ProxyRole, IsMtProxyRole, + IsTelemtRole, }; explicit ProtocolsModel(QObject *parent = nullptr); diff --git a/client/ui/models/services/telemtConfigModel.cpp b/client/ui/models/services/telemtConfigModel.cpp new file mode 100644 index 000000000..6a3fd9eb1 --- /dev/null +++ b/client/ui/models/services/telemtConfigModel.cpp @@ -0,0 +1,406 @@ +#include "telemtConfigModel.h" + +#include + +#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{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{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{AdditionalSecretsRole}); +} + +void TelemtConfigModel::setEnabled(bool enabled) { + m_protocolConfig.isEnabled = enabled; + emit dataChanged(index(0), index(0), QList{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 TelemtConfigModel::roleNames() const { + QHash 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; +} diff --git a/client/ui/models/services/telemtConfigModel.h b/client/ui/models/services/telemtConfigModel.h new file mode 100644 index 000000000..c386d210e --- /dev/null +++ b/client/ui/models/services/telemtConfigModel.h @@ -0,0 +1,130 @@ +#ifndef TELEMTCONFIGMODEL_H +#define TELEMTCONFIGMODEL_H + +#include +#include +#include + +#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 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 diff --git a/client/ui/qml/Components/SettingsContainersListView.qml b/client/ui/qml/Components/SettingsContainersListView.qml index 474803243..2cab4501c 100644 --- a/client/ui/qml/Components/SettingsContainersListView.qml +++ b/client/ui/qml/Components/SettingsContainersListView.qml @@ -48,6 +48,9 @@ ListViewType { } else if (isMtProxy) { MtProxyConfigModel.updateModel(config) PageController.goToPage(PageEnum.PageServiceMtProxySettings) + } else if (isTelemt) { + TelemtConfigModel.updateModel(config) + PageController.goToPage(PageEnum.PageServiceTelemtSettings) } else { InstallController.updateProtocols(ServersUiController.processedIndex, containerIndex) PageController.goToPage(PageEnum.PageSettingsServerProtocol) diff --git a/client/ui/qml/Pages2/PageServiceTelemtSettings.qml b/client/ui/qml/Pages2/PageServiceTelemtSettings.qml new file mode 100644 index 000000000..c4b82a2f2 --- /dev/null +++ b/client/ui/qml/Pages2/PageServiceTelemtSettings.qml @@ -0,0 +1,1447 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import SortFilterProxyModel 0.2 + +import PageEnum 1.0 +import ContainerProps 1.0 +import ProtocolEnum 1.0 +import Style 1.0 + +import "./" +import "../Controls2" +import "../Controls2/TextTypes" +import "../Config" +import "../Components" + +PageType { + id: root + + Rectangle { + anchors.fill: parent + z: -1 + color: AmneziaStyle.color.onyxBlack + } + + property int containerStatus: 1 + property bool isUpdating: false + property bool isCheckingStatus: false + property bool previousEnabled: true + property int previousContainerStatus: 1 + + property string previousPort: "" + property string previousTag: "" + property string previousPublicHost: "" + property string previousTransportMode: TelemtConfigModel.transportModeStandard() + property string previousTlsDomain: TelemtConfigModel.defaultTlsDomain() + property string previousWorkersMode: TelemtConfigModel.workersModeAuto() + property string previousWorkers: TelemtConfigModel.defaultWorkers() + property bool previousNatEnabled: false + property string previousNatInternalIp: "" + property string previousNatExternalIp: "" + + property string savedTransportMode: "" + property string savedTlsDomain: "" + property string savedPublicHost: "" + + onSavedTransportModeChanged: { + if (savedTransportMode === "faketls") { + root.syncedSecretTabIndex = 2 + } else if (savedTransportMode !== "") { + root.syncedSecretTabIndex = 0 + } + } + + property bool diagLoading: false + property int syncedSecretTabIndex: 0 + property bool pendingEnableAfterRestart: false + property bool pendingUpdateAfterEnable: false + property bool diagPortReachable: false + property bool diagTelegramReachable: false + property int diagClientsConnected: -1 + property string diagLastConfigRefresh: "" + property string diagStatsEndpoint: "" + + readonly property bool telemtNetworkBlocked: !NetworkReachabilityController.hasInternetAccess + readonly property bool navigationBlockedWhileBusy: isUpdating || diagLoading + + // Defer SSH/updateContainer so QML control handlers return before nested event loops run. + function telemtScheduleUpdate(closePage) { + var cp = closePage === undefined ? false : closePage + Qt.callLater(function () { + InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.Telemt, cp) + }) + } + + function statusText() { + if (isCheckingStatus) { + return qsTr("Checking...") + } + if (isUpdating) { + return qsTr("Updating") + } + switch (containerStatus) { + case 0: { + return qsTr("Not deployed") + } + case 1: { + return qsTr("Running") + } + case 2: { + return qsTr("Stopped") + } + case 3: { + return qsTr("Error") + } + default: { + return qsTr("Unknown") + } + } + } + + Component.onCompleted: { + root.savedTransportMode = TelemtConfigModel.getTransportMode() + root.savedTlsDomain = TelemtConfigModel.getTlsDomain() + root.savedPublicHost = TelemtConfigModel.getPublicHost() + + if (!NetworkReachabilityController.hasInternetAccess) { + isCheckingStatus = false + return + } + isCheckingStatus = true + InstallController.refreshContainerStatus(ServersUiController.processedIndex, ServersUiController.processedContainerIndex) + } + + onNavigationBlockedWhileBusyChanged: { + if (root.visible) { + PageController.disableControls(navigationBlockedWhileBusy) + } + } + + onVisibleChanged: { + if (!visible) { + PageController.disableControls(false) + diagLoading = false + } else { + PageController.disableControls(navigationBlockedWhileBusy) + } + } + + Connections { + target: NetworkReachabilityController + + function onHasInternetAccessChanged() { + if (!root.visible) { + return + } + if (NetworkReachabilityController.hasInternetAccess) { + isCheckingStatus = true + InstallController.refreshContainerStatus(ServersUiController.processedIndex, ServersUiController.processedContainerIndex) + } + } + } + + Connections { + target: InstallController + + function onUpdateContainerFinished(message, closePage) { + if (!root.visible) { + isUpdating = false + isCheckingStatus = false + return + } + isUpdating = false + containerStatus = 1 + root.savedTransportMode = TelemtConfigModel.getTransportMode() + root.savedTlsDomain = TelemtConfigModel.getTlsDomain() + root.savedPublicHost = TelemtConfigModel.getPublicHost() + PageController.showNotificationMessage(message) + if (closePage) { + PageController.closePage() + } + } + + function onInstallationErrorOccurred() { + if (!root.visible) { + isUpdating = false + isCheckingStatus = false + return + } + isUpdating = false + containerStatus = previousContainerStatus + TelemtConfigModel.setEnabled(previousEnabled) + TelemtConfigModel.setPort(previousPort) + TelemtConfigModel.setTag(previousTag) + TelemtConfigModel.setPublicHost(previousPublicHost) + TelemtConfigModel.setTransportMode(previousTransportMode) + TelemtConfigModel.setTlsDomain(previousTlsDomain) + TelemtConfigModel.setWorkersMode(previousWorkersMode) + TelemtConfigModel.setWorkers(previousWorkers) + TelemtConfigModel.setNatEnabled(previousNatEnabled) + TelemtConfigModel.setNatInternalIp(previousNatInternalIp) + TelemtConfigModel.setNatExternalIp(previousNatExternalIp) + } + + function onSetContainerEnabledFinished(enabled) { + if (!root.visible) { + isUpdating = false + return + } + if (enabled && pendingUpdateAfterEnable) { + pendingUpdateAfterEnable = false + root.telemtScheduleUpdate(false) + return + } + isUpdating = false + containerStatus = enabled ? 1 : 2 + PageController.showNotificationMessage( + enabled ? qsTr("Telemt started") : qsTr("Telemt stopped")) + } + + function onContainerStatusRefreshed(status) { + if (!root.visible) { + isCheckingStatus = false + return + } + isCheckingStatus = false + containerStatus = status + + root.savedTransportMode = TelemtConfigModel.getTransportMode() + root.savedTlsDomain = TelemtConfigModel.getTlsDomain() + root.savedPublicHost = TelemtConfigModel.getPublicHost() + if (status === 1) { + TelemtConfigModel.setEnabled(true) + InstallController.fetchContainerSecret(ServersUiController.processedIndex, ServersUiController.processedContainerIndex) + } else if (status === 2) { + TelemtConfigModel.setEnabled(false) + } + } + + function onContainerDiagnosticsRefreshed(portReachable, upstreamReachable, clientsConnected, lastConfigRefresh, statsEndpoint) { + if (!root.visible) { + return + } + diagLoading = false + diagPortReachable = portReachable + diagTelegramReachable = upstreamReachable + diagClientsConnected = clientsConnected + diagLastConfigRefresh = lastConfigRefresh + diagStatsEndpoint = statsEndpoint + } + + function onContainerSecretFetched(secret) { + if (!root.visible) { + return + } + TelemtConfigModel.validateAndSetSecret(secret) + } + } + + BackButtonType { + id: backButton + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 20 + SettingsController.safeAreaTopMargin + onFocusChanged: { + if (this.activeFocus) connectionListView.positionViewAtBeginning() + } + } + + ColumnLayout { + id: pageHeader + anchors.top: backButton.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 8 + spacing: 0 + + BaseHeaderType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 8 + headerText: qsTr("Telemt settings") + } + + LabelWithButtonType { + Layout.fillWidth: true + Layout.leftMargin: 0 + Layout.rightMargin: 16 + text: qsTr("Read more about this settings") + textColor: AmneziaStyle.color.goldenApricot + clickedFunction: function () { + Qt.openUrlExternally("https://github.com/telemt/telemt") + } + } + + TabBar { + id: mainTabBar + Layout.fillWidth: true + Layout.topMargin: 4 + + background: Rectangle { + color: AmneziaStyle.color.transparent + Rectangle { + width: parent.width + height: 1 + anchors.bottom: parent.bottom + color: AmneziaStyle.color.slateGray + } + } + + TabButtonType { + text: qsTr("Connection") + isSelected: mainTabBar.currentIndex === 0 + } + TabButtonType { + text: qsTr("Settings") + isSelected: mainTabBar.currentIndex === 1 + } + } + } + + StackLayout { + id: tabContent + anchors.top: pageHeader.bottom + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + currentIndex: mainTabBar.currentIndex + + ListViewType { + id: connectionListView + model: TelemtConfigModel + + delegate: ColumnLayout { + width: connectionListView.width + spacing: 0 + + function domainToHex(domain) { + var hex = "" + for (var i = 0; i < domain.length; i++) { + var code = domain.charCodeAt(i).toString(16) + hex += (code.length < 2 ? "0" : "") + code + } + return hex + } + + function secretForMode(mode) { + if (mode === "faketls") { + var domain = root.savedTlsDomain !== "" ? root.savedTlsDomain : TelemtConfigModel.defaultTlsDomain() + return "ee" + secret + domainToHex(domain) + } else if (mode === "padded") { + return "dd" + secret + } + // Telemt default (secure MTProto, not FakeTLS): Telegram proxy links require dd + hex secret + return "dd" + secret + } + + property int secretTabIndex: root.syncedSecretTabIndex + + function activeSecret() { + if (root.syncedSecretTabIndex === 0) { + return secretForMode("standard") + } + if (root.syncedSecretTabIndex === 1) { + return secretForMode("padded") + } + return secretForMode("faketls") + } + + function effectiveSecret() { + return activeSecret() + } + + function effectiveHost() { + return root.savedPublicHost !== "" ? root.savedPublicHost : ServersModel.getProcessedServerData("hostName") + } + + function tmeLink() { + return "https://t.me/proxy?server=" + effectiveHost() + "&port=" + port + "&secret=" + activeSecret() + } + + function tgLink() { + return "tg://proxy?server=" + effectiveHost() + "&port=" + port + "&secret=" + activeSecret() + } + + CaptionTextType { + Layout.fillWidth: true + Layout.topMargin: 24 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 8 + text: qsTr("Use Telegram connection link") + color: AmneziaStyle.color.mutedGray + } + + Rectangle { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + implicitHeight: linkRow.implicitHeight + 16 + color: AmneziaStyle.color.onyxBlack + radius: 8 + border.color: AmneziaStyle.color.slateGray + border.width: 1 + + RowLayout { + id: linkRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 12 + anchors.rightMargin: 8 + spacing: 4 + + CaptionTextType { + Layout.fillWidth: true + text: secret !== "" ? tmeLink() : qsTr("Deploy Telemt first") + color: secret !== "" ? AmneziaStyle.color.goldenApricot : AmneziaStyle.color.mutedGray + elide: Text.ElideRight + maximumLineCount: 1 + font.pixelSize: 13 + } + + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/qr-code.svg" + imageColor: AmneziaStyle.color.paleGray + visible: secret !== "" + onClicked: { + ExportController.generateQrFromString(tmeLink()) + PageController.goToShareConnectionPage( + qsTr("Telegram connection link"), + qsTr("Telemt connection link"), + "", "", "") + } + } + + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/copy.svg" + imageColor: AmneziaStyle.color.paleGray + visible: secret !== "" + onClicked: { + GC.copyToClipBoard(tmeLink()) + PageController.showNotificationMessage(qsTr("Copied")) + } + } + } + } + + Rectangle { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + implicitHeight: tgLinkRow.implicitHeight + 16 + color: AmneziaStyle.color.onyxBlack + radius: 8 + border.color: AmneziaStyle.color.slateGray + border.width: 1 + visible: secret !== "" + + RowLayout { + id: tgLinkRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 12 + anchors.rightMargin: 8 + spacing: 4 + + CaptionTextType { + Layout.fillWidth: true + text: tgLink() + color: AmneziaStyle.color.goldenApricot + elide: Text.ElideRight + maximumLineCount: 1 + font.pixelSize: 13 + } + + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/qr-code.svg" + imageColor: AmneziaStyle.color.paleGray + onClicked: { + ExportController.generateQrFromString(tgLink()) + PageController.goToShareConnectionPage( + qsTr("Telegram connection link"), + qsTr("Telemt connection link"), + "", "", "") + } + } + + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/copy.svg" + imageColor: AmneziaStyle.color.paleGray + onClicked: { + GC.copyToClipBoard(tgLink()) + PageController.showNotificationMessage(qsTr("Copied")) + } + } + } + } + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 8 + spacing: 4 + + CaptionTextType { + text: qsTr("Or enter the proxy details manually.") + color: AmneziaStyle.color.mutedGray + } + + CaptionTextType { + Layout.fillWidth: true + text: qsTr("How to do it") + color: AmneziaStyle.color.goldenApricot + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: Qt.openUrlExternally("https://core.telegram.org/proxy") + } + } + + Item { + Layout.fillWidth: true + } + } + + Rectangle { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 32 + implicitHeight: manualCol.implicitHeight + 8 + color: AmneziaStyle.color.onyxBlack + radius: 8 + border.color: AmneziaStyle.color.slateGray + border.width: 1 + + ColumnLayout { + id: manualCol + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.topMargin: 8 + spacing: 0 + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 12 + Layout.rightMargin: 8 + Layout.bottomMargin: 8 + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + CaptionTextType { + text: qsTr("Host") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 + } + CaptionTextType { + Layout.fillWidth: true + text: effectiveHost() + color: AmneziaStyle.color.paleGray + elide: Text.ElideRight + } + } + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/copy.svg" + imageColor: AmneziaStyle.color.paleGray + onClicked: { GC.copyToClipBoard(effectiveHost()) + PageController.showNotificationMessage(qsTr("Copied")) } + } + } + + DividerType { + Layout.fillWidth: true + } + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 12 + Layout.rightMargin: 8 + Layout.topMargin: 8 + Layout.bottomMargin: 8 + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + CaptionTextType { + text: qsTr("Port") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 + } + CaptionTextType { + Layout.fillWidth: true + text: port + color: AmneziaStyle.color.paleGray + } + } + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/copy.svg" + imageColor: AmneziaStyle.color.paleGray + onClicked: { GC.copyToClipBoard(port) + PageController.showNotificationMessage(qsTr("Copied")) } + } + } + + DividerType { + Layout.fillWidth: true + } + + ButtonGroup { + id: secretTabGroup + } + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 12 + Layout.rightMargin: 8 + Layout.topMargin: 4 + Layout.bottomMargin: 8 + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + CaptionTextType { + text: qsTr("Secret") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 + } + CaptionTextType { + Layout.fillWidth: true + text: activeSecret() + color: AmneziaStyle.color.paleGray + wrapMode: Text.WrapAnywhere + font.pixelSize: 13 + } + } + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/copy.svg" + imageColor: AmneziaStyle.color.paleGray + onClicked: { GC.copyToClipBoard(activeSecret()) + PageController.showNotificationMessage(qsTr("Copied")) } + } + } + } + } + + LabelWithButtonType { + id: removeButton + Layout.fillWidth: true + Layout.bottomMargin: 24 + Layout.leftMargin: 0 + Layout.rightMargin: 16 + visible: ServersModel.isProcessedServerHasWriteAccess() + text: qsTr("Delete Telemt") + textColor: AmneziaStyle.color.vibrantRed + clickedFunction: function () { + var headerText = qsTr("Remove %1 from server?").arg(ContainersModel.getProcessedContainerName()) + var descriptionText = qsTr("The proxy will be stopped and all users will lose access.") + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") + var yesButtonFunction = function () { + PageController.goToPage(PageEnum.PageDeinstalling) + InstallController.removeContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex) + } + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, function () { + }) + } + MouseArea { + anchors.fill: removeButton + cursorShape: Qt.PointingHandCursor + enabled: false + } + } + } + } + + ListViewType { + id: settingsListView + model: TelemtConfigModel + reuseItems: false + + delegate: ColumnLayout { + width: settingsListView.width + spacing: 0 + + SwitcherType { + id: enableTelemtSwitch + Layout.fillWidth: true + Layout.topMargin: 24 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + text: qsTr("Enable Telemt") + checked: isEnabled + enabled: !isCheckingStatus && containerStatus !== 0 && containerStatus !== 3 && !isUpdating + onToggled: function () { + if (checked !== isEnabled) { + previousEnabled = isEnabled + previousContainerStatus = containerStatus + isEnabled = checked + isUpdating = true + if (checked) { + root.pendingUpdateAfterEnable = true + InstallController.setContainerEnabled(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, true) + } else { + InstallController.setContainerEnabled(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, false) + } + } + } + } + + ColumnLayout { + Layout.fillWidth: true + Layout.topMargin: 16 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 * 2 + spacing: 4 + + CaptionTextType { + text: qsTr("Base secret") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 + } + + RowLayout { + Layout.fillWidth: true + spacing: 8 + + CaptionTextType { + Layout.fillWidth: true + text: secret !== "" ? secret : qsTr("Not generated") + color: secret !== "" ? AmneziaStyle.color.paleGray : AmneziaStyle.color.mutedGray + elide: Text.ElideMiddle + font.pixelSize: 14 + } + + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/refresh-cw.svg" + imageColor: AmneziaStyle.color.paleGray + visible: ServersModel.isProcessedServerHasWriteAccess() + onClicked: { + showQuestionDrawer( + qsTr("Generate new secret?"), + qsTr("All existing connection links will stop working. Users will need new links."), + qsTr("Generate"), + qsTr("Cancel"), + function () { + if (containerStatus === 1) { + isUpdating = true + TelemtConfigModel.generateSecret() + root.telemtScheduleUpdate(false) + } else { + TelemtConfigModel.generateSecret() + PageController.showNotificationMessage(qsTr("New secret saved. It will be applied when Telemt is started.")) + } + }, + function () { + } + ) + } + } + } + } + + TextFieldWithHeaderType { + id: publicHostTextField + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 4 + headerText: qsTr("Public host / IP") + textField.placeholderText: ServersModel.getProcessedServerData("hostName") + textField.text: publicHost + textField.onEditingFinished: { + textField.text = textField.text.replace(/^\s+|\s+$/g, '') + if (textField.text !== publicHost) { + publicHost = textField.text + TelemtConfigModel.setPublicHost(publicHost) + } + } + } + + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 4 + visible: publicHostTextField.textField.text === "" + text: qsTr("Leave empty to use server IP automatically") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 + wrapMode: Text.WordWrap + } + + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 12 + visible: publicHostTextField.textField.text !== "" && + publicHostTextField.textField.text !== ServersModel.getProcessedServerData("hostName") + text: qsTr("⚠ This overrides the server IP in connection links. Make sure this host/domain points to your server.") + color: AmneziaStyle.color.goldenApricot + font.pixelSize: 12 + wrapMode: Text.WordWrap + } + + TextFieldWithHeaderType { + id: portTextField + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + headerText: qsTr("Server port") + textField.placeholderText: TelemtConfigModel.defaultPort() + textField.maximumLength: 5 + textField.validator: IntValidator { + bottom: 1 + top: 65535 + } + Component.onCompleted: { + var savedPort = port + textField.text = (savedPort === TelemtConfigModel.defaultPort()) ? "" : savedPort + } + textField.onEditingFinished: { + textField.text = textField.text.replace(/^\s+|\s+$/g, '') + var portValue = textField.text === "" ? TelemtConfigModel.defaultPort() : textField.text + if (portValue !== port) { + port = portValue + TelemtConfigModel.setPort(port) + } + } + } + + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 12 + visible: transportMode === "faketls" && portTextField.textField.text !== "443" && portTextField.textField.text !== "" + text: qsTr("FakeTLS may not work on ports other than 443") + color: AmneziaStyle.color.goldenApricot + font.pixelSize: 12 + wrapMode: Text.WordWrap + } + + TextFieldWithHeaderType { + id: tagTextField + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + headerText: qsTr("Promoted channel tag (optional)") + textField.placeholderText: qsTr("leave empty if not needed") + textField.text: tag + textField.maximumLength: 64 + textField.onEditingFinished: { + textField.text = textField.text.replace(/^\s+|\s+$/g, '') + if (textField.text !== tag) { + tag = textField.text + TelemtConfigModel.setTag(tag) + } + } + } + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + spacing: 4 + + CaptionTextType { + text: qsTr("Get a tag from") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 + } + CaptionTextType { + text: "@MTProxyBot" + color: AmneziaStyle.color.goldenApricot + font.pixelSize: 12 + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: Qt.openUrlExternally("https://t.me/MTProxyBot") + } + } + } + + DropDownType { + id: transportModeDropDown + Layout.fillWidth: true + Layout.topMargin: 16 * 2 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + + drawerParent: root + drawerHeight: 0.35 + descriptionText: qsTr("Transport mode") + text: transportMode === "faketls" ? qsTr("FakeTLS") : qsTr("Standard MTProto") + + listView: Component { + ListViewType { + model: [qsTr("Standard MTProto"), qsTr("FakeTLS")] + delegate: LabelWithButtonType { + Layout.fillWidth: true + text: modelData + rightImageSource: { + var isCurrent = (index === 0 && transportMode === "standard") || + (index === 1 && transportMode === "faketls") + return isCurrent ? "qrc:/images/controls/check.svg" : "" + } + rightImageColor: AmneziaStyle.color.goldenApricot + clickedFunction: function () { + transportMode = (index === 0) ? "standard" : "faketls" + TelemtConfigModel.setTransportMode(transportMode) + transportModeDropDown.closeTriggered() + } + } + } + } + } + + TextFieldWithHeaderType { + id: tlsDomainTextField + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + visible: transportMode === "faketls" + headerText: qsTr("FakeTLS domain") + textField.placeholderText: root.previousTlsDomain + Component.onCompleted: { + var savedDomain = tlsDomain + textField.text = (savedDomain === TelemtConfigModel.defaultTlsDomain() || savedDomain === "") ? "" : savedDomain + } + textField.onEditingFinished: { + textField.text = textField.text.replace(/^\s+|\s+$/g, '') + var domainValue = textField.text === "" ? TelemtConfigModel.defaultTlsDomain() : textField.text + if (domainValue !== tlsDomain) { + tlsDomain = domainValue + TelemtConfigModel.setTlsDomain(tlsDomain) + } + } + } + + ColumnLayout { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + spacing: 4 + visible: transportMode === "faketls" + + CaptionTextType { + Layout.fillWidth: true + text: qsTr("The domain is encoded into the FakeTLS client secret (ee + base_secret + hex(domain)). It must support HTTPS / TLS 1.3.") + color: AmneziaStyle.color.mutedGray + wrapMode: Text.WordWrap + font.pixelSize: 12 + } + CaptionTextType { + Layout.fillWidth: true + text: qsTr("\u26a0 Changing the domain will invalidate all previously issued FakeTLS connection links.") + color: AmneziaStyle.color.goldenApricot + wrapMode: Text.WordWrap + font.pixelSize: 12 + } + } + + LabelWithButtonType { + id: advancedHeader + Layout.fillWidth: true + Layout.leftMargin: 0 + Layout.rightMargin: 16 + property bool expanded: false + text: qsTr("Advanced") + rightImageSource: expanded + ? "qrc:/images/controls/chevron-up.svg" + : "qrc:/images/controls/chevron-down.svg" + rightImageColor: AmneziaStyle.color.mutedGray + clickedFunction: function () { + expanded = !expanded + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + visible: advancedHeader.expanded + + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 8 + Layout.bottomMargin: 4 + text: qsTr("Additional secrets") + color: AmneziaStyle.color.mutedGray + } + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 8 + text: qsTr("Add extra secrets to allow gradual migration without disconnecting existing users.") + color: AmneziaStyle.color.charcoalGray + wrapMode: Text.WordWrap + font.pixelSize: 12 + } + + Repeater { + model: additionalSecrets + delegate: RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 4 + spacing: 8 + CaptionTextType { + Layout.fillWidth: true + text: modelData + color: AmneziaStyle.color.paleGray + elide: Text.ElideMiddle + font.pixelSize: 13 + } + ImageButtonType { + implicitWidth: 32 + implicitHeight: 32 + hoverEnabled: true + image: "qrc:/images/controls/copy.svg" + imageColor: AmneziaStyle.color.mutedGray + onClicked: { GC.copyToClipBoard(modelData) + PageController.showNotificationMessage(qsTr("Copied")) } + } + ImageButtonType { + implicitWidth: 32 + implicitHeight: 32 + hoverEnabled: true + image: "qrc:/images/controls/trash.svg" + imageColor: AmneziaStyle.color.vibrantRed + onClicked: { + TelemtConfigModel.removeAdditionalSecret(index) + root.telemtScheduleUpdate(false) + } + } + } + } + + BasicButtonType { + Layout.fillWidth: true + Layout.topMargin: 8 + + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + text: qsTr("Add additional secret") + clickedFunc: function () { + TelemtConfigModel.addAdditionalSecret() + root.telemtScheduleUpdate(false) + } + } + + DividerType { + Layout.fillWidth: true + Layout.bottomMargin: 8 + } + + LabelTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.bottomMargin: 4 + text: qsTr("Worker mode") + } + + ButtonGroup { + id: workerModeGroup + } + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 4 + spacing: 0 + visible: transportMode !== "faketls" + + HorizontalRadioButton { + Layout.fillWidth: true + text: qsTr("Auto") + ButtonGroup.group: workerModeGroup + checked: workersMode === "auto" + onClicked: { workersMode = "auto"; TelemtConfigModel.setWorkersMode("auto") } + } + HorizontalRadioButton { + Layout.fillWidth: true + text: qsTr("Manual") + ButtonGroup.group: workerModeGroup + checked: workersMode === "manual" + onClicked: { workersMode = "manual"; TelemtConfigModel.setWorkersMode("manual") } + } + } + + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 8 + visible: transportMode === "faketls" + text: qsTr("Workers are set to 0 automatically for FakeTLS mode.") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 + wrapMode: Text.WordWrap + } + + TextFieldWithHeaderType { + id: workersTextField + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + visible: workersMode === "manual" && transportMode !== "faketls" + headerText: qsTr("Workers count") + textField.placeholderText: "2" + textField.text: workers + textField.maximumLength: 3 + textField.validator: IntValidator { + bottom: 1 + top: TelemtConfigModel.maxWorkers() + } + textField.onEditingFinished: { + textField.text = textField.text.replace(/^\s+|\s+$/g, '') + if (textField.text !== workers) { + workers = textField.text + TelemtConfigModel.setWorkers(workers) + } + } + } + + DividerType { + Layout.fillWidth: true + Layout.bottomMargin: 8 + } + + SwitcherType { + Layout.fillWidth: true + Layout.rightMargin: 16 + Layout.leftMargin: 16 + Layout.bottomMargin: 4 + text: qsTr("Server is behind NAT / Docker bridge") + descriptionText: qsTr("Enable if your server is not directly accessible from the internet, e.g. Docker or private network") + checked: natEnabled + onToggled: function () { + if (checked !== natEnabled) { + natEnabled = checked + TelemtConfigModel.setNatEnabled(natEnabled) + } + } + } + + TextFieldWithHeaderType { + id: natInternalIpTextField + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + visible: natEnabled + headerText: qsTr("Internal IP") + textField.placeholderText: "172.17.0.2" + textField.text: natInternalIp + textField.onEditingFinished: { + textField.text = textField.text.replace(/^\s+|\s+$/g, '') + if (textField.text !== natInternalIp) { + natInternalIp = textField.text + TelemtConfigModel.setNatInternalIp(natInternalIp) + } + } + } + + TextFieldWithHeaderType { + id: natExternalIpTextField + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + visible: natEnabled + headerText: qsTr("External IP") + textField.placeholderText: "1.2.3.4" + textField.text: natExternalIp + textField.onEditingFinished: { + textField.text = textField.text.replace(/^\s+|\s+$/g, '') + if (textField.text !== natExternalIp) { + natExternalIp = textField.text + TelemtConfigModel.setNatExternalIp(natExternalIp) + } + } + } + } + + DividerType { + Layout.fillWidth: true + Layout.topMargin: 8 + } + + ColumnLayout { + Layout.fillWidth: true + Layout.topMargin: 16 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 8 + spacing: 8 + visible: containerStatus === 1 + + RowLayout { + Layout.fillWidth: true + + Header2Type { + Layout.fillWidth: true + headerText: qsTr("Diagnostics") + } + + ImageButtonType { + implicitWidth: 32 + implicitHeight: 32 + image: "qrc:/images/controls/refresh-cw.svg" + imageColor: diagLoading ? AmneziaStyle.color.mutedGray : AmneziaStyle.color.paleGray + hoverEnabled: !diagLoading + enabled: !diagLoading + onClicked: { + diagLoading = true + InstallController.refreshContainerDiagnostics(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, parseInt(port)) + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: 8 + Rectangle { + width: 8 + height: 8 + radius: 4 + color: diagClientsConnected >= 0 ? (diagPortReachable ? AmneziaStyle.color.paleGray : AmneziaStyle.color.vibrantRed) : AmneziaStyle.color.mutedGray + } + CaptionTextType { + Layout.fillWidth: true + text: qsTr("Public port reachable") + color: AmneziaStyle.color.paleGray + } + CaptionTextType { + text: diagClientsConnected < 0 ? qsTr("—") : (diagPortReachable ? qsTr("Yes") : qsTr("No")) + color: diagClientsConnected >= 0 ? (diagPortReachable ? AmneziaStyle.color.paleGray : AmneziaStyle.color.vibrantRed) : AmneziaStyle.color.mutedGray + } + } + + RowLayout { + Layout.fillWidth: true + spacing: 8 + Rectangle { + width: 8 + height: 8 + radius: 4 + color: diagClientsConnected >= 0 ? (diagTelegramReachable ? AmneziaStyle.color.paleGray : AmneziaStyle.color.vibrantRed) : AmneziaStyle.color.mutedGray + } + CaptionTextType { + Layout.fillWidth: true + text: qsTr("Telegram upstream reachable") + color: AmneziaStyle.color.paleGray + } + CaptionTextType { + text: diagClientsConnected < 0 ? qsTr("—") : (diagTelegramReachable ? qsTr("Yes") : qsTr("No")) + color: diagClientsConnected >= 0 ? (diagTelegramReachable ? AmneziaStyle.color.paleGray : AmneziaStyle.color.vibrantRed) : AmneziaStyle.color.mutedGray + } + } + + RowLayout { + Layout.fillWidth: true + spacing: 8 + Rectangle { + width: 8 + height: 8 + radius: 4 + color: diagClientsConnected >= 0 ? AmneziaStyle.color.goldenApricot : AmneziaStyle.color.mutedGray + } + CaptionTextType { + Layout.fillWidth: true + text: qsTr("Clients connected") + color: AmneziaStyle.color.paleGray + } + CaptionTextType { + text: diagClientsConnected < 0 ? qsTr("—") : diagClientsConnected.toString() + color: AmneziaStyle.color.paleGray + } + } + + RowLayout { + Layout.fillWidth: true + spacing: 8 + Rectangle { + width: 8 + height: 8 + radius: 4 + color: AmneziaStyle.color.mutedGray + } + CaptionTextType { + Layout.fillWidth: true + text: qsTr("Last config refresh") + color: AmneziaStyle.color.paleGray + } + CaptionTextType { + text: diagLastConfigRefresh !== "" ? diagLastConfigRefresh : qsTr("—") + color: AmneziaStyle.color.mutedGray + } + } + + LabelWithButtonType { + Layout.fillWidth: true + Layout.leftMargin: -16 + visible: diagStatsEndpoint !== "" + text: qsTr("Stats endpoint") + descriptionText: diagStatsEndpoint + descriptionOnTop: true + rightImageSource: "qrc:/images/controls/copy.svg" + rightImageColor: AmneziaStyle.color.paleGray + clickedFunction: function () { + GC.copyToClipBoard(diagStatsEndpoint) + PageController.showNotificationMessage(qsTr("Copied")) + } + } + + CaptionTextType { + Layout.fillWidth: true + text: diagLoading ? qsTr("Refreshing…") : qsTr("Tap ↻ to refresh diagnostics") + color: AmneziaStyle.color.mutedGray + visible: diagClientsConnected < 0 + } + } + + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 16 * 2 + Layout.bottomMargin: 24 + text: qsTr("If you change the settings, the proxy connection link will change. The old link will stop working.") + color: AmneziaStyle.color.mutedGray + wrapMode: Text.WordWrap + font.pixelSize: 12 + } + + BasicButtonType { + Layout.fillWidth: true + Layout.topMargin: 16 + Layout.bottomMargin: 32 + Layout.rightMargin: 16 + Layout.leftMargin: 16 + visible: ServersModel.isProcessedServerHasWriteAccess() + text: qsTr("Save") + clickedFunc: function () { + var portValue = portTextField.textField.text === "" + ? TelemtConfigModel.defaultPort() + : portTextField.textField.text + if (!portTextField.textField.acceptableInput && portTextField.textField.text !== "") { + portTextField.errorText = qsTr("The port must be in the range of 1 to 65535") + return + } + TelemtConfigModel.setPort(portValue) + TelemtConfigModel.setTag(tagTextField.textField.text) + TelemtConfigModel.setPublicHost(publicHostTextField.textField.text) + TelemtConfigModel.setTransportMode(transportMode) + var domainValue = tlsDomainTextField.textField.text === "" + ? TelemtConfigModel.defaultTlsDomain() + : tlsDomainTextField.textField.text + TelemtConfigModel.setTlsDomain(domainValue) + + if (transportMode === "faketls") { + workers = "0" + TelemtConfigModel.setWorkers("0") + } else { + TelemtConfigModel.setWorkersMode(workersMode) + TelemtConfigModel.setWorkers(workers) + } + TelemtConfigModel.setNatEnabled(natEnabled) + TelemtConfigModel.setNatInternalIp(natInternalIpTextField.textField.text) + TelemtConfigModel.setNatExternalIp(natExternalIpTextField.textField.text) + + previousPort = port + previousTag = tag + previousPublicHost = publicHost + previousTransportMode = transportMode + previousTlsDomain = tlsDomain + previousWorkersMode = workersMode + previousWorkers = workers + previousNatEnabled = natEnabled + previousNatInternalIp = natInternalIp + previousNatExternalIp = natExternalIp + isUpdating = true + root.telemtScheduleUpdate(false) + } + } + } + } + } + + Rectangle { + anchors.fill: parent + visible: isCheckingStatus || isUpdating || root.telemtNetworkBlocked + color: AmneziaStyle.color.midnightBlack + opacity: 0.6 + z: 1 + MouseArea { + anchors.fill: parent + } + BusyIndicator { + anchors.centerIn: parent + visible: isCheckingStatus || isUpdating + running: isCheckingStatus || isUpdating + width: 48 + height: 48 + } + CaptionTextType { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 24 + anchors.rightMargin: 24 + visible: root.telemtNetworkBlocked && !isCheckingStatus && !isUpdating + horizontalAlignment: Text.AlignHCenter + text: qsTr("No internet connection. Connect to the internet to change Telemt settings.") + color: AmneziaStyle.color.paleGray + wrapMode: Text.WordWrap + font.pixelSize: 14 + } + } +} diff --git a/client/ui/qml/qml.qrc b/client/ui/qml/qml.qrc index dd4a5041b..64e60c201 100644 --- a/client/ui/qml/qml.qrc +++ b/client/ui/qml/qml.qrc @@ -79,6 +79,7 @@ Pages2/PageProtocolXraySettings.qml Pages2/PageServiceDnsSettings.qml Pages2/PageServiceMtProxySettings.qml + Pages2/PageServiceTelemtSettings.qml Pages2/PageServiceSftpSettings.qml Pages2/PageServiceSocksProxySettings.qml Pages2/PageServiceTorWebsiteSettings.qml