Compare commits

...

6 Commits

Author SHA1 Message Date
yp
ee87aff848 address review findings 2026-08-24 15:55:14 +03:00
yp
d0f3856c8f remove debug logs 2026-08-24 15:13:09 +03:00
yp
1142d7a898 fix workers 2026-08-24 15:01:55 +03:00
yp
9fd142fca4 settings UI and setup wizard 2026-08-24 13:01:55 +03:00
yp
15b384aac8 core model, enums and install pipeline 2026-08-23 16:11:23 +03:00
yp
04d6ff1631 server-side container stack 2026-08-23 13:55:51 +03:00
44 changed files with 2459 additions and 31 deletions

View File

@@ -37,6 +37,7 @@ set(HEADERS ${HEADERS}
${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/installers/tProxyInstaller.h
${CLIENT_ROOT_DIR}/core/controllers/appSplitTunnelingController.h
${CLIENT_ROOT_DIR}/core/controllers/ipSplitTunnelingController.h
${CLIENT_ROOT_DIR}/core/controllers/allowedDnsController.h
@@ -120,6 +121,7 @@ set(SOURCES ${SOURCES}
${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/installers/tProxyInstaller.cpp
${CLIENT_ROOT_DIR}/core/controllers/appSplitTunnelingController.cpp
${CLIENT_ROOT_DIR}/core/controllers/ipSplitTunnelingController.cpp
${CLIENT_ROOT_DIR}/core/controllers/allowedDnsController.cpp

View File

@@ -113,6 +113,9 @@ void CoreController::initModels()
m_telemtConfigModel = new TelemtConfigModel(this);
setQmlContextProperty("TelemtConfigModel", m_telemtConfigModel);
m_tProxyConfigModel = new TProxyConfigModel(this);
setQmlContextProperty("TProxyConfigModel", m_tProxyConfigModel);
m_clientManagementModel = new ClientManagementModel(this);
setQmlContextProperty("ClientManagementModel", m_clientManagementModel);
@@ -183,6 +186,7 @@ void CoreController::initControllers()
m_ikev2ConfigModel,
#endif
m_sftpConfigModel, m_socks5ConfigModel, m_mtProxyConfigModel, m_telemtConfigModel,
m_tProxyConfigModel,
m_connectionController, this);
setQmlContextProperty("InstallController", m_installUiController);

View File

@@ -73,6 +73,7 @@
#include "ui/models/services/socks5ProxyConfigModel.h"
#include "ui/models/services/mtProxyConfigModel.h"
#include "ui/models/services/telemtConfigModel.h"
#include "ui/models/services/tProxyConfigModel.h"
#include "ui/models/ipSplitTunnelingModel.h"
#include "ui/models/newsModel.h"
@@ -226,6 +227,7 @@ private:
Socks5ProxyConfigModel* m_socks5ConfigModel;
MtProxyConfigModel* m_mtProxyConfigModel;
TelemtConfigModel* m_telemtConfigModel;
TProxyConfigModel* m_tProxyConfigModel;
CoreSignalHandlers* m_signalHandlers;
};

View File

@@ -23,6 +23,7 @@
#include "core/installers/socks5Installer.h"
#include "core/installers/mtProxyInstaller.h"
#include "core/installers/telemtInstaller.h"
#include "core/installers/tProxyInstaller.h"
#include "core/installers/torInstaller.h"
#include "core/installers/wireguardInstaller.h"
#include "core/installers/xrayInstaller.h"
@@ -103,7 +104,6 @@ InstallController::~InstallController()
ErrorCode InstallController::setupContainer(const ServerCredentials &credentials, DockerContainer container, ContainerConfig &config,
bool isUpdate)
{
qDebug().noquote() << "InstallController::setupContainer" << ContainerUtils::containerToString(container);
SshSession sshSession;
ErrorCode e = ErrorCode::NoError;
@@ -118,7 +118,6 @@ ErrorCode InstallController::setupContainer(const ServerCredentials &credentials
e = installDockerWorker(credentials, container, sshSession);
if (e)
return e;
qDebug().noquote() << "InstallController::setupContainer installDockerWorker finished";
if (!isUpdate) {
e = isServerPortBusy(credentials, container, config, sshSession);
@@ -129,29 +128,24 @@ ErrorCode InstallController::setupContainer(const ServerCredentials &credentials
e = prepareHostWorker(credentials, container, sshSession);
if (e)
return e;
qDebug().noquote() << "InstallController::setupContainer prepareHostWorker finished";
const amnezia::ScriptVars removeContainerVars =
amnezia::genBaseVars(credentials, container, QString(), QString());
const bool removeDataVolume = !isUpdate && (container == DockerContainer::MtProxy || container == DockerContainer::Telemt);
const bool removeDataVolume = !isUpdate && (container == DockerContainer::MtProxy
|| container == DockerContainer::Telemt || container == DockerContainer::TProxy);
sshSession.runScript(credentials, buildRemoveContainerScript(removeContainerVars, removeDataVolume));
qDebug().noquote() << "InstallController::setupContainer removeContainer finished";
qDebug().noquote() << "buildContainerWorker start";
e = buildContainerWorker(credentials, container, config, sshSession);
if (e)
return e;
qDebug().noquote() << "InstallController::setupContainer buildContainerWorker finished";
e = runContainerWorker(credentials, container, config, sshSession);
if (e)
return e;
qDebug().noquote() << "InstallController::setupContainer runContainerWorker finished";
e = configureContainerWorker(credentials, container, config, sshSession);
if (e)
return e;
qDebug().noquote() << "InstallController::setupContainer configureContainerWorker finished";
if (container == DockerContainer::Xray || container == DockerContainer::SSXray) {
DnsSettings dnsSettings = { m_appSettingsRepository->primaryDns(), m_appSettingsRepository->secondaryDns() };
@@ -159,11 +153,9 @@ ErrorCode InstallController::setupContainer(const ServerCredentials &credentials
e = xrayConfigurator.writeServerConfigForSetup(credentials, container, config, dnsSettings);
if (e)
return e;
qDebug().noquote() << "InstallController::setupContainer xray writeServerConfigForSetup finished";
}
setupServerFirewall(credentials, sshSession);
qDebug().noquote() << "InstallController::setupContainer setupServerFirewall finished";
return startupContainerWorker(credentials, container, config, sshSession);
}
@@ -184,6 +176,10 @@ ErrorCode InstallController::updateServerConfig(const QString &serverId, DockerC
ServerCredentials credentials = adminConfig->credentials();
SshSession sshSession;
TelemtInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
} else if (container == DockerContainer::TProxy) {
ServerCredentials credentials = adminConfig->credentials();
SshSession sshSession;
TProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
}
adminConfig->updateContainerConfig(container, newConfig);
m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin);
@@ -220,7 +216,8 @@ ErrorCode InstallController::updateServerConfig(const QString &serverId, DockerC
}
if (errorCode == ErrorCode::NoError
&& (container == DockerContainer::MtProxy || container == DockerContainer::Telemt)) {
&& (container == DockerContainer::MtProxy || container == DockerContainer::Telemt
|| container == DockerContainer::TProxy)) {
const QString containerName = ContainerUtils::containerToString(container);
errorCode = sshSession.runScript(credentials, "sudo docker restart " + containerName);
}
@@ -231,6 +228,8 @@ ErrorCode InstallController::updateServerConfig(const QString &serverId, DockerC
MtProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
} else if (container == DockerContainer::Telemt) {
TelemtInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
} else if (container == DockerContainer::TProxy) {
TProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
}
if (reinstallRequired) {
clearCachedProfile(serverId, container);
@@ -577,6 +576,8 @@ ErrorCode InstallController::configureContainerWorker(const ServerCredentials &c
MtProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, config);
} else if (container == DockerContainer::Telemt) {
TelemtInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, config);
} else if (container == DockerContainer::TProxy) {
TProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, config);
}
return ErrorCode::NoError;
@@ -628,22 +629,42 @@ ErrorCode InstallController::isServerPortBusy(const ServerCredentials &credentia
if (port.isEmpty()) {
port = QString::number(ProtocolUtils::defaultPort(protocol));
}
if (container == DockerContainer::TProxy) {
if (const auto *tProxyConfig = config.getTProxyProtocolConfig()) {
const QString httpPort =
tProxyConfig->httpPort.isEmpty() ? QString(protocols::tProxy::defaultHttpPort) : tProxyConfig->httpPort;
if (!fixedPorts.contains(httpPort) && httpPort != port) {
fixedPorts.append(httpPort);
}
}
}
QString transportProto = config.protocolConfig.transportProto();
if (transportProto.isEmpty()) {
transportProto = ProtocolUtils::transportProtoToString(ProtocolUtils::defaultTransportProto(protocol), protocol);
}
// TODO reimplement with netstat
QString script = QString("which lsof > /dev/null 2>&1 || true && sudo lsof -i -P -n 2>/dev/null | grep -E ':%1 ").arg(port);
for (auto &port : fixedPorts) {
script = script.append("|:%1").arg(port);
// Match exact host ports in lsof output (e.g. *:80) but not prefixes like *:8025 or *:8080.
QStringList portsToCheck;
portsToCheck << port;
for (const QString &fixedPort : fixedPorts) {
if (!portsToCheck.contains(fixedPort)) {
portsToCheck << fixedPort;
}
}
QStringList portRegexParts;
for (const QString &p : portsToCheck) {
portRegexParts << QString(":%1([^0-9]|$)").arg(p);
}
const QString portRegex = portRegexParts.join(QLatin1Char('|'));
QString script = QString("which lsof > /dev/null 2>&1 || true && sudo lsof -i -P -n 2>/dev/null | grep -E '%1'")
.arg(portRegex);
if (transportProto == "tcpandudp") {
QString tcpProtoScript = script;
QString udpProtoScript = script;
tcpProtoScript.append("' | grep -i tcp");
udpProtoScript.append("' | grep -i udp");
tcpProtoScript.append(" | grep -i tcp");
udpProtoScript.append(" | grep -i udp");
tcpProtoScript.append(" | grep LISTEN");
ErrorCode errorCode = sshSession.runScript(
@@ -668,7 +689,7 @@ ErrorCode InstallController::isServerPortBusy(const ServerCredentials &credentia
return ErrorCode::NoError;
}
script = script.append("' | grep -i %1").arg(transportProto);
script = script.append(" | grep -i %1").arg(transportProto);
if (transportProto == "tcp") {
script = script.append(" | grep LISTEN");
@@ -761,6 +782,24 @@ bool InstallController::isReinstallContainerRequired(DockerContainer container,
}
}
if (container == DockerContainer::TProxy) {
const auto *oldP = oldConfig.getTProxyProtocolConfig();
const auto *newP = newConfig.getTProxyProtocolConfig();
if (oldP && newP) {
const QString oldHttps =
oldP->port.isEmpty() ? QString(protocols::tProxy::defaultPort) : oldP->port;
const QString newHttps =
newP->port.isEmpty() ? QString(protocols::tProxy::defaultPort) : newP->port;
const QString oldHttp =
oldP->httpPort.isEmpty() ? QString(protocols::tProxy::defaultHttpPort) : oldP->httpPort;
const QString newHttp =
newP->httpPort.isEmpty() ? QString(protocols::tProxy::defaultHttpPort) : newP->httpPort;
if (oldHttps != newHttps || oldHttp != newHttp) {
return true;
}
}
}
if (container == DockerContainer::Socks5Proxy) {
return true;
}
@@ -797,7 +836,8 @@ ErrorCode InstallController::installDockerWorker(const ServerCredentials &creden
qDebug().noquote() << "InstallController::installDockerWorker" << stdOut;
if (container == DockerContainer::MtProxy || container == DockerContainer::Telemt) {
if (container == DockerContainer::MtProxy || container == DockerContainer::Telemt
|| container == DockerContainer::TProxy) {
QString conntrackOut;
auto cbConntrack = [&](const QString &data, libssh::Client &) {
conntrackOut += data + "\n";
@@ -1010,7 +1050,8 @@ ErrorCode InstallController::removeContainer(const QString &serverId, DockerCont
SshSession sshSession;
const amnezia::ScriptVars removeContainerVars =
amnezia::genBaseVars(credentials, container, QString(), QString());
const bool removeDataVolume = (container == DockerContainer::MtProxy || container == DockerContainer::Telemt);
const bool removeDataVolume = (container == DockerContainer::MtProxy || container == DockerContainer::Telemt
|| container == DockerContainer::TProxy);
ErrorCode errorCode =
sshSession.runScript(credentials, buildRemoveContainerScript(removeContainerVars, removeDataVolume));
@@ -1049,6 +1090,7 @@ QScopedPointer<InstallerBase> InstallController::createInstaller(DockerContainer
case DockerContainer::Socks5Proxy: return QScopedPointer<InstallerBase>(new Socks5Installer(this));
case DockerContainer::MtProxy: return QScopedPointer<InstallerBase>(new MtProxyInstaller(this));
case DockerContainer::Telemt: return QScopedPointer<InstallerBase>(new TelemtInstaller(this));
case DockerContainer::TProxy: return QScopedPointer<InstallerBase>(new TProxyInstaller(this));
default: return QScopedPointer<InstallerBase>(new InstallerBase(this));
}
}
@@ -1063,9 +1105,35 @@ ErrorCode InstallController::installContainer(const ServerCredentials &credentia
TransportProto transportProto, ContainerConfig &config)
{
config = generateConfig(container, port, transportProto);
if (container == DockerContainer::TProxy) {
auto *tProxyConfig = config.getTProxyProtocolConfig();
if (tProxyConfig) {
if (!m_tproxyInstallHostname.isEmpty()) {
tProxyConfig->hostname = m_tproxyInstallHostname;
}
if (!m_tproxyInstallEmail.isEmpty()) {
tProxyConfig->acmeEmail = m_tproxyInstallEmail;
}
}
m_tproxyInstallHostname.clear();
m_tproxyInstallEmail.clear();
// TProxy needs a hostname and ACME email before the first deploy (configure_container.sh
// exits 1 without them). Fail fast with a clear error instead of a confusing server-side
// failure if the install path is reached without them being supplied via setTProxyInstallHints().
if (!tProxyConfig || tProxyConfig->hostname.isEmpty() || tProxyConfig->acmeEmail.isEmpty()) {
return ErrorCode::InternalError;
}
}
return setupContainer(credentials, container, config, false);
}
void InstallController::setTProxyInstallHints(const QString &hostname, const QString &email)
{
m_tproxyInstallHostname = hostname;
m_tproxyInstallEmail = email;
}
bool InstallController::isUpdateDockerContainerRequired(DockerContainer container, const ContainerConfig &oldConfig, const ContainerConfig &newConfig)
{
@@ -1101,6 +1169,13 @@ bool InstallController::isUpdateDockerContainerRequired(DockerContainer containe
return true;
}
return !oldT->equalsDockerDeploymentSettings(*newT);
} else if (container == DockerContainer::TProxy) {
const auto *oldP = oldConfig.getTProxyProtocolConfig();
const auto *newP = newConfig.getTProxyProtocolConfig();
if (!oldP || !newP) {
return true;
}
return !oldP->equalsDockerDeploymentSettings(*newP);
}
return true;
@@ -1452,6 +1527,34 @@ void InstallController::updateContainerConfigAfterInstallation(DockerContainer c
telemtConfig->tmeLink = mTmeLink.captured(1);
}
}
} else if (container == DockerContainer::TProxy) {
if (auto *tProxyConfig = containerConfig.getTProxyProtocolConfig()) {
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://webproxy\?[^\s]+))"));
static const QRegularExpression reTmeLink(
QStringLiteral(R"(\[\*\]\s+t\.me\s+link:\s+(https://t\.me/webproxy\?[^\s]+))"));
static const QRegularExpression reHost(QStringLiteral(R"(\[\*\]\s+Hostname:\s+(\S+))"));
const QRegularExpressionMatch mSecret = reSecret.match(stdOut);
const QRegularExpressionMatch mTgLink = reTgLink.match(stdOut);
const QRegularExpressionMatch mTmeLink = reTmeLink.match(stdOut);
const QRegularExpressionMatch mHost = reHost.match(stdOut);
if (mSecret.hasMatch()) {
tProxyConfig->secret = mSecret.captured(1);
}
if (mTgLink.hasMatch()) {
tProxyConfig->tgLink = mTgLink.captured(1);
}
if (mTmeLink.hasMatch()) {
tProxyConfig->tmeLink = mTmeLink.captured(1);
}
if (mHost.hasMatch() && tProxyConfig->hostname.isEmpty()) {
tProxyConfig->hostname = mHost.captured(1);
}
}
}
}
@@ -1499,6 +1602,11 @@ ErrorCode InstallController::getAlreadyInstalledContainers(const ServerCredentia
auto installer = createInstaller(container);
ContainerConfig config = installer->createBaseConfig(container, port, transportProto);
if (container == DockerContainer::TProxy) {
if (auto *tProxyConfig = config.getTProxyProtocolConfig()) {
TProxyInstaller::applyDockerPublishedPorts(containerInfo, *tProxyConfig);
}
}
ErrorCode extractError = installer->extractConfigFromContainer(container, credentials, &sshSession, config);
if (extractError != ErrorCode::NoError && extractError != ErrorCode::ServerContainerMissingError) {
@@ -1524,6 +1632,11 @@ ErrorCode InstallController::getAlreadyInstalledContainers(const ServerCredentia
auto installer = createInstaller(container);
ContainerConfig config = installer->createBaseConfig(container, port, transportProto);
if (container == DockerContainer::TProxy) {
if (auto *tProxyConfig = config.getTProxyProtocolConfig()) {
TProxyInstaller::applyDockerPublishedPorts(containerInfo, *tProxyConfig);
}
}
ErrorCode extractError = installer->extractConfigFromContainer(container, credentials, &sshSession, config);
if (extractError != ErrorCode::NoError && extractError != ErrorCode::ServerContainerMissingError) {
@@ -1539,7 +1652,8 @@ ErrorCode InstallController::getAlreadyInstalledContainers(const ServerCredentia
ErrorCode InstallController::setDockerContainerEnabledState(const QString &serverId, DockerContainer container, bool enabled)
{
if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt) {
if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt
&& container != DockerContainer::TProxy) {
return ErrorCode::InternalError;
}
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
@@ -1566,6 +1680,9 @@ ErrorCode InstallController::setDockerContainerEnabledState(const QString &serve
} else if (auto *telemtConfig = currentConfig.getTelemtProtocolConfig()) {
telemtConfig->isEnabled = enabled;
persist = true;
} else if (auto *tProxyConfig = currentConfig.getTProxyProtocolConfig()) {
tProxyConfig->isEnabled = enabled;
persist = true;
}
if (persist) {
adminConfig->updateContainerConfig(container, currentConfig);
@@ -1631,7 +1748,8 @@ ErrorCode InstallController::queryMtProxyDiagnostics(const QString &serverId, Do
QString InstallController::fetchDockerContainerSecret(const QString &serverId, DockerContainer container)
{
if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt) {
if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt
&& container != DockerContainer::TProxy) {
return {};
}
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);

View File

@@ -55,6 +55,8 @@ public:
QString fetchDockerContainerSecret(const QString &serverId, DockerContainer container);
void setTProxyInstallHints(const QString &hostname, const QString &email);
ContainerConfig generateConfig(DockerContainer container, int port, TransportProto transportProto);
ErrorCode getAlreadyInstalledContainers(const ServerCredentials &credentials, QMap<DockerContainer, ContainerConfig> &installedContainers, SshSession &sshSession);
@@ -126,6 +128,8 @@ private:
SecureServersRepository* m_serversRepository;
SecureAppSettingsRepository* m_appSettingsRepository;
bool m_cancelInstallation = false;
QString m_tproxyInstallHostname;
QString m_tproxyInstallEmail;
#ifndef Q_OS_IOS
QList<QSharedPointer<QProcess>> m_sftpMountProcesses;

View File

@@ -16,6 +16,7 @@
#include "core/models/protocols/socks5ProxyProtocolConfig.h"
#include "core/models/protocols/mtProxyProtocolConfig.h"
#include "core/models/protocols/telemtProtocolConfig.h"
#include "core/models/protocols/tProxyProtocolConfig.h"
#include "core/models/protocols/ikev2ProtocolConfig.h"
#include "core/models/protocols/torProtocolConfig.h"
@@ -115,6 +116,15 @@ ContainerConfig InstallerBase::createBaseConfig(DockerContainer container, int p
config.protocolConfig = telemtConfig;
break;
}
case Proto::TProxy: {
TProxyProtocolConfig tProxyConfig;
tProxyConfig.port = portStr.isEmpty() ? QString(protocols::tProxy::defaultPort) : portStr;
tProxyConfig.httpPort = QString(protocols::tProxy::defaultHttpPort);
tProxyConfig.carrierMode = QString(protocols::tProxy::carrierModeHttps);
tProxyConfig.workers = QString(protocols::tProxy::defaultWorkers);
config.protocolConfig = tProxyConfig;
break;
}
case Proto::Ikev2: {
Ikev2ProtocolConfig ikev2Config;
config.protocolConfig = ikev2Config;

View File

@@ -162,6 +162,8 @@ ErrorCode MtProxyInstaller::queryDiagnostics(SshSession &sshSession, const Serve
if (container == DockerContainer::MtProxy || container == DockerContainer::Telemt) {
const QString containerName = ContainerUtils::containerToString(container);
const bool isTelemt = container == DockerContainer::Telemt;
const QString confFile =
isTelemt ? QStringLiteral("/data/config.toml") : QStringLiteral("/data/proxy-multi.conf");
const QString sportFilter = QString::number(listenPort);
const QString peersCmd = QStringLiteral("sudo conntrack -L -p tcp --dport ") + sportFilter
@@ -172,8 +174,6 @@ ErrorCode MtProxyInstaller::queryDiagnostics(SshSession &sshSession, const Serve
"01])\\.|::1$|fe80:|f[cd][0-9a-f][0-9a-f]:)'");
const QString clientsCmd =
QStringLiteral("CLIENTS=$(") + peersCmd + publicFilter + QStringLiteral(" | sort -u | grep -c .); ");
const QString confFile =
isTelemt ? QStringLiteral("/data/config.toml") : QStringLiteral("/data/proxy-multi.conf");
const QString statsUrl = QString();
const QString script = QStringLiteral("CN=") + containerName + QStringLiteral("; ")

View File

@@ -0,0 +1,141 @@
#include "tProxyInstaller.h"
#include "core/utils/containerEnum.h"
#include "core/utils/constants/protocolConstants.h"
#include "core/utils/selfhosted/sshSession.h"
#include "core/models/containerConfig.h"
#include "core/models/protocols/tProxyProtocolConfig.h"
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>
#include <QRegularExpression>
#include <QtGlobal>
using namespace amnezia;
namespace {
constexpr QLatin1String kTProxyClientJsonPath("/data/amnezia-tproxy-client.json");
constexpr QLatin1String kTProxyClientJsonUploadPath("data/amnezia-tproxy-client.json");
constexpr QLatin1String kTProxySecretPath("/data/secret");
constexpr QLatin1String kTProxyMetaPath("/data/tproxy-meta");
void rebuildTProxyLinksIfNeeded(TProxyProtocolConfig *tc)
{
if (!tc || tc->hostname.isEmpty() || tc->secret.isEmpty()) {
return;
}
const QString host = tc->hostname;
const QString sec = tc->secret;
// WEB proxy type fixes HTTPS/443; the canonical webproxy link carries no port.
if (tc->tgLink.isEmpty()) {
tc->tgLink = QStringLiteral("tg://webproxy?server=%1&secret=%2").arg(host, sec);
}
if (tc->tmeLink.isEmpty()) {
tc->tmeLink = QStringLiteral("https://t.me/webproxy?server=%1&secret=%2").arg(host, sec);
}
}
}
void TProxyInstaller::applyDockerPublishedPorts(const QString &dockerPsPortsLine, TProxyProtocolConfig &config)
{
static const QRegularExpression httpRe(QStringLiteral(":(\\d+)->80/(tcp|udp)"));
static const QRegularExpression httpsRe(QStringLiteral(":(\\d+)->443/(tcp|udp)"));
const QRegularExpressionMatch httpMatch = httpRe.match(dockerPsPortsLine);
const QRegularExpressionMatch httpsMatch = httpsRe.match(dockerPsPortsLine);
if (httpsMatch.hasMatch()) {
config.port = httpsMatch.captured(1);
}
if (httpMatch.hasMatch()) {
config.httpPort = httpMatch.captured(1);
}
}
TProxyInstaller::TProxyInstaller(QObject *parent) : InstallerBase(parent) {}
ErrorCode TProxyInstaller::extractConfigFromContainer(DockerContainer container, const ServerCredentials &credentials,
SshSession *sshSession, ContainerConfig &config)
{
if (container != DockerContainer::TProxy || !sshSession) {
return ErrorCode::NoError;
}
TProxyProtocolConfig *tc = config.getTProxyProtocolConfig();
if (!tc) {
return ErrorCode::NoError;
}
ErrorCode jsonErr = ErrorCode::NoError;
const QByteArray jsonRaw =
sshSession->getTextFileFromContainer(container, credentials, QString(kTProxyClientJsonPath), 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 = TProxyProtocolConfig::fromJson(merged);
}
}
static const QRegularExpression hex32(QStringLiteral("^[0-9a-fA-F]{32}$"));
ErrorCode secretErr = ErrorCode::NoError;
const QByteArray secretRaw =
sshSession->getTextFileFromContainer(container, credentials, QString(kTProxySecretPath), secretErr);
const QString sec = QString::fromUtf8(secretRaw).trimmed();
if (sec.length() == 32 && hex32.match(sec).hasMatch()) {
tc->secret = sec;
}
ErrorCode metaErr = ErrorCode::NoError;
const QByteArray metaRaw =
sshSession->getTextFileFromContainer(container, credentials, QString(kTProxyMetaPath), metaErr);
if (metaErr == ErrorCode::NoError && !metaRaw.trimmed().isEmpty()) {
const QList<QByteArray> lines = metaRaw.split('\n');
for (const QByteArray &rawLine : lines) {
const QString line = QString::fromUtf8(rawLine).trimmed();
const int eq = line.indexOf('=');
if (eq < 0) {
continue;
}
const QString key = line.left(eq);
const QString val = line.mid(eq + 1).trimmed();
if (key == QLatin1String("hostname") && tc->hostname.isEmpty()) {
tc->hostname = val;
} else if (key == QLatin1String("email") && tc->acmeEmail.isEmpty()) {
tc->acmeEmail = val;
} else if (key == QLatin1String("carrier") && tc->carrierMode.isEmpty()) {
tc->carrierMode = val;
} else if (key == QLatin1String("workers") && tc->workers.isEmpty()) {
tc->workers = val;
} else if (key == QLatin1String("http_port") && !val.isEmpty()) {
tc->httpPort = val;
} else if (key == QLatin1String("https_port") && !val.isEmpty()) {
tc->port = val;
}
}
}
rebuildTProxyLinksIfNeeded(tc);
return ErrorCode::NoError;
}
void TProxyInstaller::uploadClientSettingsSnapshot(SshSession &sshSession, const ServerCredentials &credentials,
DockerContainer container, const ContainerConfig &config)
{
const TProxyProtocolConfig *tc = config.getTProxyProtocolConfig();
if (!tc) {
return;
}
const QByteArray payload = QJsonDocument(tc->toJson()).toJson(QJsonDocument::Compact);
const ErrorCode err = sshSession.uploadTextFileToContainer(container, credentials, QString::fromUtf8(payload),
QString(kTProxyClientJsonUploadPath));
if (err != ErrorCode::NoError) {
qWarning() << "TProxyInstaller::uploadClientSettingsSnapshot failed" << err;
}
}

View File

@@ -0,0 +1,25 @@
#ifndef TPROXYINSTALLER_H
#define TPROXYINSTALLER_H
#include "installerBase.h"
#include "core/models/protocols/tProxyProtocolConfig.h"
class TProxyInstaller : public InstallerBase {
Q_OBJECT
public:
explicit TProxyInstaller(QObject *parent = nullptr);
amnezia::ErrorCode
extractConfigFromContainer(amnezia::DockerContainer container, const amnezia::ServerCredentials &credentials,
SshSession *sshSession, amnezia::ContainerConfig &config) override;
static void applyDockerPublishedPorts(const QString &dockerPsPortsLine,
amnezia::TProxyProtocolConfig &config);
static void uploadClientSettingsSnapshot(SshSession &sshSession, const amnezia::ServerCredentials &credentials,
amnezia::DockerContainer container,
const amnezia::ContainerConfig &config);
};
#endif // TPROXYINSTALLER_H

View File

@@ -133,6 +133,16 @@ const TelemtProtocolConfig* ContainerConfig::getTelemtProtocolConfig() const
return protocolConfig.as<TelemtProtocolConfig>();
}
TProxyProtocolConfig* ContainerConfig::getTProxyProtocolConfig()
{
return protocolConfig.as<TProxyProtocolConfig>();
}
const TProxyProtocolConfig* ContainerConfig::getTProxyProtocolConfig() const
{
return protocolConfig.as<TProxyProtocolConfig>();
}
Ikev2ProtocolConfig* ContainerConfig::getIkev2ProtocolConfig()
{
return protocolConfig.as<Ikev2ProtocolConfig>();

View File

@@ -63,6 +63,9 @@ struct ContainerConfig {
TelemtProtocolConfig* getTelemtProtocolConfig();
const TelemtProtocolConfig* getTelemtProtocolConfig() const;
TProxyProtocolConfig* getTProxyProtocolConfig();
const TProxyProtocolConfig* getTProxyProtocolConfig() const;
Ikev2ProtocolConfig* getIkev2ProtocolConfig();
const Ikev2ProtocolConfig* getIkev2ProtocolConfig() const;

View File

@@ -11,6 +11,7 @@
#include "core/models/protocols/dnsProtocolConfig.h"
#include "core/models/protocols/mtProxyProtocolConfig.h"
#include "core/models/protocols/telemtProtocolConfig.h"
#include "core/models/protocols/tProxyProtocolConfig.h"
namespace amnezia
{
@@ -44,6 +45,8 @@ Proto ProtocolConfig::type() const
return Proto::MtProxy;
} else if constexpr (std::is_same_v<T, TelemtProtocolConfig>) {
return Proto::Telemt;
} else if constexpr (std::is_same_v<T, TProxyProtocolConfig>) {
return Proto::TProxy;
}
return Proto::Unknown;
}, data);
@@ -75,6 +78,8 @@ QString ProtocolConfig::port() const
return arg.port.isEmpty() ? QString(protocols::mtProxy::defaultPort) : arg.port;
} else if constexpr (std::is_same_v<T, TelemtProtocolConfig>) {
return arg.port.isEmpty() ? QString(protocols::telemt::defaultPort) : arg.port;
} else if constexpr (std::is_same_v<T, TProxyProtocolConfig>) {
return arg.port.isEmpty() ? QString(protocols::tProxy::defaultPort) : arg.port;
}
return QString();
}, data);
@@ -102,6 +107,8 @@ QString ProtocolConfig::transportProto() const
return QStringLiteral("tcp");
} else if constexpr (std::is_same_v<T, TelemtProtocolConfig>) {
return QStringLiteral("tcp");
} else if constexpr (std::is_same_v<T, TProxyProtocolConfig>) {
return QStringLiteral("tcp");
}
return QString();
}, data);
@@ -317,6 +324,8 @@ ProtocolConfig ProtocolConfig::fromJson(const QJsonObject& json, Proto type)
return ProtocolConfig{MtProxyProtocolConfig::fromJson(json)};
case Proto::Telemt:
return ProtocolConfig{TelemtProtocolConfig::fromJson(json)};
case Proto::TProxy:
return ProtocolConfig{TProxyProtocolConfig::fromJson(json)};
default:
return ProtocolConfig{AwgProtocolConfig{}};
}

View File

@@ -24,6 +24,7 @@
#include "core/models/protocols/dnsProtocolConfig.h"
#include "core/models/protocols/mtProxyProtocolConfig.h"
#include "core/models/protocols/telemtProtocolConfig.h"
#include "core/models/protocols/tProxyProtocolConfig.h"
namespace amnezia
{
@@ -40,6 +41,7 @@ struct ProtocolConfig {
Socks5ProxyProtocolConfig,
MtProxyProtocolConfig,
TelemtProtocolConfig,
TProxyProtocolConfig,
Ikev2ProtocolConfig,
TorProtocolConfig,
DnsProtocolConfig

View File

@@ -0,0 +1,77 @@
#include "tProxyProtocolConfig.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/constants/protocolConstants.h"
using namespace amnezia;
QJsonObject TProxyProtocolConfig::toJson() const
{
QJsonObject obj;
if (!port.isEmpty()) {
obj[QString(configKey::port)] = port;
}
if (!httpPort.isEmpty()) {
obj[protocols::tProxy::httpPortKey] = httpPort;
}
if (!secret.isEmpty()) {
obj[protocols::tProxy::secretKey] = secret;
}
if (!hostname.isEmpty()) {
obj[protocols::tProxy::hostnameKey] = hostname;
}
if (!acmeEmail.isEmpty()) {
obj[protocols::tProxy::acmeEmailKey] = acmeEmail;
}
if (!carrierMode.isEmpty()) {
obj[protocols::tProxy::carrierModeKey] = carrierMode;
}
if (!workers.isEmpty()) {
obj[protocols::tProxy::workersKey] = workers;
}
if (!tgLink.isEmpty()) {
obj[protocols::tProxy::tgLinkKey] = tgLink;
}
if (!tmeLink.isEmpty()) {
obj[protocols::tProxy::tmeLinkKey] = tmeLink;
}
obj[protocols::tProxy::isEnabledKey] = isEnabled;
return obj;
}
TProxyProtocolConfig TProxyProtocolConfig::fromJson(const QJsonObject &json)
{
TProxyProtocolConfig c;
c.port = json.value(QString(configKey::port)).toString();
c.httpPort = json.value(protocols::tProxy::httpPortKey).toString();
c.secret = json.value(protocols::tProxy::secretKey).toString();
c.hostname = json.value(protocols::tProxy::hostnameKey).toString();
c.acmeEmail = json.value(protocols::tProxy::acmeEmailKey).toString();
c.carrierMode = json.value(protocols::tProxy::carrierModeKey).toString();
c.workers = json.value(protocols::tProxy::workersKey).toString();
c.tgLink = json.value(protocols::tProxy::tgLinkKey).toString();
c.tmeLink = json.value(protocols::tProxy::tmeLinkKey).toString();
c.isEnabled = json.value(protocols::tProxy::isEnabledKey).toBool(true);
return c;
}
bool TProxyProtocolConfig::equalsDockerDeploymentSettings(const TProxyProtocolConfig &other) const
{
const auto normCarrier = [](const QString &m) {
return m.isEmpty() ? QString(protocols::tProxy::carrierModeHttps) : m;
};
const auto normWorkers = [](const QString &w) {
return w.isEmpty() ? QString(protocols::tProxy::defaultWorkers) : w;
};
const auto normPort = [](const QString &p, const char *defaultPort) {
return p.isEmpty() ? QString(defaultPort) : p;
};
return hostname == other.hostname && secret == other.secret && acmeEmail == other.acmeEmail
&& normPort(port, protocols::tProxy::defaultPort) == normPort(other.port, protocols::tProxy::defaultPort)
&& normPort(httpPort, protocols::tProxy::defaultHttpPort)
== normPort(other.httpPort, protocols::tProxy::defaultHttpPort)
&& normCarrier(carrierMode) == normCarrier(other.carrierMode)
&& normWorkers(workers) == normWorkers(other.workers) && isEnabled == other.isEnabled;
}

View File

@@ -0,0 +1,28 @@
#ifndef TPROXYPROTOCOLCONFIG_H
#define TPROXYPROTOCOLCONFIG_H
#include <QJsonObject>
#include <QString>
namespace amnezia {
struct TProxyProtocolConfig {
QString port;
QString httpPort;
QString secret;
QString hostname;
QString acmeEmail;
QString carrierMode;
QString workers;
QString tgLink;
QString tmeLink;
bool isEnabled = true;
QJsonObject toJson() const;
static TProxyProtocolConfig fromJson(const QJsonObject &json);
bool equalsDockerDeploymentSettings(const TProxyProtocolConfig &other) const;
};
} // namespace amnezia
#endif // TPROXYPROTOCOLCONFIG_H

View File

@@ -71,6 +71,7 @@ QMap<Proto, QString> ProtocolUtils::protocolHumanNames()
{ Proto::Socks5Proxy, QObject::tr("SOCKS5 proxy server") },
{ Proto::MtProxy, QObject::tr("MTProxy (Telegram)") },
{ Proto::Telemt, QObject::tr("Telemt (Telegram)") },
{ Proto::TProxy, QObject::tr("TProxy (Telegram WEB)") },
};
}
@@ -97,6 +98,7 @@ ServiceType ProtocolUtils::protocolService(Proto p)
case Proto::Socks5Proxy: return ServiceType::Other;
case Proto::MtProxy: return ServiceType::Other;
case Proto::Telemt: return ServiceType::Other;
case Proto::TProxy: return ServiceType::Other;
default: return ServiceType::Other;
}
}
@@ -111,6 +113,7 @@ int ProtocolUtils::getPortForInstall(Proto p)
return QRandomGenerator::global()->bounded(30000, 50000);
case MtProxy:
case Telemt:
case TProxy:
default:
return defaultPort(p);
}
@@ -132,6 +135,7 @@ int ProtocolUtils::defaultPort(Proto p)
case Proto::Socks5Proxy: return 38080;
case Proto::MtProxy: return QString(protocols::mtProxy::defaultPort).toInt();
case Proto::Telemt: return QString(protocols::telemt::defaultPort).toInt();
case Proto::TProxy: return QString(protocols::tProxy::defaultPort).toInt();
default: return -1;
}
}
@@ -152,6 +156,7 @@ bool ProtocolUtils::defaultPortChangeable(Proto p)
case Proto::Socks5Proxy: return true;
case Proto::MtProxy: return true;
case Proto::Telemt: return true;
case Proto::TProxy: return true;
default: return false;
}
}
@@ -174,6 +179,7 @@ TransportProto ProtocolUtils::defaultTransportProto(Proto p)
case Proto::Socks5Proxy: return TransportProto::Tcp;
case Proto::MtProxy: return TransportProto::Tcp;
case Proto::Telemt: return TransportProto::Tcp;
case Proto::TProxy: return TransportProto::Tcp;
default: return TransportProto::Udp;
}
}
@@ -195,6 +201,7 @@ bool ProtocolUtils::defaultTransportProtoChangeable(Proto p)
case Proto::Socks5Proxy: return false;
case Proto::MtProxy: return false;
case Proto::Telemt: return false;
case Proto::TProxy: return false;
default: return false;
}
}

