feat: local proxy mode (#3166)

* fix: share double-stop guard via VpnProtocol base class

* feat: add local proxy core module

* feat: local proxy settings api

* feat: wire local proxy into app controllers

* feat: local proxy settings ui

* feat: arbitrate shared xray engine ownership via ipc tokens

* fix: remove connection type

* chore: add qthttpserver to desktop ci qt modules

* fix: local proxy port fallback and active port reporting

* fix: keep text field button aligned when error is shown

* fix: hide local proxy for free servers and guard device unlink

* fix: show local proxy state per server

* fix: anchor text field button to content

* fix: adapt local proxy pages to design

* fix: copy logic and port logic

* refactor: move local proxy files to controllers and utils

* refactor: merge local proxy into LocalProxyController

* refactor: reuse gateway config request and keep local proxy state in memory

---------

Co-authored-by: vkamn <vk@amnezia.org>
This commit is contained in:
NickVs2015andvkamn authored and GitHub committed 2026-10-18 21:25:04 +08:00
1 parent ffba1cb2a4
commit ddde43635e
39 files changed
+1534 -15

No files matched your search

+4 -4
View File
@@ -88,7 +88,7 @@ jobs:
host: 'linux'
target: 'desktop'
arch: 'linux_gcc_64'
modules: 'qtremoteobjects qt5compat qtshadertools'
modules: 'qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets'
dir: ${{ runner.temp }}
setup-python: 'true'
tools: 'tools_ifw'
@@ -209,7 +209,7 @@ jobs:
host: 'windows'
target: 'desktop'
arch: 'win64_msvc2022_64'
modules: 'qtremoteobjects qt5compat qtshadertools'
modules: 'qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets'
dir: ${{ runner.temp }}
setup-python: 'true'
tools: 'tools_ifw'
@@ -528,7 +528,7 @@ jobs:
host: 'mac'
target: 'desktop'
arch: 'clang_64'
modules: 'qtremoteobjects qt5compat qtshadertools'
modules: 'qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets'
dir: ${{ runner.temp }}
setup-python: 'true'
set-env: 'true'
@@ -671,7 +671,7 @@ jobs:
version: ${{ env.QT_VERSION }}
host: 'mac'
target: 'desktop'
modules: 'qtremoteobjects qt5compat qtshadertools qtmultimedia'
modules: 'qtremoteobjects qt5compat qtshadertools qtmultimedia qthttpserver qtwebsockets'
arch: 'clang_64'
dir: ${{ runner.temp }}
set-env: 'true'
+2 -2
View File
@@ -37,7 +37,7 @@ add_definitions(-DFREE_V2_ENDPOINT="$ENV{FREE_V2_ENDPOINT}")
add_definitions(-DPREM_V1_ENDPOINT="$ENV{PREM_V1_ENDPOINT}")
if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID))
set(PACKAGES ${PACKAGES} Widgets)
set(PACKAGES ${PACKAGES} Widgets HttpServer)
endif()
find_package(Qt6 REQUIRED COMPONENTS ${PACKAGES})
@@ -60,7 +60,7 @@ if(TARGET Qt6::CorePrivate)
endif()
if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID))
set(LIBS ${LIBS} Qt6::Widgets)
set(LIBS ${LIBS} Qt6::Widgets Qt6::HttpServer)
endif()
qt_standard_project_setup()
+7
View File
@@ -44,6 +44,7 @@ set(HEADERS ${HEADERS}
${CLIENT_ROOT_DIR}/core/controllers/selfhosted/exportController.h
${CLIENT_ROOT_DIR}/core/controllers/connectionController.h
${CLIENT_ROOT_DIR}/core/controllers/settingsController.h
${CLIENT_ROOT_DIR}/core/controllers/localProxyController.h
${CLIENT_ROOT_DIR}/core/controllers/logsController.h
${CLIENT_ROOT_DIR}/core/controllers/api/servicesCatalogController.h
${CLIENT_ROOT_DIR}/core/controllers/api/storePurchaseController.h
@@ -69,6 +70,9 @@ set(HEADERS ${HEADERS}
${CLIENT_ROOT_DIR}/core/utils/api/apiUtils.h
${CLIENT_ROOT_DIR}/core/utils/api/countryCatalog.h
${CLIENT_ROOT_DIR}/core/utils/api/gatewayPayloadBuilder.h
${CLIENT_ROOT_DIR}/core/utils/localProxy/localProxyConstants.h
${CLIENT_ROOT_DIR}/core/utils/localProxy/portUtils.h
${CLIENT_ROOT_DIR}/core/utils/localProxy/xrayConfigUtils.h
${CLIENT_ROOT_DIR}/core/utils/osSignalHandler.h
${CLIENT_ROOT_DIR}/core/utils/swiftBridge.h
${CLIENT_ROOT_DIR}/core/utils/utilities.h
@@ -130,6 +134,7 @@ set(SOURCES ${SOURCES}
${CLIENT_ROOT_DIR}/core/controllers/selfhosted/exportController.cpp
${CLIENT_ROOT_DIR}/core/controllers/connectionController.cpp
${CLIENT_ROOT_DIR}/core/controllers/settingsController.cpp
${CLIENT_ROOT_DIR}/core/controllers/localProxyController.cpp
${CLIENT_ROOT_DIR}/core/controllers/logsController.cpp
${CLIENT_ROOT_DIR}/core/controllers/api/servicesCatalogController.cpp
${CLIENT_ROOT_DIR}/core/controllers/api/storePurchaseController.cpp
@@ -157,6 +162,8 @@ set(SOURCES ${SOURCES}
${CLIENT_ROOT_DIR}/core/utils/api/apiUtils.cpp
${CLIENT_ROOT_DIR}/core/utils/api/countryCatalog.cpp
${CLIENT_ROOT_DIR}/core/utils/api/gatewayPayloadBuilder.cpp
${CLIENT_ROOT_DIR}/core/utils/localProxy/portUtils.cpp
${CLIENT_ROOT_DIR}/core/utils/localProxy/xrayConfigUtils.cpp
${CLIENT_ROOT_DIR}/core/utils/serverConfigUtils.cpp
${CLIENT_ROOT_DIR}/core/utils/osSignalHandler.cpp
${CLIENT_ROOT_DIR}/core/utils/utilities.cpp
@@ -362,6 +362,33 @@ ErrorCode SubscriptionController::updateServiceFromGateway(const QString &server
return applyUpdatedServiceConfig(serverId, serviceProtocol, protocolData, responseBody);
}
ErrorCode SubscriptionController::fetchServiceConfig(const QString &serverId, const QString &serviceProtocol,
QJsonObject &serverConfigJson)
{
auto apiV2 = m_serversRepository->apiV2Config(serverId);
if (!apiV2.has_value()) {
return ErrorCode::InternalError;
}
ProtocolData protocolData = generateProtocolData(serviceProtocol);
QJsonObject apiPayload = GatewayPayloadBuilder(m_appSettingsRepository)
.addField(apiDefs::key::userCountryCode, apiV2->apiConfig.userCountryCode)
.addField(apiDefs::key::serviceType, apiV2->serviceType())
.addField(apiDefs::key::serviceProtocol, serviceProtocol)
.addField(apiDefs::key::publicKey, publicKeyForProtocol(serviceProtocol, protocolData))
.addField(apiDefs::key::authData, apiV2->authData.toJson())
.build();
QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody, apiV2->apiConfig.isTestPurchase);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
return extractServerConfigJsonFromResponse(responseBody, serviceProtocol, protocolData, serverConfigJson);
}
void SubscriptionController::markSubscriptionExpiredByServer(const QString &serverId, const ApiV2ServerConfig &apiV2)
{
ApiV2ServerConfig expiredApiV2 = apiV2;
@@ -45,6 +45,8 @@ public:
ErrorCode importTrialFromGateway(const QString &userCountryCode, const QString &serviceType,
const QString &serviceProtocol, const QString &email);
ErrorCode fetchServiceConfig(const QString &serverId, const QString &serviceProtocol, QJsonObject &serverConfigJson);
ErrorCode updateServiceFromGateway(const QString &serverId, const QString &newCountryCode, bool isConnectEvent,
CaptchaInfo *captchaInfoOut = nullptr, ProtocolData *usedProtocolDataOut = nullptr);
@@ -179,6 +179,7 @@ void CoreController::initCoreControllers()
m_importCoreController = new ImportController(m_serversRepository, m_appSettingsRepository, this);
m_connectionController = new ConnectionController(m_serversRepository, m_appSettingsRepository, m_vpnConnection.get(), this);
m_settingsController = new SettingsController(m_serversRepository, m_appSettingsRepository, this);
m_localProxyController = new LocalProxyController(m_serversRepository, m_appSettingsRepository, m_subscriptionController, this);
}
void CoreController::initControllers()
@@ -212,6 +213,9 @@ void CoreController::initControllers()
m_settingsUiController = new SettingsUiController(m_settingsController, m_serversController, this);
setQmlContextProperty("SettingsController", m_settingsUiController);
m_localProxyUiController = new LocalProxyUiController(m_localProxyController, this);
setQmlContextProperty("LocalProxyController", m_localProxyUiController);
m_logsUiController = new LogsUiController(m_settingsController, this);
setQmlContextProperty("LogsController", m_logsUiController);
+4
View File
@@ -24,6 +24,7 @@
#include "ui/controllers/selfhosted/installUiController.h"
#include "ui/controllers/qml/pageController.h"
#include "ui/controllers/settingsUiController.h"
#include "ui/controllers/localProxyUiController.h"
#include "ui/controllers/logsUiController.h"
#include "ui/controllers/serversUiController.h"
#include "ui/controllers/ipSplitTunnelingUiController.h"
@@ -44,6 +45,7 @@
#include "core/controllers/api/newsController.h"
#include "core/controllers/selfhosted/installController.h"
#include "core/controllers/settingsController.h"
#include "core/controllers/localProxyController.h"
#include "core/controllers/connectionController.h"
#include "core/controllers/updateController.h"
@@ -175,6 +177,7 @@ private:
ImportController* m_importCoreController;
ExportUiController* m_exportUiController;
SettingsUiController* m_settingsUiController;
LocalProxyUiController* m_localProxyUiController;
LogsUiController* m_logsUiController;
ServersUiController* m_serversUiController;
IpSplitTunnelingUiController* m_ipSplitTunnelingUiController;
@@ -204,6 +207,7 @@ private:
ExportController* m_exportController;
ConnectionController* m_connectionController;
SettingsController* m_settingsController;
LocalProxyController* m_localProxyController;
ContainersModel* m_containersModel;
ContainersModel* m_defaultServerContainersModel;
@@ -1,5 +1,6 @@
#include "coreSignalHandlers.h"
#include <QCoreApplication>
#include <QTimer>
#include <QtConcurrent>
@@ -32,6 +33,7 @@
#include "core/controllers/selfhosted/installController.h"
#include "core/controllers/selfhosted/exportController.h"
#include "core/controllers/connectionController.h"
#include "core/controllers/localProxyController.h"
#include "ui/models/clientManagementModel.h"
#include "ui/controllers/api/apiNewsUiController.h"
#include "ui/models/containersModel.h"
@@ -88,6 +90,7 @@ void CoreSignalHandlers::initAllHandlers()
initIosSettingsHandler();
initNotificationHandler();
initUpdateFoundHandler();
initLocalProxyHandler();
}
void CoreSignalHandlers::initErrorMessagesHandler()
@@ -494,3 +497,12 @@ void CoreSignalHandlers::initUpdateFoundHandler()
});
}
void CoreSignalHandlers::initLocalProxyHandler()
{
connect(m_coreController->m_connectionController, &ConnectionController::openConnectionRequested, m_coreController->m_localProxyController,
&LocalProxyController::onVpnConnectionRequested);
connect(m_coreController->m_localProxyController, &LocalProxyController::stoppedByVpn, m_coreController->m_pageController,
&PageController::showNotificationMessage);
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, m_coreController->m_localProxyController,
&LocalProxyController::disable);
}
@@ -43,6 +43,7 @@ private:
void initIosSettingsHandler();
void initNotificationHandler();
void initUpdateFoundHandler();
void initLocalProxyHandler();
CoreController* m_coreController;
};
@@ -0,0 +1,525 @@
#include "localProxyController.h"
#include "core/controllers/api/subscriptionController.h"
#include "core/repositories/secureAppSettingsRepository.h"
#include "core/repositories/secureServersRepository.h"
#include "core/utils/api/apiUtils.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/localProxy/localProxyConstants.h"
#include "core/utils/localProxy/portUtils.h"
#include "core/utils/localProxy/xrayConfigUtils.h"
#include <QDebug>
#include <QJsonDocument>
#include <QJsonParseError>
#ifdef AMNEZIA_DESKTOP
#include "core/utils/ipcClient.h"
#include <QHostAddress>
#include <QHttpServer>
#include <QHttpServerResponse>
#include <QTcpServer>
#endif
using namespace amnezia;
LocalProxyController::LocalProxyController(SecureServersRepository *serversRepository,
SecureAppSettingsRepository *appSettingsRepository,
SubscriptionController *subscriptionController, QObject *parent)
: QObject(parent),
m_serversRepository(serversRepository),
m_appSettingsRepository(appSettingsRepository),
m_subscriptionController(subscriptionController)
{
connect(m_serversRepository, &SecureServersRepository::serverEdited, this, &LocalProxyController::onServerEdited);
connect(m_serversRepository, &SecureServersRepository::serverRemoved, this,
[this](const QString &serverId, int) { onServerRemoved(serverId); });
}
LocalProxyController::~LocalProxyController()
{
stop();
}
bool LocalProxyController::isEnabled() const
{
return m_isEnabled;
}
int LocalProxyController::port() const
{
return m_appSettingsRepository->localProxyPort();
}
QString LocalProxyController::ownerId() const
{
return m_appSettingsRepository->localProxyOwnerId();
}
bool LocalProxyController::isPortUserDefined() const
{
return m_appSettingsRepository->isLocalProxyPortUserDefined();
}
int LocalProxyController::activePort() const
{
return m_activePort;
}
bool LocalProxyController::setPort(int port)
{
if (port < localProxy::proxyPortMin || port > localProxy::proxyPortMax) {
return false;
}
const bool isPortChanged = m_appSettingsRepository->localProxyPort() != static_cast<quint16>(port);
if (isPortChanged) {
m_appSettingsRepository->setLocalProxyPort(static_cast<quint16>(port));
}
m_appSettingsRepository->setLocalProxyPortUserDefined(true);
emit stateChanged();
if (isPortChanged && m_isEnabled) {
start();
}
return true;
}
bool LocalProxyController::isPortBusy(int port) const
{
return !portUtils::isPortAvailable(port);
}
bool LocalProxyController::enable(const QString &ownerId, int port)
{
if (!isSupported() || ownerId.isEmpty() || port < localProxy::proxyPortMin || port > localProxy::proxyPortMax) {
return false;
}
if (m_isEnabled && m_appSettingsRepository->localProxyOwnerId() != ownerId) {
return false;
}
if (!portUtils::isPortAvailable(port) && m_appSettingsRepository->isLocalProxyPortUserDefined()) {
return false;
}
if (m_appSettingsRepository->localProxyPort() != static_cast<quint16>(port)) {
m_appSettingsRepository->setLocalProxyPort(static_cast<quint16>(port));
}
m_appSettingsRepository->setLocalProxyOwnerId(ownerId);
m_isEnabled = true;
emit stateChanged();
start();
return true;
}
void LocalProxyController::disable()
{
if (!m_isEnabled) {
return;
}
qDebug() << "Local proxy is disabled";
m_isEnabled = false;
stop();
emit stateChanged();
}
void LocalProxyController::start()
{
if (!startApi(localProxy::apiPort)) {
disableWithError(tr("Local proxy failed to start. Check if the port is available"));
return;
}
if (!syncXray()) {
disableWithError(tr("Couldn’t start the proxy due to an internal error. Try restarting the app"));
return;
}
qDebug() << "Local proxy is running on 127.0.0.1:" << m_apiPort;
}
void LocalProxyController::onServerEdited(const QString &serverId)
{
if (m_apiPort == 0 || m_appSettingsRepository->localProxyOwnerId() != serverId) {
return;
}
qDebug() << "Owner server edited, restarting Xray";
if (restartXray()) {
m_appliedProxyPort = m_appSettingsRepository->localProxyPort();
} else {
disableWithError(tr("Couldn’t start the proxy due to an internal error. Try restarting the app"));
}
}
void LocalProxyController::onServerRemoved(const QString &serverId)
{
if (m_appSettingsRepository->localProxyOwnerId() != serverId) {
return;
}
m_appSettingsRepository->setLocalProxyOwnerId(QString());
if (m_isEnabled) {
m_isEnabled = false;
stop();
}
emit stateChanged();
}
void LocalProxyController::onVpnConnectionRequested()
{
if (!m_isEnabled) {
return;
}
disable();
emit stoppedByVpn(tr("Local proxy stopped because VPN was turned on"));
}
void LocalProxyController::stop()
{
stopXray();
stopApi();
m_appliedProxyPort = 0;
}
bool LocalProxyController::syncXray()
{
const quint16 proxyPort = m_appSettingsRepository->localProxyPort();
bool synced;
if (!isXrayEngineRunning()) {
synced = startXray();
} else if (m_appliedProxyPort != proxyPort) {
qDebug() << "Proxy port changed from" << m_appliedProxyPort << "to" << proxyPort;
synced = restartXray();
} else {
return true;
}
if (synced) {
m_appliedProxyPort = proxyPort;
}
return synced;
}
bool LocalProxyController::startXray()
{
if (isXrayEngineRunning()) {
return true;
}
const auto config = buildXrayConfig();
if (!config) {
return false;
}
if (!startXrayEngine(config->serializedConfig)) {
return false;
}
qDebug() << "Xray started on port" << config->proxyPort;
setActivePort(config->proxyPort);
return true;
}
bool LocalProxyController::stopXray()
{
if (!stopXrayEngine()) {
return false;
}
setActivePort(0);
return true;
}
bool LocalProxyController::restartXray()
{
qDebug() << "Restarting Xray with updated config";
if (!stopXray()) {
return false;
}
return startXray();
}
void LocalProxyController::disableWithError(const QString &message)
{
qWarning() << message;
m_isEnabled = false;
stop();
emit stateChanged();
emit startFailed(message);
}
void LocalProxyController::setActivePort(int port)
{
if (m_activePort == port) {
return;
}
m_activePort = port;
emit stateChanged();
}
int LocalProxyController::runningProxyPort() const
{
return isXrayEngineRunning() ? m_activePort : 0;
}
std::optional<LocalProxyController::XrayConfig> LocalProxyController::buildXrayConfig() const
{
const QString ownerId = m_appSettingsRepository->localProxyOwnerId();
if (ownerId.isEmpty()) {
qWarning() << "Local proxy owner server id is not configured";
return std::nullopt;
}
const auto ownerServer = m_serversRepository->serverJsonById(ownerId);
if (!ownerServer) {
qWarning() << "Owner server with id" << ownerId << "not found";
return std::nullopt;
}
if (!apiUtils::isPremiumServer(*ownerServer)) {
qWarning() << "Server" << ownerServer->value(configKey::name).toString() << "is not premium, local proxy is unavailable";
return std::nullopt;
}
auto serializedConfig = xrayConfigUtils::extractLastConfig(*ownerServer);
if (!serializedConfig) {
serializedConfig = fetchXrayConfigFromGateway(ownerId);
if (!serializedConfig) {
return std::nullopt;
}
}
QJsonParseError parseError;
const QJsonDocument doc = QJsonDocument::fromJson(serializedConfig->toUtf8(), &parseError);
if (parseError.error != QJsonParseError::NoError || !doc.isObject()) {
qWarning() << "Failed to parse Xray config JSON:" << parseError.errorString();
return std::nullopt;
}
const auto proxyPort = selectProxyPort();
if (!proxyPort) {
return std::nullopt;
}
QJsonObject xrayConfig = doc.object();
if (!xrayConfigUtils::setInboundPort(xrayConfig, *proxyPort)) {
qWarning() << "Failed to override local proxy inbound port, using original config";
return XrayConfig { *serializedConfig, 0 };
}
return XrayConfig { QString::fromUtf8(QJsonDocument(xrayConfig).toJson(QJsonDocument::Compact)), *proxyPort };
}
std::optional<int> LocalProxyController::selectProxyPort() const
{
int port = m_appSettingsRepository->localProxyPort();
if (port < localProxy::proxyPortMin || port > localProxy::proxyPortMax) {
port = localProxy::defaultProxyPort;
}
if (portUtils::waitForPort(port, localProxy::portReleaseWaitMs)) {
return port;
}
if (m_appSettingsRepository->isLocalProxyPortUserDefined()) {
qWarning() << "Local proxy port" << port << "is already in use";
return std::nullopt;
}
std::optional<int> freePort;
if (port != localProxy::defaultProxyPort && portUtils::isPortAvailable(localProxy::defaultProxyPort)) {
freePort = localProxy::defaultProxyPort;
} else {
freePort = portUtils::findFirstAvailablePort(localProxy::defaultProxyPort + 1, localProxy::proxyPortMax);
}
if (!freePort) {
qWarning() << "No available local proxy port in range" << localProxy::defaultProxyPort + 1 << "-"
<< localProxy::proxyPortMax;
return std::nullopt;
}
qDebug() << "Local proxy port" << port << "is busy, using" << *freePort;
return freePort;
}
std::optional<QString> LocalProxyController::fetchXrayConfigFromGateway(const QString &serverId) const
{
QJsonObject serverConfig;
const ErrorCode errorCode = m_subscriptionController->fetchServiceConfig(serverId, configKey::vless, serverConfig);
if (errorCode != ErrorCode::NoError) {
qWarning() << "Gateway request failed with error code" << static_cast<int>(errorCode);
return std::nullopt;
}
const auto serializedConfig = xrayConfigUtils::extractLastConfig(serverConfig);
if (!serializedConfig) {
qWarning() << "Gateway response lacks Xray last_config payload";
return std::nullopt;
}
qDebug() << "Fetched Xray config from gateway";
return serializedConfig;
}
#ifdef AMNEZIA_DESKTOP
bool LocalProxyController::isSupported() const
{
return true;
}
bool LocalProxyController::startApi(quint16 port)
{
if (m_httpServer && m_apiPort == port) {
return true;
}
stopApi();
const auto proxyPortValue = [this]() {
const int port = runningProxyPort();
return port > 0 ? QJsonValue(port) : QJsonValue::Null;
};
auto httpServer = new QHttpServer(this);
httpServer->route("/api/v1/up", QHttpServerRequest::Method::Post, [this, proxyPortValue] {
const bool started = startXray();
return QHttpServerResponse(QJsonObject { { "status", started ? "ok" : "error" }, { "proxyPort", proxyPortValue() } });
});
httpServer->route("/api/v1/down", QHttpServerRequest::Method::Post, [this] {
const bool stopped = stopXray();
return QHttpServerResponse(QJsonObject { { "status", stopped ? "ok" : "error" } });
});
httpServer->route("/api/v1/ping", QHttpServerRequest::Method::Get, [proxyPortValue] {
return QHttpServerResponse(QJsonObject { { "status", "ok" }, { "proxyPort", proxyPortValue() } });
});
auto tcpServer = new QTcpServer(httpServer);
if (!tcpServer->listen(QHostAddress::LocalHost, port) || !httpServer->bind(tcpServer)) {
qWarning() << "Failed to start HTTP API server on port" << port;
delete httpServer;
return false;
}
m_httpServer = httpServer;
m_apiPort = port;
qDebug() << "HTTP API server is running on localhost:" << port;
return true;
}
void LocalProxyController::stopApi()
{
delete m_httpServer;
m_httpServer = nullptr;
m_apiPort = 0;
}
bool LocalProxyController::startXrayEngine(const QString &config)
{
if (config.trimmed().isEmpty()) {
qWarning() << "Xray config content is empty";
return false;
}
const qint64 token = IpcClient::withInterface(
[&](QSharedPointer<IpcInterfaceReplica> iface) -> qint64 {
auto xrayStart = iface->xrayStartOwned(config);
if (!xrayStart.waitForFinished()) {
return 0;
}
return xrayStart.returnValue();
},
[]() -> qint64 { return 0; });
if (token == 0) {
qWarning() << "Failed to start Xray via IPC";
return false;
}
m_xrayToken = token;
return true;
}
bool LocalProxyController::stopXrayEngine()
{
if (m_xrayToken == 0) {
return true;
}
const bool stopped = IpcClient::withInterface(
[this](QSharedPointer<IpcInterfaceReplica> iface) {
auto xrayStop = iface->xrayStopOwned(m_xrayToken);
return xrayStop.waitForFinished() && xrayStop.returnValue();
},
[]() { return false; });
if (!stopped) {
qWarning() << "Failed to stop Xray via IPC";
return false;
}
m_xrayToken = 0;
qDebug() << "Xray stopped";
return true;
}
bool LocalProxyController::isXrayEngineRunning() const
{
if (m_xrayToken == 0) {
return false;
}
return IpcClient::withInterface(
[this](QSharedPointer<IpcInterfaceReplica> iface) {
auto currentToken = iface->xrayCurrentToken();
return currentToken.waitForFinished() && currentToken.returnValue() == m_xrayToken;
},
[]() { return false; });
}
#else
bool LocalProxyController::isSupported() const
{
return false;
}
bool LocalProxyController::startApi(quint16)
{
return false;
}
void LocalProxyController::stopApi()
{
}
bool LocalProxyController::startXrayEngine(const QString &)
{
return false;
}
bool LocalProxyController::stopXrayEngine()
{
return true;
}
bool LocalProxyController::isXrayEngineRunning() const
{
return false;
}
#endif
@@ -0,0 +1,84 @@
#ifndef LOCALPROXYCONTROLLER_H
#define LOCALPROXYCONTROLLER_H
#include <optional>
#include <QObject>
#include <QString>
class QHttpServer;
class SecureServersRepository;
class SecureAppSettingsRepository;
class SubscriptionController;
class LocalProxyController : public QObject
{
Q_OBJECT
public:
LocalProxyController(SecureServersRepository *serversRepository, SecureAppSettingsRepository *appSettingsRepository,
SubscriptionController *subscriptionController, QObject *parent = nullptr);
~LocalProxyController();
bool isSupported() const;
bool isEnabled() const;
int port() const;
QString ownerId() const;
bool isPortUserDefined() const;
int activePort() const;
bool setPort(int port);
bool isPortBusy(int port) const;
bool enable(const QString &ownerId, int port);
void disable();
public slots:
void onServerEdited(const QString &serverId);
void onServerRemoved(const QString &serverId);
void onVpnConnectionRequested();
signals:
void stateChanged();
void startFailed(const QString &message);
void stoppedByVpn(const QString &message);
private:
struct XrayConfig
{
QString serializedConfig;
int proxyPort = 0;
};
void start();
void stop();
bool syncXray();
bool startXray();
bool stopXray();
bool restartXray();
void disableWithError(const QString &message);
void setActivePort(int port);
int runningProxyPort() const;
std::optional<XrayConfig> buildXrayConfig() const;
std::optional<int> selectProxyPort() const;
std::optional<QString> fetchXrayConfigFromGateway(const QString &serverId) const;
bool startApi(quint16 port);
void stopApi();
bool startXrayEngine(const QString &config);
bool stopXrayEngine();
bool isXrayEngineRunning() const;
SecureServersRepository *m_serversRepository;
SecureAppSettingsRepository *m_appSettingsRepository;
SubscriptionController *m_subscriptionController;
bool m_isEnabled = false;
QHttpServer *m_httpServer = nullptr;
quint16 m_apiPort = 0;
qint64 m_xrayToken = 0;
quint16 m_appliedProxyPort = 0;
int m_activePort = 0;
};
#endif // LOCALPROXYCONTROLLER_H
@@ -61,6 +61,10 @@ void OpenVpnProtocol::cleanupResources()
void OpenVpnProtocol::stop()
{
if (guardStop()) {
return;
}
qDebug() << "OpenVpnProtocol::stop()";
const bool wasActive = m_connectionState == Vpn::ConnectionState::Preparing
@@ -179,6 +183,8 @@ void OpenVpnProtocol::updateRouteGateway(QString line)
ErrorCode OpenVpnProtocol::start()
{
resetStopGuard();
cleanupResources();
if (!QFileInfo::exists(configPath())) {
+14
View File
@@ -27,6 +27,20 @@ VpnProtocol::VpnProtocol(const QJsonObject &configuration, QObject *parent)
connect(m_timeoutTimer, &QTimer::timeout, this, &VpnProtocol::onTimeout);
}
bool VpnProtocol::guardStop()
{
if (m_stopped) {
return true;
}
m_stopped = true;
return false;
}
void VpnProtocol::resetStopGuard()
{
m_stopped = false;
}
void VpnProtocol::setLastError(ErrorCode lastError)
{
m_lastError = lastError;
+5
View File
@@ -89,6 +89,11 @@ protected:
void startTimeoutTimer();
void stopTimeoutTimer();
bool guardStop();
void resetStopGuard();
bool m_stopped = false;
Vpn::ConnectionState m_connectionState;
QString m_routeGateway;
+4 -1
View File
@@ -50,8 +50,10 @@ WireguardProtocol::~WireguardProtocol()
void WireguardProtocol::stop()
{
stopMzImpl();
if (guardStop()) {
return;
}
stopMzImpl();
}
ErrorCode WireguardProtocol::startMzImpl()
@@ -75,5 +77,6 @@ ErrorCode WireguardProtocol::stopMzImpl()
ErrorCode WireguardProtocol::start()
{
resetStopGuard();
return startMzImpl();
}
+12 -4
View File
@@ -67,6 +67,8 @@ XrayProtocol::~XrayProtocol()
ErrorCode XrayProtocol::start()
{
resetStopGuard();
qDebug() << "XrayProtocol::start()";
// Inject SOCKS5 auth into the inbound before starting xray.
@@ -92,11 +94,12 @@ ErrorCode XrayProtocol::start()
return IpcClient::withInterface(
[&](QSharedPointer<IpcInterfaceReplica> iface) {
auto xrayStart = iface->xrayStart(xrayConfigStr);
if (!xrayStart.waitForFinished() || !xrayStart.returnValue()) {
auto xrayStart = iface->xrayStartOwned(xrayConfigStr);
if (!xrayStart.waitForFinished() || xrayStart.returnValue() == 0) {
qCritical() << "Failed to start xray";
return ErrorCode::XrayExecutableCrashed;
}
m_xrayToken = xrayStart.returnValue();
return startTun2Socks();
},
[]() { return ErrorCode::AmneziaServiceConnectionFailed; });
@@ -104,9 +107,13 @@ ErrorCode XrayProtocol::start()
void XrayProtocol::stop()
{
if (guardStop()) {
return;
}
qDebug() << "XrayProtocol::stop()";
IpcClient::withInterface([](QSharedPointer<IpcInterfaceReplica> iface) {
IpcClient::withInterface([this](QSharedPointer<IpcInterfaceReplica> iface) {
auto disableKillSwitch = iface->disableKillSwitch();
if (!disableKillSwitch.waitForFinished() || !disableKillSwitch.returnValue())
qWarning() << "Failed to disable killswitch";
@@ -123,9 +130,10 @@ void XrayProtocol::stop()
if (!deleteTun.waitForFinished() || !deleteTun.returnValue())
qWarning() << "Failed to delete tun";
auto xrayStop = iface->xrayStop();
auto xrayStop = iface->xrayStopOwned(m_xrayToken);
if (!xrayStop.waitForFinished() || !xrayStop.returnValue())
qWarning() << "Failed to stop xray";
m_xrayToken = 0;
});
if (m_tun2socksProcess) {
+4
View File
@@ -38,6 +38,10 @@ private:
int m_tun2socksRetryCount = 0;
static constexpr int maxTun2SocksRetries = 5;
static constexpr int tun2socksRetryDelayMs = 400;
// Owner token of the shared xray engine; a late stop with a stale token
// won't kill an engine restarted by another consumer (e.g. the local proxy).
qint64 m_xrayToken = 0;
};
#endif // XRAYPROTOCOL_H
@@ -504,3 +504,33 @@ void SecureAppSettingsRepository::setXraySavedConfigs(const QByteArray &data)
{
setValue("Xray/savedConfigs", data);
}
QString SecureAppSettingsRepository::localProxyOwnerId() const
{
return value("Conf/localProxyOwnerId", "").toString();
}
void SecureAppSettingsRepository::setLocalProxyOwnerId(const QString &serverId)
{
setValue("Conf/localProxyOwnerId", serverId);
}
quint16 SecureAppSettingsRepository::localProxyPort() const
{
return static_cast<quint16>(value("Conf/localProxyPort", 10808).toUInt());
}
void SecureAppSettingsRepository::setLocalProxyPort(quint16 port)
{
setValue("Conf/localProxyPort", port);
}
bool SecureAppSettingsRepository::isLocalProxyPortUserDefined() const
{
return value("Conf/localProxyPortUserDefined", false).toBool();
}
void SecureAppSettingsRepository::setLocalProxyPortUserDefined(bool userDefined)
{
setValue("Conf/localProxyPortUserDefined", userDefined);
}
@@ -105,6 +105,13 @@ public:
QByteArray xraySavedConfigs() const;
void setXraySavedConfigs(const QByteArray &data);
QString localProxyOwnerId() const;
void setLocalProxyOwnerId(const QString &serverId);
quint16 localProxyPort() const;
void setLocalProxyPort(quint16 port);
bool isLocalProxyPortUserDefined() const;
void setLocalProxyPortUserDefined(bool userDefined);
signals:
void appLanguageChanged(QLocale locale);
void allowedDnsServersChanged(const QStringList &servers);
@@ -323,6 +323,15 @@ serverConfigUtils::ConfigType SecureServersRepository::serverKind(const QString
return serverConfigUtils::configTypeFromJson(withoutStorageServerId(it.value()));
}
std::optional<QJsonObject> SecureServersRepository::serverJsonById(const QString &serverId) const
{
const auto it = m_serverJsonById.constFind(serverId);
if (it == m_serverJsonById.constEnd()) {
return std::nullopt;
}
return it.value();
}
std::optional<SelfHostedAdminServerConfig> SecureServersRepository::selfHostedAdminConfig(const QString &serverId) const
{
const auto it = m_serverJsonById.constFind(serverId);
@@ -31,6 +31,8 @@ public:
void removeServer(const QString &serverId);
serverConfigUtils::ConfigType serverKind(const QString &serverId) const;
std::optional<QJsonObject> serverJsonById(const QString &serverId) const;
std::optional<SelfHostedAdminServerConfig> selfHostedAdminConfig(const QString &serverId) const;
std::optional<SelfHostedUserServerConfig> selfHostedUserConfig(const QString &serverId) const;
std::optional<NativeServerConfig> nativeConfig(const QString &serverId) const;
@@ -0,0 +1,15 @@
#ifndef LOCALPROXYDEFS_H
#define LOCALPROXYDEFS_H
#include <QtGlobal>
namespace amnezia::localProxy
{
constexpr int defaultProxyPort = 10808;
constexpr int proxyPortMin = 1024;
constexpr int proxyPortMax = 65535;
constexpr quint16 apiPort = 49490;
constexpr int portReleaseWaitMs = 800;
}
#endif // LOCALPROXYDEFS_H
@@ -0,0 +1,51 @@
#include "portUtils.h"
#include "localProxyConstants.h"
#include <QElapsedTimer>
#include <QHostAddress>
#include <QTcpServer>
#include <QThread>
using namespace amnezia;
bool portUtils::isPortAvailable(int port)
{
if (port < localProxy::proxyPortMin || port > localProxy::proxyPortMax) {
return false;
}
QTcpServer server;
const bool success = server.listen(QHostAddress::LocalHost, static_cast<quint16>(port));
server.close();
return success;
}
bool portUtils::waitForPort(int port, int timeoutMs)
{
QElapsedTimer timer;
timer.start();
while (!isPortAvailable(port)) {
if (timer.elapsed() >= timeoutMs) {
return false;
}
QThread::msleep(200);
}
return true;
}
std::optional<int> portUtils::findFirstAvailablePort(int startPort, int endPort)
{
startPort = qMax(startPort, localProxy::proxyPortMin);
endPort = qMin(endPort, localProxy::proxyPortMax);
for (int port = startPort; port <= endPort; ++port) {
if (isPortAvailable(port)) {
return port;
}
}
return std::nullopt;
}
+13
View File
@@ -0,0 +1,13 @@
#ifndef PORTUTILS_H
#define PORTUTILS_H
#include <optional>
namespace portUtils
{
bool isPortAvailable(int port);
bool waitForPort(int port, int timeoutMs);
std::optional<int> findFirstAvailablePort(int startPort, int endPort);
}
#endif // PORTUTILS_H
@@ -0,0 +1,47 @@
#include "xrayConfigUtils.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/containers/containerUtils.h"
#include <QJsonArray>
using namespace amnezia;
std::optional<QString> xrayConfigUtils::extractLastConfig(const QJsonObject &serverConfig)
{
const QJsonArray containers = serverConfig.value(configKey::containers).toArray();
const QString targetContainer = ContainerUtils::containerToString(DockerContainer::Xray);
for (const QJsonValue &value : containers) {
const QJsonObject container = value.toObject();
if (container.value(configKey::container).toString() != targetContainer) {
continue;
}
const QJsonObject proto = container.value(QString(configKey::xray)).toObject();
const QString serialized = proto.value(configKey::lastConfig).toString();
if (!serialized.isEmpty()) {
return serialized;
}
}
return std::nullopt;
}
bool xrayConfigUtils::setInboundPort(QJsonObject &xrayConfig, int port)
{
if (!xrayConfig.value(QLatin1String("inbounds")).isArray()) {
return false;
}
QJsonArray inbounds = xrayConfig.value(QLatin1String("inbounds")).toArray();
if (inbounds.isEmpty() || !inbounds.at(0).isObject()) {
return false;
}
QJsonObject firstInbound = inbounds.at(0).toObject();
firstInbound.insert(QLatin1String("port"), port);
inbounds[0] = firstInbound;
xrayConfig.insert(QLatin1String("inbounds"), inbounds);
return true;
}
@@ -0,0 +1,15 @@
#ifndef XRAYCONFIGUTILS_H
#define XRAYCONFIGUTILS_H
#include <optional>
#include <QJsonObject>
#include <QString>
namespace xrayConfigUtils
{
std::optional<QString> extractLastConfig(const QJsonObject &serverConfig);
bool setInboundPort(QJsonObject &xrayConfig, int port);
}
#endif // XRAYCONFIGUTILS_H
@@ -0,0 +1,58 @@
#include "localProxyUiController.h"
LocalProxyUiController::LocalProxyUiController(LocalProxyController *localProxyController, QObject *parent)
: QObject(parent), m_localProxyController(localProxyController)
{
connect(m_localProxyController, &LocalProxyController::stateChanged, this, &LocalProxyUiController::stateChanged);
connect(m_localProxyController, &LocalProxyController::startFailed, this, &LocalProxyUiController::startFailed);
}
bool LocalProxyUiController::isSupported() const
{
return m_localProxyController->isSupported();
}
bool LocalProxyUiController::isEnabled() const
{
return m_localProxyController->isEnabled();
}
int LocalProxyUiController::port() const
{
return m_localProxyController->port();
}
QString LocalProxyUiController::ownerId() const
{
return m_localProxyController->ownerId();
}
int LocalProxyUiController::activePort() const
{
return m_localProxyController->activePort();
}
bool LocalProxyUiController::isPortUserDefined() const
{
return m_localProxyController->isPortUserDefined();
}
bool LocalProxyUiController::setPort(int port)
{
return m_localProxyController->setPort(port);
}
bool LocalProxyUiController::isPortBusy(int port) const
{
return m_localProxyController->isPortBusy(port);
}
bool LocalProxyUiController::enable(const QString &ownerId, int port)
{
return m_localProxyController->enable(ownerId, port);
}
void LocalProxyUiController::disable()
{
m_localProxyController->disable();
}
@@ -0,0 +1,41 @@
#ifndef LOCALPROXYUICONTROLLER_H
#define LOCALPROXYUICONTROLLER_H
#include <QObject>
#include "core/controllers/localProxyController.h"
class LocalProxyUiController : public QObject
{
Q_OBJECT
public:
explicit LocalProxyUiController(LocalProxyController *localProxyController, QObject *parent = nullptr);
Q_PROPERTY(bool isSupported READ isSupported CONSTANT)
Q_PROPERTY(bool isEnabled READ isEnabled NOTIFY stateChanged)
Q_PROPERTY(int port READ port NOTIFY stateChanged)
Q_PROPERTY(QString ownerId READ ownerId NOTIFY stateChanged)
Q_PROPERTY(int activePort READ activePort NOTIFY stateChanged)
public slots:
bool isSupported() const;
bool isEnabled() const;
int port() const;
QString ownerId() const;
int activePort() const;
bool isPortUserDefined() const;
bool setPort(int port);
bool isPortBusy(int port) const;
bool enable(const QString &ownerId, int port);
void disable();
signals:
void stateChanged();
void startFailed(const QString &message);
private:
LocalProxyController *m_localProxyController;
};
#endif // LOCALPROXYUICONTROLLER_H
+16 -1
View File
@@ -17,6 +17,7 @@ Item {
property bool passwordToggle: false
property bool passwordRevealed: false
property bool clearable: false
property string trailingIconSource
property string buttonText
property string buttonImageSource
@@ -29,6 +30,8 @@ Item {
readonly property bool hasError: root.errorText !== ""
signal trailingClicked()
implicitWidth: content.implicitWidth
implicitHeight: content.implicitHeight
@@ -146,7 +149,7 @@ Item {
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
visible: clearButton.visible || eyeButton.visible
visible: root.clearable || root.passwordToggle || root.trailingIconSource !== ""
IconButtonType {
id: clearButton
@@ -174,6 +177,18 @@ Item {
onClicked: root.passwordRevealed = !root.passwordRevealed
}
IconButtonType {
id: trailingButton
implicitWidth: 40
implicitHeight: 40
visible: root.trailingIconSource !== ""
icon.source: root.trailingIconSource
onClicked: root.trailingClicked()
}
}
}
@@ -75,6 +75,12 @@ PageType {
return
}
if (isCurrentDevice && LocalProxyController.isEnabled
&& LocalProxyController.ownerId === ServersUiController.processedServerId) {
PageController.showNotificationMessage(qsTr("Cannot unlink device while local proxy is running"))
return
}
var headerText = qsTr("Are you sure you want to unlink this device?")
var descriptionText = qsTr("This will unlink the device from your subscription. You can reconnect it anytime by pressing \"Reload API config\" in subscription settings on device.")
var yesButtonText = qsTr("Continue")
@@ -268,6 +268,30 @@ PageType {
spacing: 0
ListItemType {
id: localProxyItem
Layout.fillWidth: true
visible: LocalProxyController.isSupported && ServersUiController.processedServerIsPremium
text: qsTr("Local proxy")
subtitle: {
if (!LocalProxyController.isEnabled) {
return qsTr("Off")
}
if (LocalProxyController.ownerId !== ServersUiController.processedServerId) {
return qsTr("In use by another server")
}
return qsTr("Running: 127.0.0.1:%1").arg(LocalProxyController.activePort > 0 ? LocalProxyController.activePort : LocalProxyController.port)
}
trailingIconSource: "qrc:/images/controls/chevron-right.svg"
onClicked: {
PageController.goToPage(PageEnum.PageSettingsLocalProxy)
}
}
ListItemType {
id: vpnKey
@@ -403,6 +427,9 @@ PageType {
var yesButtonFunction = function() {
if (ServersUiController.isDefaultServerCurrentlyProcessed() && ConnectionController.isConnected) {
PageController.showNotificationMessage(qsTr("Cannot unlink device during active connection"))
} else if (LocalProxyController.isEnabled
&& LocalProxyController.ownerId === ServersUiController.processedServerId) {
PageController.showNotificationMessage(qsTr("Cannot unlink device while local proxy is running"))
} else {
PageController.showBusyIndicator(true)
if (SubscriptionUiController.deactivateDevice(ServersUiController.processedServerId)) {
@@ -438,6 +465,9 @@ PageType {
var yesButtonFunction = function() {
if (ServersUiController.isDefaultServerCurrentlyProcessed() && ConnectionController.isConnected) {
PageController.showNotificationMessage(qsTr("Cannot remove server during active connection"))
} else if (LocalProxyController.isEnabled
&& LocalProxyController.ownerId === ServersUiController.processedServerId) {
PageController.showNotificationMessage(qsTr("Cannot remove server while local proxy is running"))
} else {
PageController.showBusyIndicator(true)
SubscriptionUiController.removeServer(ServersUiController.processedServerId)
@@ -0,0 +1,381 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import PageEnum 1.0
import Style 1.0
import "./"
import "../Controls2"
import "../Controls2/TextTypes"
import "../Config"
PageType {
id: root
readonly property int localProxyPortMin: 1024
readonly property int localProxyPortMax: 65535
readonly property bool isEnabledForThisServer: LocalProxyController.isEnabled
&& LocalProxyController.ownerId === ServersUiController.processedServerId
readonly property bool isOwnedByOtherServer: LocalProxyController.isEnabled
&& LocalProxyController.ownerId !== ""
&& LocalProxyController.ownerId !== ServersUiController.processedServerId
property int pendingStartRequestedPort: -1
property bool pendingStartVpnWasActive: false
property bool pendingEnableAfterVpnDisconnect: false
property string pendingEnableServerId: ""
property int pendingEnableRequestedPort: -1
function clearPendingEnableAfterVpnDisconnect() {
root.pendingEnableAfterVpnDisconnect = false
root.pendingEnableServerId = ""
root.pendingEnableRequestedPort = -1
}
function enableLocalProxyNow(serverId, requestedPort, vpnWasActive) {
if (!LocalProxyController.enable(serverId, requestedPort)) {
PageController.showNotificationMessage(qsTr("Failed to enable local proxy. Check the port (%1-%2)")
.arg(root.localProxyPortMin)
.arg(root.localProxyPortMax))
return false
}
root.pendingStartRequestedPort = requestedPort
root.pendingStartVpnWasActive = vpnWasActive
startSuccessToastTimer.restart()
return true
}
function portRangeErrorText() {
return qsTr("Port must be between %1 and %2").arg(root.localProxyPortMin).arg(root.localProxyPortMax)
}
function portBusyErrorText(port) {
return qsTr("Port %1 is already in use on this device. Choose a different one").arg(port)
}
function runningPortText() {
const activePort = LocalProxyController.activePort
return (activePort > 0 ? activePort : LocalProxyController.port).toString()
}
function computePortErrorText() {
const text = portField.textField.text.trim()
if (text === "") {
return qsTr("Port is required")
}
const value = parseInt(text)
if (isNaN(value) || value < root.localProxyPortMin || value > root.localProxyPortMax) {
return root.portRangeErrorText()
}
const isOwnActivePort = root.isEnabledForThisServer && value === LocalProxyController.activePort
if (!isOwnActivePort && LocalProxyController.isPortBusy(value)) {
return root.portBusyErrorText(value)
}
return ""
}
function handleLocalProxyToggle(checked) {
if (checked) {
if (!ServersUiController.processedServerIsPremium) {
PageController.showNotificationMessage(qsTr("Local proxy is available only for Amnezia Premium"))
return
}
const wasVpnActive = ConnectionController.isConnected || ConnectionController.isConnectionInProgress
let serverId = ServersUiController.processedServerId
if (!serverId) {
serverId = ServersUiController.defaultServerId
}
if (!serverId) {
PageController.showNotificationMessage(qsTr("Unable to determine the current server"))
return
}
if (LocalProxyController.isEnabled
&& LocalProxyController.ownerId
&& LocalProxyController.ownerId !== serverId) {
PageController.showNotificationMessage(qsTr("Local proxy is already enabled for another server"))
return
}
let requestedPort = LocalProxyController.port
if (portField.textField.text.trim() !== "") {
const validationError = root.computePortErrorText()
if (validationError !== "") {
portField.errorText = validationError
return
}
requestedPort = portField.portValue()
LocalProxyController.setPort(requestedPort)
} else if (requestedPort < root.localProxyPortMin || requestedPort > root.localProxyPortMax) {
portField.errorText = root.portRangeErrorText()
return
} else if (LocalProxyController.isPortBusy(requestedPort)
&& LocalProxyController.isPortUserDefined()) {
portField.errorText = root.portBusyErrorText(requestedPort)
return
}
portField.errorText = ""
if (wasVpnActive) {
root.pendingEnableAfterVpnDisconnect = true
root.pendingEnableServerId = serverId
root.pendingEnableRequestedPort = requestedPort
ConnectionController.closeConnection()
return
}
root.enableLocalProxyNow(serverId, requestedPort, false)
} else {
startSuccessToastTimer.stop()
root.clearPendingEnableAfterVpnDisconnect()
root.pendingStartRequestedPort = -1
root.pendingStartVpnWasActive = false
LocalProxyController.disable()
PageController.showNotificationMessage(qsTr("Local proxy stopped"))
}
}
FlickableType {
id: flickable
anchors.top: parent.top
anchors.bottom: bottomBar.top
contentHeight: content.implicitHeight
ColumnLayout {
id: content
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
spacing: 0
NavigationBarType {
id: navigationBar
Layout.fillWidth: true
title: qsTr("Local proxy")
description: qsTr("Used by specific apps (e.g., CensorTracker extension) via Amnezia Premium.\nOnly one can be active at a time: VPN or local proxy")
header.showSwitcher: ServersUiController.processedServerIsPremium
header.switcher.checked: root.isEnabledForThisServer
header.switcher.enabled: !root.isOwnedByOtherServer
Connections {
target: navigationBar.header
function onSwitcherToggled(checked) {
if (checked !== root.isEnabledForThisServer) {
root.handleLocalProxyToggle(checked)
}
navigationBar.header.switcher.checked = Qt.binding(function() {
return root.isEnabledForThisServer
})
}
}
}
NotificationType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 16
visible: root.isOwnedByOtherServer
variant: NotificationType.Warning
text: qsTr("Local proxy is already running for \"%1\". Turn it off there to use it with this server")
.arg(ServersUiController.serverName(LocalProxyController.ownerId))
}
LinkButtonType {
Layout.leftMargin: 16
Layout.rightMargin: 16
size: LinkButtonType.Medium
text: qsTr("Learn more")
onClicked: {
const path = LanguageUiController.currentLanguageName === "Русский"
? "ru/documentation/instructions/local-proxy"
: "documentation/instructions/local-proxy"
Qt.openUrlExternally(LanguageUiController.getCurrentDocsUrl(path))
}
}
TextFieldType {
id: portField
Layout.fillWidth: true
Layout.topMargin: 24
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 16
headerText: qsTr("Address and port")
placeholderText: LocalProxyController.port.toString()
trailingIconSource: root.isEnabledForThisServer ? "qrc:/images/controls/copy.svg" : ""
textField.leftPadding: 16 + portPrefix.implicitWidth
textField.inputMethodHints: Qt.ImhDigitsOnly | Qt.ImhNoPredictiveText
textField.validator: RegularExpressionValidator {
regularExpression: /^[0-9]{0,5}$/
}
onTrailingClicked: {
const portText = root.runningPortText()
GC.copyToClipBoard("127.0.0.1:" + portText)
PageController.showNotificationMessage(qsTr("Copied: 127.0.0.1:%1").arg(portText))
}
function syncPortValue() {
if (root.isEnabledForThisServer && LocalProxyController.activePort > 0) {
textField.text = LocalProxyController.activePort.toString()
} else if (LocalProxyController.isPortUserDefined()) {
textField.text = LocalProxyController.port.toString()
} else {
textField.text = ""
}
}
function portValue() {
const value = parseInt(textField.text)
return isNaN(value) ? -1 : value
}
Component.onCompleted: syncPortValue()
Text {
id: portPrefix
parent: portField.textField
x: 16
y: portField.textField.topPadding
+ (portField.textField.height - portField.textField.topPadding - portField.textField.bottomPadding - height) / 2
z: 1
text: "127.0.0.1:"
color: AmneziaStyle.color.textPrimary
font: portField.textField.font
}
}
}
}
BottomBarType {
id: bottomBar
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
ButtonType {
Layout.fillWidth: true
text: qsTr("Save")
onClicked: {
const validationError = root.computePortErrorText()
portField.errorText = validationError
if (validationError !== "") {
return
}
const value = portField.portValue()
if (!LocalProxyController.setPort(value)) {
portField.errorText = root.portRangeErrorText()
return
}
PageController.showNotificationMessage(qsTr("Port saved: %1").arg(value))
portField.syncPortValue()
}
}
}
Timer {
id: startSuccessToastTimer
interval: 250
repeat: false
running: false
onTriggered: {
if (!LocalProxyController.isEnabled) {
return
}
const activePort = LocalProxyController.activePort
const requestedPort = root.pendingStartRequestedPort
const shownPort = activePort > 0 ? activePort : requestedPort
if (activePort > 0 && requestedPort > 0 && activePort !== requestedPort) {
PageController.showNotificationMessage(qsTr("Port %1 is in use — selected free port %2")
.arg(requestedPort)
.arg(activePort))
} else if (root.pendingStartVpnWasActive && shownPort > 0) {
PageController.showNotificationMessage(qsTr("VPN off. Local proxy running: 127.0.0.1:%1")
.arg(shownPort))
} else if (shownPort > 0) {
PageController.showNotificationMessage(qsTr("Local proxy running: 127.0.0.1:%1")
.arg(shownPort))
}
root.pendingStartRequestedPort = -1
root.pendingStartVpnWasActive = false
}
}
Connections {
target: ConnectionController
function onConnectionStateChanged() {
if (!root.pendingEnableAfterVpnDisconnect) {
return
}
if (ConnectionController.isConnected || ConnectionController.isConnectionInProgress) {
return
}
const serverId = root.pendingEnableServerId
const requestedPort = root.pendingEnableRequestedPort
root.clearPendingEnableAfterVpnDisconnect()
root.enableLocalProxyNow(serverId, requestedPort, true)
}
}
Connections {
target: LocalProxyController
function onStateChanged() {
if (!portField.textField.activeFocus) {
portField.syncPortValue()
}
}
function onStartFailed(message) {
startSuccessToastTimer.stop()
root.pendingStartRequestedPort = -1
root.pendingStartVpnWasActive = false
PageController.showNotificationMessage(message)
}
}
Connections {
target: ServersUiController
function onProcessedServerIdChanged() {
if (!portField.textField.activeFocus) {
portField.syncPortValue()
}
}
}
}
+1
View File
@@ -145,6 +145,7 @@
<file>Pages2/PageSettingsDns.qml</file>
<file>Pages2/PageSettingsKillSwitch.qml</file>
<file>Pages2/PageSettingsKillSwitchExceptions.qml</file>
<file>Pages2/PageSettingsLocalProxy.qml</file>
<file>Pages2/PageSettingsLogging.qml</file>
<file>Pages2/PageSettingsLogViewer.qml</file>
<file>Pages2/PageSettingsServerData.qml</file>
+1
View File
@@ -41,6 +41,7 @@ namespace PageLoader
PageSettingsApiDevices,
PageSettingsApiSubscriptionKey,
PageSettingsKillSwitchExceptions,
PageSettingsLocalProxy,
PageServiceSftpSettings,
PageServiceTorWebsiteSettings,
+3
View File
@@ -40,6 +40,9 @@ class IpcInterface
SLOT(bool xrayStart(const QString &config));
SLOT(bool xrayStop());
SLOT(qint64 xrayStartOwned(const QString &config));
SLOT(bool xrayStopOwned(qint64 token));
SLOT(qint64 xrayCurrentToken());
SLOT( bool startNetworkCheck(const QString& serverIpv4Gateway, const QString& deviceIpv4Address) );
SLOT( bool stopNetworkCheck() );
+29 -2
View File
@@ -310,7 +310,7 @@ bool IpcServer::xrayStart(const QString& cfg)
qDebug() << "IpcServer::xrayStart";
#endif
return Xray::getInstance().startXray(cfg);
return Xray::getInstance().start(cfg) != 0;
}
bool IpcServer::xrayStop()
@@ -319,5 +319,32 @@ bool IpcServer::xrayStop()
qDebug() << "IpcServer::xrayStop";
#endif
return Xray::getInstance().stopXray();
return Xray::getInstance().stopAny();
}
qint64 IpcServer::xrayStartOwned(const QString& cfg)
{
#ifdef MZ_DEBUG
qDebug() << "IpcServer::xrayStartOwned";
#endif
return Xray::getInstance().start(cfg);
}
bool IpcServer::xrayStopOwned(qint64 token)
{
#ifdef MZ_DEBUG
qDebug() << "IpcServer::xrayStopOwned";
#endif
return Xray::getInstance().stop(token);
}
qint64 IpcServer::xrayCurrentToken()
{
#ifdef MZ_DEBUG
qDebug() << "IpcServer::xrayCurrentToken";
#endif
return Xray::getInstance().currentToken();
}
+3
View File
@@ -44,6 +44,9 @@ public:
virtual bool restoreResolvers() override;
virtual bool xrayStart(const QString& cfg) override;
virtual bool xrayStop() override;
virtual qint64 xrayStartOwned(const QString& cfg) override;
virtual bool xrayStopOwned(qint64 token) override;
virtual qint64 xrayCurrentToken() override;
virtual bool startNetworkCheck(const QString& serverIpv4Gateway, const QString& deviceIpv4Address) override;
virtual bool stopNetworkCheck() override;
+47
View File
@@ -31,6 +31,53 @@
#include "xray_defs.h"
#endif
qint64 Xray::start(const QString &cfg)
{
if (m_currentToken != 0) {
qDebug() << "Xray::start() engine is busy (token" << m_currentToken << "), stopping it for the new owner";
stopXray();
m_currentToken = 0;
}
if (!startXray(cfg)) {
return 0;
}
m_currentToken = ++m_lastToken;
qDebug() << "Xray::start() engine started, owner token" << m_currentToken;
return m_currentToken;
}
bool Xray::stop(qint64 token)
{
if (m_currentToken == 0) {
return true;
}
if (token == 0 || token != m_currentToken) {
qDebug() << "Xray::stop() stale owner token" << token << "(current" << m_currentToken << "), ignoring";
return true;
}
m_currentToken = 0;
return stopXray();
}
bool Xray::stopAny()
{
if (m_currentToken == 0) {
return true;
}
m_currentToken = 0;
return stopXray();
}
qint64 Xray::currentToken() const
{
return m_currentToken;
}
bool Xray::startXray(const QString &cfg)
{
qDebug() << "Xray::startXray()";
+12 -1
View File
@@ -12,10 +12,21 @@ public:
return instance;
}
// The engine is shared between consumers (VPN protocol, local proxy).
// start() returns an owner token; stop(token) is a no-op unless the token
// still owns the running engine, so a late teardown can't kill a newer owner.
qint64 start(const QString& cfg);
bool stop(qint64 token);
bool stopAny();
qint64 currentToken() const;
private:
bool startXray(const QString& cfg);
bool stopXray();
private:
qint64 m_currentToken = 0;
qint64 m_lastToken = 0;
static void ctxSockCallback(uintptr_t fd, void* ctx) {
reinterpret_cast<Xray*>(ctx)->sockCallback(fd);
}