View File

@@ -142,6 +142,7 @@ namespace amnezia
constexpr QLatin1String socks5proxy("socks5proxy");
constexpr QLatin1String mtproxy("mtproxy");
constexpr QLatin1String telemt("telemt");
constexpr QLatin1String tproxy("tproxy");
constexpr QLatin1String splitTunnelSites("splitTunnelSites");
constexpr QLatin1String splitTunnelType("splitTunnelType");

View File

@@ -294,6 +294,29 @@ namespace amnezia
constexpr int botTagHexLength = 32;
}
namespace tProxy
{
constexpr char secretKey[] = "tproxy_secret";
constexpr char hostnameKey[] = "tproxy_hostname";
constexpr char acmeEmailKey[] = "tproxy_acme_email";
constexpr char carrierModeKey[] = "tproxy_carrier_mode";
constexpr char tgLinkKey[] = "tproxy_tg_link";
constexpr char tmeLinkKey[] = "tproxy_tme_link";
constexpr char isEnabledKey[] = "tproxy_is_enabled";
constexpr char workersKey[] = "tproxy_workers";
constexpr char httpPortKey[] = "tproxy_http_port";
constexpr char carrierModeHttps[] = "https";
constexpr char carrierModeHttpsLanes[] = "https-lanes";
constexpr char carrierModeWebsocket[] = "websocket";
constexpr char carrierModeWebsocketLanes[] = "websocket-lanes";
constexpr char defaultHttpPort[] = "80";
constexpr char defaultPort[] = "443";
constexpr char defaultWorkers[] = "1";
constexpr int maxWorkers = 32;
}
} // namespace protocols
}

View File

@@ -28,6 +28,7 @@ namespace amnezia
Socks5Proxy,
MtProxy,
Telemt,
TProxy,
};
Q_ENUM_NS(DockerContainer)
} // namespace ContainerEnumNS

View File

@@ -79,6 +79,7 @@ QMap<DockerContainer, QString> ContainerUtils::containerHumanNames()
{ DockerContainer::Socks5Proxy, QObject::tr("SOCKS5 proxy server") },
{ DockerContainer::MtProxy, QObject::tr("MTProxy (Telegram)") },
{ DockerContainer::Telemt, QObject::tr("Telemt (Telegram)") },
{ DockerContainer::TProxy, QObject::tr("TProxy (Telegram WEB)") },
};
}
@@ -117,6 +118,8 @@ QMap<DockerContainer, QString> ContainerUtils::containerDescriptions()
QObject::tr("Telegram MTProto proxy server") },
{ DockerContainer::Telemt,
QObject::tr("Telegram MTProto proxy (Telemt, Rust)") },
{ DockerContainer::TProxy,
QObject::tr("Telegram WEB proxy (tproxy-server)") },
};
}
@@ -196,6 +199,10 @@ QMap<DockerContainer, QString> ContainerUtils::containerDetailedDescriptions()
{ DockerContainer::Telemt,
QObject::tr("Telegram MTProto proxy powered by Telemt (Rust). "
"Supports secure and TLS fronting modes with optional traffic masking.") },
{ DockerContainer::TProxy,
QObject::tr("Telegram WEB proxy. Clients connect over HTTPS to a hostname; "
"the server relays traffic to official MTProxy. Requires a domain, "
"ports 80 and 443, and a WEB-capable Telegram app.") },
};
}
@@ -227,6 +234,7 @@ Proto ContainerUtils::defaultProtocol(DockerContainer c)
case DockerContainer::Socks5Proxy: return Proto::Socks5Proxy;
case DockerContainer::MtProxy: return Proto::MtProxy;
case DockerContainer::Telemt: return Proto::Telemt;
case DockerContainer::TProxy: return Proto::TProxy;
default: return Proto::Unknown;
}
}
@@ -256,6 +264,7 @@ bool ContainerUtils::isSupportedByCurrentPlatform(DockerContainer c)
case DockerContainer::SSXray: return true;
case DockerContainer::MtProxy: return true;
case DockerContainer::Telemt: return true;
case DockerContainer::TProxy: return true;
default:
return false;
}
@@ -273,6 +282,7 @@ bool ContainerUtils::isSupportedByCurrentPlatform(DockerContainer c)
case DockerContainer::SSXray: return true;
case DockerContainer::MtProxy: return true;
case DockerContainer::Telemt: return true;
case DockerContainer::TProxy: return true;
default:
return false;
}
@@ -293,6 +303,7 @@ bool ContainerUtils::isSupportedByCurrentPlatform(DockerContainer c)
case DockerContainer::SSXray: return true;
case DockerContainer::MtProxy: return true;
case DockerContainer::Telemt: return true;
case DockerContainer::TProxy: return true;
default: return false;
}
@@ -361,6 +372,7 @@ bool ContainerUtils::isShareable(DockerContainer container)
case DockerContainer::Socks5Proxy: return false;
case DockerContainer::MtProxy: return false;
case DockerContainer::Telemt: return false;
case DockerContainer::TProxy: return false;
default: return true;
}
}
@@ -396,6 +408,7 @@ int ContainerUtils::installPageOrder(DockerContainer container)
case DockerContainer::SSXray: return 8;
case DockerContainer::MtProxy:
case DockerContainer::Telemt:
case DockerContainer::TProxy:
return 20;
default: return 0;
}

View File

@@ -33,6 +33,7 @@ namespace amnezia
Socks5Proxy,
MtProxy,
Telemt,
TProxy,
};
Q_ENUM_NS(Proto)

View File

@@ -21,6 +21,7 @@
#include "core/models/protocols/socks5ProxyProtocolConfig.h"
#include "core/models/protocols/mtProxyProtocolConfig.h"
#include "core/models/protocols/telemtProtocolConfig.h"
#include "core/models/protocols/tProxyProtocolConfig.h"
using namespace amnezia;
using namespace ProtocolUtils;
@@ -41,6 +42,7 @@ QString amnezia::scriptFolder(amnezia::DockerContainer container)
case DockerContainer::Socks5Proxy: return QLatin1String("socks5_proxy");
case DockerContainer::MtProxy: return QLatin1String("mtproxy");
case DockerContainer::Telemt: return QLatin1String("telemt");
case DockerContainer::TProxy: return QLatin1String("tproxy");
default: return QString();
}
}
@@ -402,6 +404,30 @@ amnezia::ScriptVars amnezia::genTelemtVars(const ContainerConfig &containerConfi
return vars;
}
amnezia::ScriptVars amnezia::genTProxyVars(const ContainerConfig &containerConfig)
{
ScriptVars vars;
if (auto *tProxyConfig = containerConfig.getTProxyProtocolConfig()) {
const TProxyProtocolConfig &c = *tProxyConfig;
vars.append({{"$TPROXY_SECRET", c.secret}});
vars.append({{"$TPROXY_REGENERATE_SECRET",
c.secret.isEmpty() ? QStringLiteral("1") : QStringLiteral("0")}});
vars.append({{"$TPROXY_HOSTNAME", c.hostname}});
vars.append({{"$TPROXY_ACME_EMAIL", c.acmeEmail}});
vars.append({{"$TPROXY_HTTPS_PORT",
c.port.isEmpty() ? QString(protocols::tProxy::defaultPort) : c.port}});
vars.append({{"$TPROXY_HTTP_PORT",
c.httpPort.isEmpty() ? QString(protocols::tProxy::defaultHttpPort) : c.httpPort}});
vars.append({{"$TPROXY_CARRIER_MODE",
c.carrierMode.isEmpty() ? QString(protocols::tProxy::carrierModeHttps) : c.carrierMode}});
vars.append({{"$TPROXY_WORKERS",
c.workers.isEmpty() ? QString(protocols::tProxy::defaultWorkers) : c.workers}});
}
return vars;
}
amnezia::ScriptVars amnezia::genProtocolVarsForContainer(DockerContainer container, const ContainerConfig &containerConfig)
{
ScriptVars vars;
@@ -432,6 +458,9 @@ amnezia::ScriptVars amnezia::genProtocolVarsForContainer(DockerContainer contain
case Proto::Telemt:
vars.append(genTelemtVars(containerConfig));
break;
case Proto::TProxy:
vars.append(genTProxyVars(containerConfig));
break;
default:
break;
}

View File

@@ -70,6 +70,7 @@ ScriptVars genSftpVars(const ContainerConfig &containerConfig);
ScriptVars genSocks5ProxyVars(const ContainerConfig &containerConfig);
ScriptVars genMtProxyVars(const ContainerConfig &containerConfig);
ScriptVars genTelemtVars(const ContainerConfig &containerConfig);
ScriptVars genTProxyVars(const ContainerConfig &containerConfig);
ScriptVars genProtocolVarsForContainer(DockerContainer container, const ContainerConfig &containerConfig);
}

View File

@@ -103,7 +103,8 @@ ErrorCode SshSession::runContainerScript(const ServerCredentials &credentials, D
if (e)
return e;
const bool useSh = container == DockerContainer::Socks5Proxy || container == DockerContainer::MtProxy || container == DockerContainer::Telemt;
const bool useSh = container == DockerContainer::Socks5Proxy || container == DockerContainer::MtProxy
|| container == DockerContainer::Telemt || container == DockerContainer::TProxy;
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);

View File

@@ -33,6 +33,10 @@
<file>telemt/Dockerfile</file>
<file>telemt/run_container.sh</file>
<file>telemt/start.sh</file>
<file>tproxy/configure_container.sh</file>
<file>tproxy/Dockerfile</file>
<file>tproxy/run_container.sh</file>
<file>tproxy/start.sh</file>
<file>openvpn/configure_container.sh</file>
<file>openvpn/Dockerfile</file>
<file>openvpn/run_container.sh</file>

View File

@@ -10,6 +10,10 @@ sudo iptables -C FORWARD -i docker0 ! -o docker0 -j ACCEPT || sudo iptables -A F
sudo iptables -C FORWARD -i docker0 -o docker0 -j ACCEPT || sudo iptables -A FORWARD -i docker0 -o docker0 -j ACCEPT
# Tuning network
# Telegram proxies (mtproxy/telemt/tproxy) each open hundreds of middle-proxy
# connections; the default nf_conntrack_max (often 8192) overflows and new
# connections get dropped, so raise it.
sudo sysctl -w net.netfilter.nf_conntrack_max=262144 2>/dev/null || true; \
sudo sysctl fs.file-max=51200; \
sudo sysctl net.core.rmem_max=67108864; \
sudo sysctl net.core.wmem_max=67108864; \

View File

@@ -0,0 +1,58 @@
# syntax=docker/dockerfile:1
# Telegram WEB proxy: Caddy + tproxy-server + official MTProxy.
# Official MTProxy is x86_64-only (same constraint as upstream install.sh).
FROM golang:1.22-bookworm AS relay
WORKDIR /src
ARG TPROXY_COMMIT=2873a08806d6e4d84830b9b5c4b0ec0f46af91f8
RUN set -eux; \
apt-get update && apt-get install -y --no-install-recommends ca-certificates curl git && rm -rf /var/lib/apt/lists/*; \
curl -fL --retry 5 --retry-delay 3 --connect-timeout 10 --max-time 120 \
-o /tmp/tproxy.tar.gz \
"https://github.com/telegramdesktop/tproxy-server/archive/${TPROXY_COMMIT}.tar.gz"; \
tar -xzf /tmp/tproxy.tar.gz -C /src --strip-components=1; \
go build -trimpath -o /out/tproxy-server ./cmd/tproxy-server
FROM debian:12-slim AS mtproxy
ARG MTPROXY_COMMIT=f36d8af769ffaeac36978d38c2c0f6d1104c2137
ARG MTPROXY_SHA256=919795c416b870670841a21d1930ad97a24c7b84b9eb8c6f9e3de32f2fdf4655
RUN set -eux; \
apt-get update && apt-get install -y --no-install-recommends \
ca-certificates curl build-essential libssl-dev zlib1g-dev make && \
rm -rf /var/lib/apt/lists/*; \
curl -fL --retry 5 --retry-delay 3 --connect-timeout 10 --max-time 120 \
-o /tmp/MTProxy.tar.gz \
"https://github.com/TelegramMessenger/MTProxy/archive/${MTPROXY_COMMIT}.tar.gz"; \
echo "${MTPROXY_SHA256} /tmp/MTProxy.tar.gz" | sha256sum -c -; \
mkdir -p /src && tar -xzf /tmp/MTProxy.tar.gz -C /src --strip-components=1; \
make -C /src -j"$(nproc)"; \
test -x /src/objs/bin/mtproto-proxy; \
install -m 0755 /src/objs/bin/mtproto-proxy /mtproto-proxy
FROM debian:12-slim
RUN set -eux; \
ARCH="$(uname -m)"; \
if [ "$ARCH" != "x86_64" ]; then \
echo "tproxy-server requires an x86_64 server (official MTProxy is x86_64-only)" >&2; \
exit 1; \
fi; \
apt-get update && apt-get install -y --no-install-recommends \
ca-certificates curl openssl libssl3 zlib1g && \
rm -rf /var/lib/apt/lists/*; \
curl -fL --retry 5 --retry-delay 3 --connect-timeout 10 --max-time 120 \
-o /tmp/caddy.tgz \
"https://github.com/caddyserver/caddy/releases/download/v2.8.4/caddy_2.8.4_linux_amd64.tar.gz"; \
tar -xzf /tmp/caddy.tgz -C /usr/local/bin caddy; \
chmod 0755 /usr/local/bin/caddy; \
rm -f /tmp/caddy.tgz
COPY --from=relay /out/tproxy-server /usr/local/bin/tproxy-server
COPY --from=mtproxy /mtproto-proxy /usr/local/bin/mtproto-proxy
RUN mkdir -p /opt/amnezia /data /data/site /data/caddy \
&& printf '#!/bin/sh\ntail -f /dev/null\n' > /opt/amnezia/start.sh \
&& chmod a+x /opt/amnezia/start.sh /usr/local/bin/tproxy-server /usr/local/bin/mtproto-proxy
VOLUME /data
ENTRYPOINT ["/bin/sh", "/opt/amnezia/start.sh"]
CMD [""]

View File

@@ -0,0 +1,146 @@
#!/bin/sh
mkdir -p /data/site /data/caddy
if [ "$TPROXY_REGENERATE_SECRET" = "1" ]; then
SECRET=$(openssl rand -hex 16)
elif [ -n "$TPROXY_SECRET" ]; then
SECRET="$TPROXY_SECRET"
elif [ -f /data/secret ]; then
SECRET=$(cat /data/secret)
else
SECRET=$(openssl rand -hex 16)
fi
echo "$SECRET" | grep -qE '^[0-9a-fA-F]{32}$' || SECRET=$(openssl rand -hex 16)
HOST=$TPROXY_HOSTNAME
EMAIL=$TPROXY_ACME_EMAIL
CARRIER=$TPROXY_CARRIER_MODE
[ -z "$CARRIER" ] && CARRIER=https
WORKERS=$TPROXY_WORKERS
[ -z "$WORKERS" ] && WORKERS=1
HTTPS_PORT=$TPROXY_HTTPS_PORT
[ -z "$HTTPS_PORT" ] && HTTPS_PORT=443
HTTP_PORT=$TPROXY_HTTP_PORT
[ -z "$HTTP_PORT" ] && HTTP_PORT=80
if [ -z "$HOST" ] || [ -z "$EMAIL" ]; then
echo "[!] ERROR: TPROXY_HOSTNAME and TPROXY_ACME_EMAIL are required"
exit 1
fi
echo "$SECRET" > /data/secret
chmod 600 /data/secret 2>/dev/null || true
{
printf 'hostname=%s\n' "$HOST"
printf 'email=%s\n' "$EMAIL"
printf 'carrier=%s\n' "$CARRIER"
printf 'workers=%s\n' "$WORKERS"
printf 'http_port=%s\n' "$HTTP_PORT"
printf 'https_port=%s\n' "$HTTPS_PORT"
} > /data/tproxy-meta
# Decoy landing: unique enough per hostname/secret, not a shared Amnezia fingerprint page.
SITE_ID=$(echo -n "${HOST}${SECRET}" | openssl dgst -sha256 | awk '{print substr($2,1,12)}')
cat > /data/site/index.html << SITE
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${HOST}</title>
<style>
body { font-family: Georgia, serif; margin: 12% auto; max-width: 36em; color: #222; }
h1 { font-weight: normal; font-size: 1.4em; }
p { line-height: 1.5; color: #555; }
</style>
</head>
<body>
<h1>${HOST}</h1>
<p>This host is reserved. Ref ${SITE_ID}.</p>
</body>
</html>
SITE
cat > /data/config.json << EOF
{
"public_hostname": "$HOST",
"listen": "127.0.0.1:8080",
"admin_listen": "127.0.0.1:8081",
"public_dir": "/data/site",
"profiles_file": "/data/profiles.json",
"enable_pprof": false
}
EOF
cat > /data/profiles.json << EOF
{
"profiles": [
{
"name": "default",
"secret": "$SECRET",
"backend": "127.0.0.1:2398",
"carrier_mode": "$CARRIER"
}
]
}
EOF
chmod 0400 /data/profiles.json
curl -s --max-time 15 https://core.telegram.org/getProxySecret -o /data/proxy-secret
if [ ! -s /data/proxy-secret ]; then
echo "[!] WARNING: proxy-secret download failed or empty"
fi
curl -s --max-time 15 https://core.telegram.org/getProxyConfig -o /data/proxy-multi.conf
if [ ! -s /data/proxy-multi.conf ]; then
echo "[!] WARNING: proxy-multi.conf download failed or empty"
fi
if [ -n "$EMAIL" ] && [ -n "$HOST" ]; then
cat > /data/Caddyfile << EOF
{
email $EMAIL
admin off
servers {
protocols h1 h2
timeouts {
read_header 10s
read_body 60s
}
}
}
$HOST {
reverse_proxy 127.0.0.1:8080 {
transport http {
read_timeout 40s
write_timeout 40s
}
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Proto {scheme}
header_up X-Forwarded-Host {host}
}
}
EOF
else
cat > /data/Caddyfile << EOF
{
auto_https off
admin off
}
:80 {
reverse_proxy 127.0.0.1:8080
}
EOF
fi
echo "[*] Secret: $SECRET"
echo "[*] Hostname: $HOST"
if [ -n "$HOST" ]; then
echo "[*] tg:// link: tg://webproxy?server=${HOST}&secret=${SECRET}"
echo "[*] t.me link: https://t.me/webproxy?server=${HOST}&secret=${SECRET}"
else
echo "[*] tg:// link: "
echo "[*] t.me link: "
fi

View File

@@ -0,0 +1,9 @@
# Run Telegram WEB proxy (host ports map to Caddy 80/443 inside the container)
sudo docker run -d \
--log-driver none \
--restart always \
-p $TPROXY_HTTP_PORT:80/tcp \
-p $TPROXY_HTTPS_PORT:443/tcp \
-v amnezia-tproxy-data:/data \
--name $CONTAINER_NAME \
$CONTAINER_NAME

View File

@@ -0,0 +1,77 @@
#!/bin/sh
if [ ! -f /data/secret ]; then
echo "ERROR: /data/secret not found — run configure_container first"
tail -f /dev/null
exit 1
fi
SECRET=$(cat /data/secret)
WORKERS=1
if [ -f /data/tproxy-meta ]; then
_w=$(grep '^workers=' /data/tproxy-meta 2>/dev/null | head -1 | cut -d= -f2-)
[ -n "$_w" ] && WORKERS="$_w"
fi
if [ ! -s /data/proxy-secret ]; then
echo "[!] WARNING: /data/proxy-secret missing or empty"
fi
if [ ! -s /data/proxy-multi.conf ]; then
echo "[!] WARNING: /data/proxy-multi.conf missing or empty"
fi
if [ ! -f /data/config.json ]; then
echo "[!] WARNING: /data/config.json missing"
fi
if [ ! -f /data/Caddyfile ]; then
echo "[!] WARNING: /data/Caddyfile missing"
fi
mkdir -p /data/caddy /data/site
export XDG_DATA_HOME=/data/caddy
export XDG_CONFIG_HOME=/data/caddy
stop_children() {
kill $MPID $RPID $CPID 2>/dev/null
wait
}
trap 'stop_children; exit 0' TERM INT
# Official MTProxy runs inside docker (container IP != public IP). Without --nat-info
# it fails middle-proxy registration and never opens upstream connections to Telegram
# DC, so the relay gets no downstream bytes. Mirror the standalone mtproxy launch.
NAT_FLAG=""
NAT_VALUE=""
INTERNAL_IP=$(hostname -i 2>/dev/null | awk '{print $1}')
# Amnezia injects the resolved public server IP; api.ipify from inside the container
# is unreliable (often empty) and an empty external IP breaks middle-proxy registration.
EXTERNAL_IP="$SERVER_IP_ADDRESS"
if [ -z "$EXTERNAL_IP" ]; then
EXTERNAL_IP=$(curl -s --max-time 5 https://api.ipify.org 2>/dev/null)
[ -z "$EXTERNAL_IP" ] && EXTERNAL_IP=$(curl -s --max-time 5 https://ifconfig.me 2>/dev/null)
fi
if [ -n "$INTERNAL_IP" ] && [ -n "$EXTERNAL_IP" ] && [ "$INTERNAL_IP" != "$EXTERNAL_IP" ]; then
NAT_FLAG="--nat-info"
# mtproto-proxy wants a single arg: --nat-info <local-addr>:<global-addr> (colon-separated).
NAT_VALUE="${INTERNAL_IP}:${EXTERNAL_IP}"
fi
mtproto-proxy \
-u root \
-p 8888 \
-H 2398 \
-S ${SECRET} \
--aes-pwd /data/proxy-secret \
-M ${WORKERS} \
-C 60000 \
--allow-skip-dh \
${NAT_FLAG:+${NAT_FLAG} ${NAT_VALUE}} \
/data/proxy-multi.conf &
MPID=$!
tproxy-server -config /data/config.json &
RPID=$!
caddy run --config /data/Caddyfile --adapter caddyfile &
CPID=$!
wait $MPID $RPID $CPID

View File

@@ -53,6 +53,7 @@ InstallUiController::InstallUiController(InstallController *installController,
Socks5ProxyConfigModel *socks5ConfigModel,
MtProxyConfigModel* mtConfigModel,
TelemtConfigModel *telemtConfigModel,
TProxyConfigModel *tProxyConfigModel,
ConnectionController *connectionController,
QObject *parent)
: QObject(parent),
@@ -73,6 +74,7 @@ InstallUiController::InstallUiController(InstallController *installController,
m_socks5ConfigModel(socks5ConfigModel),
m_mtProxyConfigModel(mtConfigModel),
m_telemtConfigModel(telemtConfigModel),
m_tProxyConfigModel(tProxyConfigModel),
m_connectionController(connectionController)
{
connect(m_installController, &InstallController::configValidated, this, &InstallUiController::configValidated);
@@ -85,6 +87,11 @@ InstallUiController::~InstallUiController()
void InstallUiController::install(DockerContainer container, int port, TransportProto transportProto, const QString &serverId)
{
if (container == DockerContainer::TProxy && m_tProxyConfigModel) {
m_installController->setTProxyInstallHints(m_tProxyConfigModel->getHostname(),
m_tProxyConfigModel->getAcmeEmail());
}
const bool isNewServer = serverId.isEmpty();
ServerCredentials serverCredentials;
@@ -257,6 +264,10 @@ bool InstallUiController::buildContainerConfigFromModel(int containerIndex, int
containerConfig.protocolConfig = m_telemtConfigModel->getProtocolConfig();
break;
}
case Proto::TProxy: {
containerConfig.protocolConfig = m_tProxyConfigModel->getProtocolConfig();
break;
}
#ifdef Q_OS_WINDOWS
case Proto::Ikev2: {
containerConfig.protocolConfig = m_ikev2ConfigModel->getProtocolConfig();
@@ -304,10 +315,12 @@ void InstallUiController::updateServerConfig(const QString &serverId, int contai
ContainerConfig oldContainerConfig = m_serversController->getContainerConfig(serverId, container);
const bool asyncUpdate = container == DockerContainer::MtProxy || container == DockerContainer::Telemt
|| container == DockerContainer::TProxy
|| container == DockerContainer::Xray || container == DockerContainer::SSXray;
if (asyncUpdate) {
const bool emitBusy = container == DockerContainer::MtProxy || container == DockerContainer::Telemt;
const bool emitBusy = container == DockerContainer::MtProxy || container == DockerContainer::Telemt
|| container == DockerContainer::TProxy;
if (emitBusy)
emit serverIsBusy(true);
auto *watcher = new QFutureWatcher<ErrorCode>(this);
@@ -358,7 +371,8 @@ void InstallUiController::updateServerConfig(const QString &serverId, int contai
void InstallUiController::setContainerEnabled(const QString &serverId, int containerIndex, bool enabled)
{
const DockerContainer container = static_cast<DockerContainer>(containerIndex);
if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt) {
if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt
&& container != DockerContainer::TProxy) {
return;
}
@@ -389,7 +403,8 @@ void InstallUiController::setContainerEnabled(const QString &serverId, int conta
void InstallUiController::refreshContainerStatus(const QString &serverId, int containerIndex)
{
const DockerContainer container = static_cast<DockerContainer>(containerIndex);
if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt) {
if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt
&& container != DockerContainer::TProxy) {
return;
}
@@ -445,7 +460,8 @@ void InstallUiController::refreshContainerDiagnostics(const QString &serverId, i
void InstallUiController::fetchContainerSecret(const QString &serverId, int containerIndex)
{
const DockerContainer container = static_cast<DockerContainer>(containerIndex);
if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt) {
if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt
&& container != DockerContainer::TProxy) {
return;
}
@@ -711,6 +727,7 @@ void InstallUiController::updateProtocolConfigModel(const QString &serverId, int
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;
case Proto::TProxy: updateIfPresent(m_tProxyConfigModel, containerConfig.getTProxyProtocolConfig()); break;
#ifdef Q_OS_WINDOWS
case Proto::Ikev2: updateIfPresent(m_ikev2ConfigModel, containerConfig.getIkev2ProtocolConfig()); break;
#endif

View File

@@ -31,6 +31,7 @@
#include "core/models/protocols/socks5ProxyProtocolConfig.h"
#include "ui/models/services/mtProxyConfigModel.h"
#include "ui/models/services/telemtConfigModel.h"
#include "ui/models/services/tProxyConfigModel.h"
class InstallUiController : public QObject
{
@@ -53,6 +54,7 @@ public:
Socks5ProxyConfigModel* socks5ConfigModel,
MtProxyConfigModel* mtConfigModel,
TelemtConfigModel* telemtConfigModel,
TProxyConfigModel* tProxyConfigModel,
ConnectionController* connectionController,
QObject *parent = nullptr);
~InstallUiController();
@@ -155,6 +157,7 @@ private:
Socks5ProxyConfigModel* m_socks5ConfigModel;
MtProxyConfigModel* m_mtProxyConfigModel;
TelemtConfigModel* m_telemtConfigModel;
TProxyConfigModel* m_tProxyConfigModel;
ConnectionController* m_connectionController;
ServerCredentials m_processedServerCredentials;

View File

@@ -572,6 +572,8 @@ QStringList ServersUiController::getAllInstalledServicesName(int serverIndex) co
servicesName.append("MTProxy");
} else if (container == DockerContainer::Telemt) {
servicesName.append("Telemt");
} else if (container == DockerContainer::TProxy) {
servicesName.append("TProxy");
}
}
}

View File

@@ -77,6 +77,7 @@ QVariant ContainersModel::data(const QModelIndex &index, int role) const
case IsSocks5ProxyRole: return container == DockerContainer::Socks5Proxy;
case IsMtProxyRole: return container == DockerContainer::MtProxy;
case IsTelemtRole: return container == DockerContainer::Telemt;
case IsTProxyRole: return container == DockerContainer::TProxy;
case InstallPageOrderRole: return ContainerUtils::installPageOrder(container);
}
@@ -191,5 +192,6 @@ QHash<int, QByteArray> ContainersModel::roleNames() const
roles[IsSocks5ProxyRole] = "isSocks5Proxy";
roles[IsMtProxyRole] = "isMtProxy";
roles[IsTelemtRole] = "isTelemt";
roles[IsTProxyRole] = "isTProxy";
return roles;
}

View File

@@ -53,6 +53,7 @@ public:
IsSocks5ProxyRole,
IsMtProxyRole,
IsTelemtRole,
IsTProxyRole,
};
Q_INVOKABLE void openContainerSettings(int containerIndex);

View File

@@ -44,6 +44,7 @@ QHash<int, QByteArray> ProtocolsModel::roleNames() const
roles[IsSocks5ProxyRole] = "isSocks5Proxy";
roles[IsMtProxyRole] = "isMtProxy";
roles[IsTelemtRole] = "isTelemt";
roles[IsTProxyRole] = "isTProxy";
return roles;
}
@@ -75,6 +76,7 @@ QVariant ProtocolsModel::data(const QModelIndex &index, int role) const
case IsSocks5ProxyRole: return proto == Proto::Socks5Proxy;
case IsMtProxyRole: return proto == Proto::MtProxy;
case IsTelemtRole: return proto == Proto::Telemt;
case IsTProxyRole: return proto == Proto::TProxy;
case RawConfigRole:
return getRawConfig();
case IsClientProtocolExistsRole:
@@ -130,6 +132,7 @@ PageLoader::PageEnum ProtocolsModel::serverProtocolPage(Proto protocol) const
case Proto::Socks5Proxy: return PageLoader::PageEnum::PageServiceSocksProxySettings;
case Proto::MtProxy: return PageLoader::PageEnum::PageServiceMtProxySettings;
case Proto::Telemt: return PageLoader::PageEnum::PageServiceTelemtSettings;
case Proto::TProxy: return PageLoader::PageEnum::PageServiceTProxySettings;
default: return PageLoader::PageEnum::PageProtocolOpenVpnSettings;
}
}

View File

@@ -28,6 +28,7 @@ public:
IsSocks5ProxyRole,
IsMtProxyRole,
IsTelemtRole,
IsTProxyRole,
};
explicit ProtocolsModel(QObject *parent = nullptr);

View File

@@ -0,0 +1,420 @@
#include "tProxyConfigModel.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/constants/protocolConstants.h"
#include <QRandomGenerator>
#include <QRegularExpression>
using namespace amnezia;
namespace {
const QRegularExpression kHostnameRe(QStringLiteral("^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$"));
const QRegularExpression kEmailRe(QStringLiteral("^[A-Za-z0-9._+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$"));
const QRegularExpression kHex32Re(QStringLiteral("^[0-9a-fA-F]{32}$"));
}
TProxyConfigModel::TProxyConfigModel(QObject *parent) : QAbstractListModel(parent) {}
int TProxyConfigModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return 1;
}
bool TProxyConfigModel::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::HttpPortRole:
m_protocolConfig.httpPort = value.toString();
break;
case Roles::SecretRole:
m_protocolConfig.secret = value.toString();
break;
case Roles::HostnameRole: {
const QString h = sanitizeHostnameFieldText(value.toString());
if (!isValidHostname(h)) {
return false;
}
m_protocolConfig.hostname = h;
break;
}
case Roles::AcmeEmailRole: {
const QString e = sanitizeAcmeEmailFieldText(value.toString());
if (!isValidAcmeEmail(e)) {
return false;
}
m_protocolConfig.acmeEmail = e;
break;
}
case Roles::CarrierModeRole: {
const QString m = value.toString();
if (!isValidCarrierMode(m)) {
return false;
}
m_protocolConfig.carrierMode = m;
break;
}
case Roles::WorkersRole:
m_protocolConfig.workers = sanitizeWorkersFieldText(value.toString());
break;
case Roles::IsEnabledRole:
m_protocolConfig.isEnabled = value.toBool();
break;
default:
return false;
}
emit dataChanged(index, index, QList{role});
return true;
}
QVariant TProxyConfigModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || index.row() != 0) {
return QVariant();
}
switch (role) {
case Roles::PortRole:
return m_protocolConfig.port.isEmpty() ? QString(protocols::tProxy::defaultPort) : m_protocolConfig.port;
case Roles::HttpPortRole:
return m_protocolConfig.httpPort.isEmpty() ? QString(protocols::tProxy::defaultHttpPort) : m_protocolConfig.httpPort;
case Roles::SecretRole:
return m_protocolConfig.secret;
case Roles::HostnameRole:
return m_protocolConfig.hostname;
case Roles::AcmeEmailRole:
return m_protocolConfig.acmeEmail;
case Roles::CarrierModeRole:
return m_protocolConfig.carrierMode.isEmpty() ? QString(protocols::tProxy::carrierModeHttps)
: m_protocolConfig.carrierMode;
case Roles::WorkersRole:
return m_protocolConfig.workers.isEmpty() ? QString(protocols::tProxy::defaultWorkers)
: m_protocolConfig.workers;
case Roles::TgLinkRole:
return m_protocolConfig.tgLink;
case Roles::TmeLinkRole:
return m_protocolConfig.tmeLink;
case Roles::IsEnabledRole:
return m_protocolConfig.isEnabled;
}
return QVariant();
}
void TProxyConfigModel::updateModel(DockerContainer container, const TProxyProtocolConfig &protocolConfig)
{
beginResetModel();
m_container = container;
m_protocolConfig = protocolConfig;
endResetModel();
}
void TProxyConfigModel::updateModel(const QJsonObject &config)
{
beginResetModel();
m_fullConfig = config;
m_protocolConfig = TProxyProtocolConfig::fromJson(config.value(configKey::tproxy).toObject());
if (m_protocolConfig.port.isEmpty()) {
m_protocolConfig.port = protocols::tProxy::defaultPort;
}
if (m_protocolConfig.httpPort.isEmpty()) {
m_protocolConfig.httpPort = protocols::tProxy::defaultHttpPort;
}
if (m_protocolConfig.carrierMode.isEmpty()) {
m_protocolConfig.carrierMode = protocols::tProxy::carrierModeHttps;
}
if (m_protocolConfig.workers.isEmpty()) {
m_protocolConfig.workers = protocols::tProxy::defaultWorkers;
}
endResetModel();
}
QJsonObject TProxyConfigModel::getConfig()
{
m_fullConfig.insert(configKey::tproxy, m_protocolConfig.toJson());
return m_fullConfig;
}
TProxyProtocolConfig TProxyConfigModel::getProtocolConfig()
{
return m_protocolConfig;
}
void TProxyConfigModel::generateSecret()
{
QString secret;
for (int i = 0; i < 16; ++i) {
const quint32 byte = QRandomGenerator::global()->bounded(256);
secret += QStringLiteral("%1").arg(byte, 2, 16, QChar('0'));
}
m_protocolConfig.secret = secret;
emit dataChanged(index(0), index(0), QList<int>{SecretRole});
}
void TProxyConfigModel::setSecret(const QString &secret)
{
if (secret.isEmpty()) {
return;
}
setData(index(0), secret, SecretRole);
}
bool TProxyConfigModel::validateAndSetSecret(const QString &rawSecret)
{
if (!kHex32Re.match(rawSecret).hasMatch()) {
return false;
}
return setData(index(0), rawSecret, SecretRole);
}
void TProxyConfigModel::setPort(const QString &port)
{
setData(index(0), sanitizePortFieldText(port), PortRole);
}
void TProxyConfigModel::setHttpPort(const QString &port)
{
setData(index(0), sanitizePortFieldText(port), HttpPortRole);
}
void TProxyConfigModel::setHostname(const QString &hostname)
{
setData(index(0), hostname, HostnameRole);
}
void TProxyConfigModel::setAcmeEmail(const QString &email)
{
setData(index(0), email, AcmeEmailRole);
}
void TProxyConfigModel::setCarrierMode(const QString &mode)
{
setData(index(0), mode, CarrierModeRole);
}
void TProxyConfigModel::setWorkers(const QString &workers)
{
setData(index(0), workers, WorkersRole);
}
void TProxyConfigModel::setEnabled(bool enabled)
{
setData(index(0), enabled, IsEnabledRole);
}
QString TProxyConfigModel::getHostname() const
{
return m_protocolConfig.hostname;
}
QString TProxyConfigModel::getSecret() const
{
return m_protocolConfig.secret;
}
QString TProxyConfigModel::getAcmeEmail() const
{
return m_protocolConfig.acmeEmail;
}
QString TProxyConfigModel::getCarrierMode() const
{
return m_protocolConfig.carrierMode.isEmpty() ? QString(protocols::tProxy::carrierModeHttps)
: m_protocolConfig.carrierMode;
}
QString TProxyConfigModel::getWorkers() const
{
return m_protocolConfig.workers.isEmpty() ? QString(protocols::tProxy::defaultWorkers) : m_protocolConfig.workers;
}
QString TProxyConfigModel::getPort() const
{
return m_protocolConfig.port.isEmpty() ? QString(protocols::tProxy::defaultPort) : m_protocolConfig.port;
}
QString TProxyConfigModel::getHttpPort() const
{
return m_protocolConfig.httpPort.isEmpty() ? QString(protocols::tProxy::defaultHttpPort) : m_protocolConfig.httpPort;
}
QString TProxyConfigModel::defaultPort() const
{
return QString(protocols::tProxy::defaultPort);
}
QString TProxyConfigModel::defaultHttpPort() const
{
return QString(protocols::tProxy::defaultHttpPort);
}
QString TProxyConfigModel::defaultWorkers() const
{
return QString(protocols::tProxy::defaultWorkers);
}
int TProxyConfigModel::maxWorkers() const
{
return protocols::tProxy::maxWorkers;
}
QString TProxyConfigModel::carrierModeHttps() const
{
return QString(protocols::tProxy::carrierModeHttps);
}
bool TProxyConfigModel::isValidHostname(const QString &host) const
{
if (host.isEmpty() || !host.contains(QLatin1Char('.'))) {
return false;
}
return kHostnameRe.match(host).hasMatch();
}
bool TProxyConfigModel::isHostnameTypingIncomplete(const QString &text) const
{
const QString t = text.trimmed().toLower();
if (t.isEmpty()) {
return true;
}
if (t.startsWith(QLatin1Char('-')) || t.startsWith(QLatin1Char('.'))) {
return true;
}
if (t.endsWith(QLatin1Char('.')) || t.endsWith(QLatin1Char('-'))) {
return true;
}
return !t.contains(QLatin1Char('.'));
}
QString TProxyConfigModel::sanitizeHostnameFieldText(const QString &input) const
{
QString out = input.trimmed().toLower();
out.remove(QRegularExpression(QStringLiteral("[^a-z0-9.-]")));
while (out.startsWith(QLatin1Char('-')) || out.startsWith(QLatin1Char('.'))) {
out.remove(0, 1);
}
return out;
}
bool TProxyConfigModel::isValidAcmeEmail(const QString &email) const
{
return kEmailRe.match(email.trimmed()).hasMatch();
}
bool TProxyConfigModel::isAcmeEmailTypingIncomplete(const QString &text) const
{
const QString t = text.trimmed();
if (t.isEmpty()) {
return true;
}
if (t.endsWith(QLatin1Char('.')) || t.endsWith(QLatin1Char('@')) || t.endsWith(QLatin1Char('-'))
|| t.endsWith(QLatin1Char('+')) || t.endsWith(QLatin1Char('_'))) {
return true;
}
if (!t.contains(QLatin1Char('@'))) {
return true;
}
const int at = t.indexOf(QLatin1Char('@'));
const QString domain = t.mid(at + 1);
if (!domain.contains(QLatin1Char('.'))) {
return true;
}
return false;
}
QString TProxyConfigModel::sanitizeAcmeEmailFieldText(const QString &input) const
{
QString out;
for (const QChar c : input.trimmed()) {
if (c.isLetterOrNumber() || c == QLatin1Char('.') || c == QLatin1Char('_') || c == QLatin1Char('+')
|| c == QLatin1Char('-') || c == QLatin1Char('@')) {
out.append(c);
}
}
const int firstAt = out.indexOf(QLatin1Char('@'));
if (firstAt >= 0) {
const int secondAt = out.indexOf(QLatin1Char('@'), firstAt + 1);
if (secondAt >= 0) {
out.remove(secondAt, 1);
}
}
return out;
}
bool TProxyConfigModel::isValidCarrierMode(const QString &mode) const
{
return mode == QLatin1String(protocols::tProxy::carrierModeHttps)
|| mode == QLatin1String(protocols::tProxy::carrierModeHttpsLanes)
|| mode == QLatin1String(protocols::tProxy::carrierModeWebsocket)
|| mode == QLatin1String(protocols::tProxy::carrierModeWebsocketLanes);
}
QString TProxyConfigModel::carrierModeLabel(const QString &mode) const
{
if (mode == QLatin1String(protocols::tProxy::carrierModeHttpsLanes)) {
return QStringLiteral("HTTPS lanes");
}
if (mode == QLatin1String(protocols::tProxy::carrierModeWebsocket)) {
return QStringLiteral("WebSocket");
}
if (mode == QLatin1String(protocols::tProxy::carrierModeWebsocketLanes)) {
return QStringLiteral("WebSocket lanes");
}
return QStringLiteral("HTTPS");
}
QString TProxyConfigModel::sanitizeWorkersFieldText(const QString &input) const
{
QString digits;
for (const QChar c : input) {
if (c.isDigit()) {
digits.append(c);
}
}
bool ok = false;
const int n = digits.toInt(&ok);
if (!ok || n < 1) {
return QString(protocols::tProxy::defaultWorkers);
}
if (n > protocols::tProxy::maxWorkers) {
return QString::number(protocols::tProxy::maxWorkers);
}
return QString::number(n);
}
QString TProxyConfigModel::sanitizePortFieldText(const QString &input) const
{
QString out;
out.reserve(qMin(input.size(), 5));
for (const QChar &c : input) {
const ushort u = c.unicode();
if (u >= '0' && u <= '9' && out.size() < 5) {
out.append(c);
}
}
return out;
}
QHash<int, QByteArray> TProxyConfigModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[PortRole] = "port";
roles[HttpPortRole] = "httpPort";
roles[SecretRole] = "secret";
roles[HostnameRole] = "hostname";
roles[AcmeEmailRole] = "acmeEmail";
roles[CarrierModeRole] = "carrierMode";
roles[WorkersRole] = "workers";
roles[TgLinkRole] = "tgLink";
roles[TmeLinkRole] = "tmeLink";
roles[IsEnabledRole] = "isEnabled";
return roles;
}

View File

@@ -0,0 +1,87 @@
#ifndef TPROXYCONFIGMODEL_H
#define TPROXYCONFIGMODEL_H
#include <QAbstractListModel>
#include <QJsonObject>
#include "core/utils/containerEnum.h"
#include "core/models/protocols/tProxyProtocolConfig.h"
class TProxyConfigModel : public QAbstractListModel {
Q_OBJECT
public:
enum Roles {
PortRole = Qt::UserRole + 1,
HttpPortRole,
SecretRole,
HostnameRole,
AcmeEmailRole,
CarrierModeRole,
WorkersRole,
TgLinkRole,
TmeLinkRole,
IsEnabledRole
};
explicit TProxyConfigModel(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::TProxyProtocolConfig &protocolConfig);
void updateModel(const QJsonObject &config);
QJsonObject getConfig();
amnezia::TProxyProtocolConfig 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 setHttpPort(const QString &port);
Q_INVOKABLE void setHostname(const QString &hostname);
Q_INVOKABLE void setAcmeEmail(const QString &email);
Q_INVOKABLE void setCarrierMode(const QString &mode);
Q_INVOKABLE void setWorkers(const QString &workers);
Q_INVOKABLE void setEnabled(bool enabled);
Q_INVOKABLE QString getHostname() const;
Q_INVOKABLE QString getSecret() const;
Q_INVOKABLE QString getAcmeEmail() const;
Q_INVOKABLE QString getCarrierMode() const;
Q_INVOKABLE QString getWorkers() const;
Q_INVOKABLE QString getPort() const;
Q_INVOKABLE QString getHttpPort() const;
Q_INVOKABLE QString defaultPort() const;
Q_INVOKABLE QString defaultHttpPort() const;
Q_INVOKABLE QString defaultWorkers() const;
Q_INVOKABLE int maxWorkers() const;
Q_INVOKABLE QString carrierModeHttps() const;
Q_INVOKABLE bool isValidHostname(const QString &host) const;
Q_INVOKABLE bool isHostnameTypingIncomplete(const QString &text) const;
Q_INVOKABLE QString sanitizeHostnameFieldText(const QString &input) const;
Q_INVOKABLE bool isValidAcmeEmail(const QString &email) const;
Q_INVOKABLE bool isAcmeEmailTypingIncomplete(const QString &text) const;
Q_INVOKABLE QString sanitizeAcmeEmailFieldText(const QString &input) const;
Q_INVOKABLE bool isValidCarrierMode(const QString &mode) const;
Q_INVOKABLE QString carrierModeLabel(const QString &mode) const;
Q_INVOKABLE QString sanitizeWorkersFieldText(const QString &input) const;
Q_INVOKABLE QString sanitizePortFieldText(const QString &input) const;
protected:
QHash<int, QByteArray> roleNames() const override;
private:
amnezia::DockerContainer m_container;
QJsonObject m_fullConfig;
amnezia::TProxyProtocolConfig m_protocolConfig;
};
#endif // TPROXYCONFIGMODEL_H

View File

@@ -54,6 +54,9 @@ ListViewType {
} else if (isTelemt) {
TelemtConfigModel.updateModel(config)
PageController.goToPage(PageEnum.PageServiceTelemtSettings, false)
} else if (isTProxy) {
TProxyConfigModel.updateModel(config)
PageController.goToPage(PageEnum.PageServiceTProxySettings, false)
} else {
InstallController.updateProtocols(ServersUiController.processedServerId, containerIndex)
PageController.goToPage(PageEnum.PageSettingsServerProtocol)

View File

@@ -0,0 +1,963 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import PageEnum 1.0
import ProtocolEnum 1.0
import Style 1.0
import "./"
import "../Controls2"
import "../Controls2/TextTypes"
import "../Config"
import "../Components"
PageType {
id: root
property int containerStatus: 1
property int statusErrorCode: 0
property bool isUpdating: false
property bool isCheckingStatus: false
property bool isFetchingSecret: false
property bool previousEnabled: true
property int previousContainerStatus: 1
property string previousHostname: ""
property string previousEmail: ""
property string previousSecret: ""
property string savedHostname: ""
property string savedCarrierMode: TProxyConfigModel.carrierModeHttps()
property string savedWorkers: TProxyConfigModel.defaultWorkers()
property bool pendingUpdateAfterEnable: false
property bool remoteOperationBusy: false
readonly property bool tproxyNetworkBlocked: !NetworkReachabilityController.hasInternetAccess
readonly property bool pageBusy: isCheckingStatus || isFetchingSecret || isUpdating || remoteOperationBusy
readonly property bool navigationBlockedWhileBusy: pageBusy
property bool pageOpenHandled: false
property bool busyIndicatorShown: false
property bool containerStatusRefreshCallPending: false
function syncPageBusyIndicator() {
if (!root.pageOpenHandled) {
return
}
var wantBusy = root.pageBusy
if (wantBusy === root.busyIndicatorShown) {
return
}
root.busyIndicatorShown = wantBusy
PageController.showBusyIndicator(wantBusy)
}
onPageBusyChanged: syncPageBusyIndicator()
function tproxyRequestContainerStatusRefresh() {
if (!NetworkReachabilityController.hasInternetAccess) {
isCheckingStatus = false
syncPageBusyIndicator()
return
}
isCheckingStatus = true
syncPageBusyIndicator()
InstallController.refreshContainerStatus(ServersUiController.processedServerId,
ServersUiController.processedContainerIndex)
}
function tproxyScheduleContainerStatusRefresh() {
if (containerStatusRefreshCallPending) {
return
}
containerStatusRefreshCallPending = true
Qt.callLater(function () {
containerStatusRefreshCallPending = false
root.tproxyRequestContainerStatusRefresh()
})
}
function tproxyOnPageShown() {
if (root.pageOpenHandled) {
return
}
root.pageOpenHandled = true
PageController.disableControls(navigationBlockedWhileBusy)
if (!NetworkReachabilityController.hasInternetAccess) {
isCheckingStatus = false
} else {
isCheckingStatus = true
}
syncPageBusyIndicator()
root.tproxyScheduleContainerStatusRefresh()
}
function scheduleUpdate() {
Qt.callLater(function () {
InstallController.updateServerConfig(ServersUiController.processedServerId,
ServersUiController.processedContainerIndex, ProtocolEnum.TProxy, false)
})
}
Connections {
target: InstallController
function onUpdateContainerFinished(message, closePage) {
if (!root.visible) {
isUpdating = false
isCheckingStatus = false
isFetchingSecret = false
return
}
isUpdating = false
containerStatus = 1
savedHostname = TProxyConfigModel.getHostname()
savedCarrierMode = TProxyConfigModel.getCarrierMode()
savedWorkers = TProxyConfigModel.getWorkers()
PageController.showNotificationMessage(message)
}
function onInstallationErrorOccurred() {
if (!root.visible) {
isUpdating = false
isCheckingStatus = false
isFetchingSecret = false
return
}
isUpdating = false
isFetchingSecret = false
containerStatus = previousContainerStatus
TProxyConfigModel.setEnabled(previousEnabled)
TProxyConfigModel.setHostname(previousHostname)
TProxyConfigModel.setAcmeEmail(previousEmail)
TProxyConfigModel.setCarrierMode(savedCarrierMode)
TProxyConfigModel.setWorkers(savedWorkers)
if (previousSecret !== "")
TProxyConfigModel.setSecret(previousSecret)
}
function onSetContainerEnabledFinished(enabled) {
if (!root.visible) {
isUpdating = false
return
}
if (enabled && root.pendingUpdateAfterEnable) {
root.pendingUpdateAfterEnable = false
isUpdating = true
root.scheduleUpdate()
return
}
isUpdating = false
containerStatus = enabled ? 1 : 2
PageController.showNotificationMessage(enabled ? qsTr("TProxy started") : qsTr("TProxy stopped"))
}
function onServerIsBusy(busy) {
root.remoteOperationBusy = busy
}
function onContainerStatusRefreshed(status, errorCode) {
if (!root.visible) {
isCheckingStatus = false
isFetchingSecret = false
return
}
containerStatus = status
root.statusErrorCode = errorCode
if (status === 3 && errorCode !== 0) {
PageController.showNotificationMessage(
qsTr("Settings locked: connection timed out (error code %1). Re-open the page to retry.").arg(errorCode))
}
savedHostname = TProxyConfigModel.getHostname()
savedCarrierMode = TProxyConfigModel.getCarrierMode()
savedWorkers = TProxyConfigModel.getWorkers()
if (status === 1) {
TProxyConfigModel.setEnabled(true)
isFetchingSecret = true
isCheckingStatus = false
InstallController.fetchContainerSecret(ServersUiController.processedServerId,
ServersUiController.processedContainerIndex)
} else {
isFetchingSecret = false
isCheckingStatus = false
if (status === 2) {
TProxyConfigModel.setEnabled(false)
}
}
syncPageBusyIndicator()
}
function onContainerSecretFetched(value) {
if (!root.visible) {
isFetchingSecret = false
return
}
isFetchingSecret = false
syncPageBusyIndicator()
if (value !== "")
TProxyConfigModel.validateAndSetSecret(value)
}
}
Component.onCompleted: {
savedHostname = TProxyConfigModel.getHostname()
savedCarrierMode = TProxyConfigModel.getCarrierMode()
savedWorkers = TProxyConfigModel.getWorkers()
Qt.callLater(root.tproxyOnPageShown)
}
onNavigationBlockedWhileBusyChanged: {
if (root.visible) {
PageController.disableControls(navigationBlockedWhileBusy)
}
}
onVisibleChanged: {
if (!visible) {
root.pageOpenHandled = false
containerStatusRefreshCallPending = false
isCheckingStatus = false
isFetchingSecret = false
busyIndicatorShown = false
statusErrorCode = 0
PageController.disableControls(false)
PageController.showBusyIndicator(false)
} else {
root.tproxyOnPageShown()
}
}
Connections {
target: NetworkReachabilityController
function onHasInternetAccessChanged() {
if (!root.visible) {
return
}
if (NetworkReachabilityController.hasInternetAccess) {
root.tproxyScheduleContainerStatusRefresh()
}
}
}
Item {
id: contentLayer
anchors.fill: parent
enabled: !root.pageBusy
BackButtonType {
id: backButton
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 20 + PageController.safeAreaTopMargin
onFocusChanged: {
if (this.activeFocus) {
if (mainTabBar.currentIndex === 0) {
connectionListView.positionViewAtBeginning()
} else {
settingsListView.positionViewAtBeginning()
}
}
}
}
ColumnLayout {
id: pageHeader
anchors.top: backButton.bottom
anchors.left: parent.left
anchors.right: parent.right
spacing: 0
BaseHeaderType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 24
headerText: qsTr("TProxy settings")
descriptionLinkText: qsTr("Read more about Telegram WEB proxy")
descriptionLinkUrl: "https://github.com/telegramdesktop/tproxy-server"
}
CaptionTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 8
visible: root.tproxyNetworkBlocked
text: qsTr("No internet connection. Connect to the internet to change TProxy settings.")
color: AmneziaStyle.color.mutedGray
wrapMode: Text.WordWrap
font.pixelSize: 14
}
}
TabBar {
id: mainTabBar
anchors.top: pageHeader.bottom
anchors.left: parent.left
anchors.right: parent.right
width: parent.width
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 {
anchors.top: mainTabBar.bottom
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
currentIndex: mainTabBar.currentIndex
ListViewType {
id: connectionListView
model: TProxyConfigModel
delegate: ColumnLayout {
width: connectionListView.width
spacing: 0
function effectiveHost() {
return root.savedHostname !== "" ? root.savedHostname : hostname
}
function displayTmeLink() {
if (tmeLink !== "")
return tmeLink
var host = effectiveHost()
if (host === "" || secret === "")
return ""
return "https://t.me/webproxy?server=" + host + "&secret=" + secret
}
function displayTgLink() {
if (tgLink !== "")
return tgLink
var host = effectiveHost()
if (host === "" || secret === "")
return ""
return "tg://webproxy?server=" + host + "&secret=" + secret
}
CaptionTextType {
Layout.fillWidth: true
Layout.topMargin: 24
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 8
text: qsTr("Use Telegram WEB proxy 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 !== "" ? displayTmeLink() : qsTr("Set a hostname 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/copy.svg"
imageColor: AmneziaStyle.color.paleGray
visible: secret !== ""
onClicked: {
GC.copyToClipBoard(displayTmeLink())
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: displayTgLink()
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.generateQrFromStringRaw(displayTgLink())
PageController.goToShareConnectionPage(qsTr("Telegram connection link"),
qsTr("TProxy WEB proxy link"), "", "", "")
}
}
ImageButtonType {
implicitWidth: 36
implicitHeight: 36
hoverEnabled: true
image: "qrc:/images/controls/copy.svg"
imageColor: AmneziaStyle.color.paleGray
onClicked: {
GC.copyToClipBoard(displayTgLink())
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://github.com/telegramdesktop/tproxy-server")
}
}
Item {
Layout.fillWidth: true
}
}
Rectangle {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 16
implicitHeight: manualCol.implicitHeight + 8
color: AmneziaStyle.color.onyxBlack
radius: 8
border.color: AmneziaStyle.color.slateGray
border.width: 1
visible: secret !== ""
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("Secret")
color: AmneziaStyle.color.mutedGray
font.pixelSize: 12
}
CaptionTextType {
Layout.fillWidth: true
text: secret
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(secret)
PageController.showNotificationMessage(qsTr("Copied"))
}
}
}
}
}
CaptionTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 8
Layout.bottomMargin: 16
text: qsTr("Needs a WEB-capable Telegram client (Desktop proof of concept). Classic MTProxy links will not work.")
color: AmneziaStyle.color.mutedGray
wrapMode: Text.WordWrap
font.pixelSize: 12
}
LabelWithButtonType {
id: removeButton
Layout.fillWidth: true
Layout.bottomMargin: 24
Layout.leftMargin: 0
Layout.rightMargin: 16
visible: ServersUiController.isProcessedServerHasWriteAccess()
text: qsTr("Delete TProxy")
textColor: AmneziaStyle.color.vibrantRed
clickedFunction: function () {
var headerText = qsTr("Remove %1 from server?").arg(ContainersModel.getProcessedContainerName())
var yesButtonFunction = function () {
PageController.goToPage(PageEnum.PageDeinstalling)
InstallController.removeContainer(ServersUiController.processedServerId,
ServersUiController.processedContainerIndex)
}
showQuestionDrawer(headerText,
qsTr("The proxy will be stopped and all users will lose access."),
qsTr("Continue"), qsTr("Cancel"), yesButtonFunction, function () {})
}
}
}
}
ListViewType {
id: settingsListView
model: TProxyConfigModel
delegate: ColumnLayout {
width: settingsListView.width
spacing: 0
readonly property bool fieldsEditable: isEnabled && containerStatus === 1 && !root.pageBusy
SwitcherType {
Layout.fillWidth: true
Layout.topMargin: 24
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 16
text: qsTr("Enable TProxy")
checked: isEnabled
enabled: containerStatus !== 0 && containerStatus !== 3 && !root.pageBusy && !root.tproxyNetworkBlocked
onToggled: function () {
if (checked !== isEnabled) {
previousEnabled = isEnabled
previousContainerStatus = containerStatus
root.previousSecret = secret
isEnabled = checked
isUpdating = true
if (checked) {
root.pendingUpdateAfterEnable = true
InstallController.setContainerEnabled(ServersUiController.processedServerId,
ServersUiController.processedContainerIndex, true)
} else {
InstallController.setContainerEnabled(ServersUiController.processedServerId,
ServersUiController.processedContainerIndex, false)
}
}
}
}
CaptionTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 8
visible: !fieldsEditable && !root.pageBusy
text: (containerStatus === 1 || containerStatus === 2)
? qsTr("Enable TProxy to edit settings")
: (statusErrorCode !== 0
? qsTr("Settings locked: connection timed out (error code %1). Re-open the page to retry.").arg(statusErrorCode)
: qsTr("Cannot reach the server — settings are unavailable"))
color: AmneziaStyle.color.mutedGray
wrapMode: Text.WordWrap
}
TextFieldWithHeaderType {
id: hostnameTextField
enabled: fieldsEditable
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 8
headerText: qsTr("Hostname")
textField.placeholderText: qsTr("proxy.example.com")
textField.text: hostname
textField.onTextChanged: {
var cur = hostnameTextField.textField.text
var clean = TProxyConfigModel.sanitizeHostnameFieldText(cur)
if (clean !== cur) {
textField.text = clean
textField.cursorPosition = clean.length
return
}
if (TProxyConfigModel.isHostnameTypingIncomplete(cur)) {
hostnameTextField.errorText = ""
return
}
if (!TProxyConfigModel.isValidHostname(clean)) {
hostnameTextField.errorText = qsTr("Use lowercase letters, digits, dots and hyphens")
return
}
hostnameTextField.errorText = ""
}
textField.onEditingFinished: {
var h = TProxyConfigModel.sanitizeHostnameFieldText(textField.text)
textField.text = h
if (!TProxyConfigModel.isValidHostname(h)) {
hostnameTextField.errorText = qsTr("Enter a lowercase DNS hostname (A record to this server)")
return
}
hostnameTextField.errorText = ""
TProxyConfigModel.setHostname(h)
}
}
CaptionTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 16
text: qsTr("Required. Point a DNS A record at this server. Do not put a CDN in front.")
color: AmneziaStyle.color.mutedGray
wrapMode: Text.WordWrap
font.pixelSize: 12
}
TextFieldWithHeaderType {
id: emailTextField
enabled: fieldsEditable
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 8
headerText: qsTr("ACME email")
textField.placeholderText: qsTr("you@example.com")
textField.text: acmeEmail
textField.onTextChanged: {
var cur = emailTextField.textField.text
var clean = TProxyConfigModel.sanitizeAcmeEmailFieldText(cur)
if (clean !== cur) {
textField.text = clean
textField.cursorPosition = clean.length
return
}
if (TProxyConfigModel.isAcmeEmailTypingIncomplete(cur)) {
emailTextField.errorText = ""
return
}
if (!TProxyConfigModel.isValidAcmeEmail(clean)) {
emailTextField.errorText = qsTr("Enter a valid email for the TLS certificate")
return
}
emailTextField.errorText = ""
}
textField.onEditingFinished: {
var e = TProxyConfigModel.sanitizeAcmeEmailFieldText(textField.text)
textField.text = e
if (!TProxyConfigModel.isValidAcmeEmail(e)) {
emailTextField.errorText = qsTr("Enter a valid email for the TLS certificate")
return
}
emailTextField.errorText = ""
TProxyConfigModel.setAcmeEmail(e)
}
}
CaptionTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 16
text: qsTr("Used by Caddy to issue a Let's Encrypt certificate.")
color: AmneziaStyle.color.mutedGray
wrapMode: Text.WordWrap
font.pixelSize: 12
}
TextFieldWithHeaderType {
id: httpsPortTextField
enabled: fieldsEditable
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 8
headerText: qsTr("HTTPS port")
textField.placeholderText: TProxyConfigModel.defaultPort()
textField.maximumLength: 5
textField.inputMethodHints: Qt.ImhDigitsOnly
textField.validator: IntValidator { bottom: 1; top: 65535 }
Component.onCompleted: {
var savedPort = port
textField.text = (savedPort === TProxyConfigModel.defaultPort()) ? "" : savedPort
}
textField.onTextChanged: {
var cur = httpsPortTextField.textField.text
var clean = TProxyConfigModel.sanitizePortFieldText(cur)
if (clean !== cur) {
textField.text = clean
textField.cursorPosition = clean.length
}
}
textField.onEditingFinished: {
textField.text = TProxyConfigModel.sanitizePortFieldText(textField.text)
if (textField.text !== "" && !httpsPortTextField.textField.acceptableInput) {
httpsPortTextField.errorText = qsTr("The port must be in the range of 1 to 65535")
} else {
httpsPortTextField.errorText = ""
}
}
}
CaptionTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 16
text: qsTr("Host ports are mapped to 80 and 443 inside the container. Include the HTTPS port in the Telegram WEB proxy link. Port 80 must stay reachable from the internet for the ACME (Let's Encrypt) certificate.")
color: AmneziaStyle.color.mutedGray
wrapMode: Text.WordWrap
font.pixelSize: 12
}
CaptionTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
text: qsTr("Carrier mode")
color: AmneziaStyle.color.mutedGray
font.pixelSize: 12
}
DropDownType {
id: carrierDropDown
enabled: fieldsEditable
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 16
drawerParent: root
drawerHeight: 0.45
headerText: qsTr("Carrier mode")
text: TProxyConfigModel.carrierModeLabel(carrierMode)
listView: Component {
ListViewType {
model: ["https", "https-lanes", "websocket", "websocket-lanes"]
delegate: LabelWithButtonType {
Layout.fillWidth: true
text: TProxyConfigModel.carrierModeLabel(modelData)
rightImageSource: modelData === carrierMode ? "qrc:/images/controls/check.svg" : ""
rightImageColor: AmneziaStyle.color.goldenApricot
clickedFunction: function () {
TProxyConfigModel.setCarrierMode(modelData)
carrierDropDown.closeTriggered()
}
}
}
}
}
TextFieldWithHeaderType {
id: workersTextField
enabled: fieldsEditable
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 16
headerText: qsTr("MTProxy workers")
textField.placeholderText: TProxyConfigModel.defaultWorkers()
textField.text: workers
textField.maximumLength: 2
textField.inputMethodHints: Qt.ImhDigitsOnly
textField.validator: IntValidator {
bottom: 1
top: TProxyConfigModel.maxWorkers()
}
textField.onTextChanged: {
var cur = workersTextField.textField.text
if (cur === "") {
return
}
var n = parseInt(cur, 10)
var maxW = TProxyConfigModel.maxWorkers()
if (isNaN(n) || n < 1) {
n = 1
}
if (n > maxW) {
n = maxW
}
var clamped = String(n)
if (clamped !== cur) {
textField.text = clamped
textField.cursorPosition = clamped.length
}
}
textField.onEditingFinished: {
var w = TProxyConfigModel.sanitizeWorkersFieldText(textField.text)
textField.text = w
TProxyConfigModel.setWorkers(w)
}
}
BasicButtonType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 32
text: qsTr("Save")
enabled: fieldsEditable && !root.tproxyNetworkBlocked
clickedFunc: function () {
hostnameTextField.errorText = ""
emailTextField.errorText = ""
httpsPortTextField.errorText = ""
var h = TProxyConfigModel.sanitizeHostnameFieldText(hostnameTextField.textField.text)
var e = TProxyConfigModel.sanitizeAcmeEmailFieldText(emailTextField.textField.text)
hostnameTextField.textField.text = h
emailTextField.textField.text = e
httpsPortTextField.textField.text =
TProxyConfigModel.sanitizePortFieldText(httpsPortTextField.textField.text)
var hasError = false
var portErr = qsTr("The port must be in the range of 1 to 65535")
var httpsPortValue = httpsPortTextField.textField.text === ""
? TProxyConfigModel.defaultPort()
: httpsPortTextField.textField.text
// HTTP port (ACME) is fixed to the default (80): Let's Encrypt HTTP-01
// always hits :80, so it is not user-editable here.
var httpPortValue = TProxyConfigModel.defaultHttpPort()
if (!TProxyConfigModel.isValidHostname(h)) {
hostnameTextField.errorText =
qsTr("Enter a lowercase DNS hostname (A record to this server)")
hasError = true
}
if (!TProxyConfigModel.isValidAcmeEmail(e)) {
emailTextField.errorText = qsTr("Enter a valid email for the TLS certificate")
hasError = true
}
if (httpsPortTextField.textField.text !== ""
&& !httpsPortTextField.textField.acceptableInput) {
httpsPortTextField.errorText = portErr
hasError = true
}
if (hasError) {
return
}
previousHostname = TProxyConfigModel.getHostname()
previousEmail = TProxyConfigModel.getAcmeEmail()
previousSecret = secret
TProxyConfigModel.setHostname(h)
TProxyConfigModel.setAcmeEmail(e)
TProxyConfigModel.setPort(httpsPortValue)
TProxyConfigModel.setHttpPort(httpPortValue)
TProxyConfigModel.setWorkers(TProxyConfigModel.sanitizeWorkersFieldText(workersTextField.textField.text))
isUpdating = true
root.scheduleUpdate()
}
}
}
}
}
}
}

View File

@@ -216,6 +216,80 @@ PageType {
textField.validator: IntValidator { bottom: 1; top: 65535 }
}
TextFieldWithHeaderType {
id: tproxyHostname
visible: isTProxy
Layout.fillWidth: true
Layout.topMargin: 16
Layout.rightMargin: 16
Layout.leftMargin: 16
headerText: qsTr("Hostname")
textField.placeholderText: qsTr("proxy.example.com")
textField.onTextChanged: {
var cur = tproxyHostname.textField.text
var clean = TProxyConfigModel.sanitizeHostnameFieldText(cur)
if (clean !== cur) {
textField.text = clean
textField.cursorPosition = clean.length
return
}
if (TProxyConfigModel.isHostnameTypingIncomplete(cur)) {
tproxyHostname.errorText = ""
return
}
if (!TProxyConfigModel.isValidHostname(clean)) {
tproxyHostname.errorText = qsTr("Use lowercase letters, digits, dots and hyphens")
return
}
tproxyHostname.errorText = ""
}
}
TextFieldWithHeaderType {
id: tproxyEmail
visible: isTProxy
Layout.fillWidth: true
Layout.topMargin: 16
Layout.rightMargin: 16
Layout.leftMargin: 16
headerText: qsTr("ACME email")
textField.placeholderText: qsTr("you@example.com")
textField.onTextChanged: {
var cur = tproxyEmail.textField.text
var clean = TProxyConfigModel.sanitizeAcmeEmailFieldText(cur)
if (clean !== cur) {
textField.text = clean
textField.cursorPosition = clean.length
return
}
if (TProxyConfigModel.isAcmeEmailTypingIncomplete(cur)) {
tproxyEmail.errorText = ""
return
}
if (!TProxyConfigModel.isValidAcmeEmail(clean)) {
tproxyEmail.errorText = qsTr("Enter a valid email for the TLS certificate")
return
}
tproxyEmail.errorText = ""
}
}
CaptionTextType {
visible: isTProxy
Layout.fillWidth: true
Layout.topMargin: 8
Layout.rightMargin: 16
Layout.leftMargin: 16
text: qsTr("Needs a DNS A record and free host ports for HTTP (ACME) and HTTPS. Classic Telegram MTProxy links will not work.")
color: AmneziaStyle.color.mutedGray
wrapMode: Text.WordWrap
font.pixelSize: 12
}
Rectangle {
Layout.fillHeight: true
Layout.rightMargin: 16
@@ -235,7 +309,47 @@ PageType {
text: qsTr("Install")
clickedFunc: function() {
if (!port.textField.acceptableInput &&
if (isTProxy) {
tproxyHostname.errorText = ""
tproxyEmail.errorText = ""
port.errorText = ""
var host = TProxyConfigModel.sanitizeHostnameFieldText(tproxyHostname.textField.text)
var email = TProxyConfigModel.sanitizeAcmeEmailFieldText(tproxyEmail.textField.text)
tproxyHostname.textField.text = host
tproxyEmail.textField.text = email
port.textField.text = TProxyConfigModel.sanitizePortFieldText(port.textField.text)
var hasError = false
var portErr = qsTr("The port must be in the range of 1 to 65535")
// Empty port means "use the default" (443), consistent with the settings page.
var portValue = port.textField.text === ""
? TProxyConfigModel.defaultPort()
: port.textField.text
if (!TProxyConfigModel.isValidHostname(host)) {
tproxyHostname.errorText = qsTr("Enter a lowercase DNS hostname")
hasError = true
}
if (!TProxyConfigModel.isValidAcmeEmail(email)) {
tproxyEmail.errorText = qsTr("Enter a valid email for the TLS certificate")
hasError = true
}
if (port.textField.text !== "" && !port.textField.acceptableInput) {
port.errorText = portErr
hasError = true
}
if (hasError) {
return
}
port.textField.text = portValue
TProxyConfigModel.setHostname(host)
TProxyConfigModel.setAcmeEmail(email)
TProxyConfigModel.setPort(portValue)
TProxyConfigModel.setHttpPort(TProxyConfigModel.defaultHttpPort())
} else if (!port.textField.acceptableInput &&
ContainerProps.containerTypeToString(dockerContainer) !== "torwebsite" &&
ContainerProps.containerTypeToString(dockerContainer) !== "ikev2") {
port.errorText = qsTr("The port must be in the range of 1 to 65535")

View File

@@ -91,6 +91,7 @@
<file>Pages2/PageServiceDnsSettings.qml</file>
<file>Pages2/PageServiceMtProxySettings.qml</file>
<file>Pages2/PageServiceTelemtSettings.qml</file>
<file>Pages2/PageServiceTProxySettings.qml</file>
<file>Pages2/PageServiceSftpSettings.qml</file>
<file>Pages2/PageServiceSocksProxySettings.qml</file>
<file>Pages2/PageServiceTorWebsiteSettings.qml</file>

View File

@@ -47,6 +47,7 @@ namespace PageLoader
PageServiceSocksProxySettings,
PageServiceMtProxySettings,
PageServiceTelemtSettings,
PageServiceTProxySettings,
PageSetupWizardStart,
PageSetupWizardCredentials,