Compare commits

..

8 Commits

Author SHA1 Message Date
yp 5d16645b84 fix: android icons (#2725)
* update icons android

* remove comment
2026-06-24 00:08:21 +08:00
Yaroslav Gurov 203a092dc9 fix: blobs for macos-ne and codesigning of qt blobs (#2754) 2026-06-24 00:07:42 +08:00
yp d8b8590bc4 fix: XRay validation audit (#2749)
* flow default and config fixes

* host/SNI/path validation backend + flow-default flag

* input validation, numeric limits and live Save on settings pages

* MinMaxRowType clamping + DropDownType fit-content drawer
2026-06-24 00:07:26 +08:00
yp 9b8bfaa6f8 fix: regression testing vs 4.8.15.4 (#2730)
* fixed revoke

* fixed async update xray/mtproxy/telemt

* fixed connect premium config

* fixed autostart app hide

* fixed clear profile

* (6) fixed xtls-rprx-vision→empty

* (7) fixed appendClient abort & fix restore admin

* (8) fixed async|clientsUpdated

* fixed increment name server N

* remove comment & reset file

* chore: add tr to nextAvailableServerName

---------

Co-authored-by: vkamn <vk@amnezia.org>
2026-06-23 23:05:58 +08:00
Yaroslav Gurov 890103a16a fix: update amneziawg (#2743)
* chore(conan): update amneziawg

* fix(conan): use cmake 4.2+ to support MSVC26

* fix(ci/cd): use the latest cmake generator available on windows
2026-06-17 19:56:53 +07:00
yp 56ab82f87f fix: Use shared OpenSSL on Android (#2736) 2026-06-16 10:57:32 +07:00
lunardunno 3984acbb44 feat: updating install_docker.sh script (#2661)
* Updating install_docker.sh script

Implementing a Docker service status check.
The Docker reinstall step has been removed due to the implementation of Docker service checking.
Implementing locale checking and assignment.
Implementation of execution of some actions through commands with sudo, to reduce delays caused by differences in the values ​​of the PATH variable for the root user and the user included in the sudo group.
Implementation of a verification step for the install containerization app to avoid installing unsupported podman-docker applications.

* adding message handling to install controller

Adding handling for "Containerization app is not supported" and "Service status not active" messages to the controller.

* Error Codes added

Error Codes added for ServerContainerizationNotSupported & DockerServiceNotActive

* Adding extended descriptions of new errors

* fix last line in errorCodes.h

* fix last line in errorStrings.cpp

* Changing the names of errors

* various changes in the script

The messages output for processing by the server controller have been changed: "Container runtime is not supported" and "Container runtime service is not running."
The redundant check and output of the "Packet manager not found" message, as well as the interruption of script execution, have been eliminated, as this situation is handled by the server controller at an earlier stage (check_server_is_busy.sh) and only there.
Added installation of the whish package if it is missing from the OS, for subsequent re-execution of the install_docker.sh and check_server_is_busy.sh scripts.
Implemented an alternative method for detecting the package manager if the whish package is initially missing from the OS.
The algorithm for setting the $pm variable (package manager) has been changed.

* processed phrases have been changed

The phrases processed by the server controller have been changed.

* Attempting to use "command -v"

Switching to using "command -v" instead of "which".

* "which" as main, "command" as backup.

* "which" as main, "command" as backup for check user

* which  LOCK_CMD with sudo

Run the "which" with sudo to check the $LOCK_CMD variable in case the user's PATH variable has incorrect values ​​if the user is not root and is only a member of the sudo group.

* suppressing sudo password prompt

* suppressing sudo password prompt

* suppressing sudo password prompt install_docker.sh

* Changing the phrase for check stdout

"sudo:" with "not found" instead of "command not found"

* Changing phrases for check stdout check_user_in_sudo.sh‎

* sudo|docker and not found, in one line

* check only sudoers
2026-06-15 22:28:38 +07:00
yp cc404378f9 fix: remove only amnezia- prefixed docker volumes (#2728) 2026-06-15 13:12:19 +07:00
60 changed files with 705 additions and 437 deletions
+1 -1
View File
@@ -157,7 +157,7 @@ jobs:
run: pip install "conan==2.28.0" run: pip install "conan==2.28.0"
- name: 'Build dependencies' - name: 'Build dependencies'
run: cmake -S . -B build -G "Visual Studio 17 2022" -DPREBUILTS_ONLY=1 run: cmake -S . -B build -DPREBUILTS_ONLY=1
- name: 'Authorize in remote' - name: 'Authorize in remote'
if: github.ref == 'refs/heads/dev' if: github.ref == 'refs/heads/dev'
+6
View File
@@ -119,7 +119,13 @@ void AmneziaApplication::init()
win->setPersistentSceneGraph(true); win->setPersistentSceneGraph(true);
win->setPersistentGraphics(true); win->setPersistentGraphics(true);
#endif #endif
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
win->show(); win->show();
#else
if (!m_coreController || !m_coreController->pageController()->shouldStartMinimized()) {
win->show();
}
#endif
} }
}, },
Qt::QueuedConnection); Qt::QueuedConnection);
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:type="linear"
android:angle="135"
android:startColor="#2A2A2E"
android:centerColor="#17171A"
android:endColor="#0E0E11" />
</shape>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<inset xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/ic_amnezia_round"
android:insetLeft="19.5%"
android:insetTop="19.5%"
android:insetRight="19.5%"
android:insetBottom="19.5%" />
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
</adaptive-icon>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

+16
View File
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="NoActionBar">
<item name="android:windowBackground">@color/black</item>
<item name="android:colorBackground">@color/black</item>
<item name="android:windowActionBar">false</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
<item name="android:enforceNavigationBarContrast">false</item>
<item name="android:enforceStatusBarContrast">false</item>
<item name="android:windowSplashScreenBackground">@color/ic_launcher_background</item>
<item name="android:windowSplashScreenIconBackgroundColor">@color/ic_launcher_background</item>
<item name="android:windowSplashScreenAnimatedIcon">@mipmap/icon</item>
</style>
</resources>
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#0E0E11</color>
</resources>
+1 -1
View File
@@ -152,5 +152,5 @@ message(${QtCore_location})
get_filename_component(QT_BIN_DIR_DETECTED "${QtCore_location}/../../../../../bin" ABSOLUTE) get_filename_component(QT_BIN_DIR_DETECTED "${QtCore_location}/../../../../../bin" ABSOLUTE)
add_custom_command(TARGET ${PROJECT} POST_BUILD add_custom_command(TARGET ${PROJECT} POST_BUILD
COMMAND ${QT_BIN_DIR_DETECTED}/macdeployqt $<TARGET_BUNDLE_DIR:AmneziaVPN> -appstore-compliant -qmldir=${CMAKE_CURRENT_SOURCE_DIR} COMMAND ${QT_BIN_DIR_DETECTED}/macdeployqt $<TARGET_BUNDLE_DIR:AmneziaVPN> -appstore-compliant -qmldir=${CMAKE_CURRENT_SOURCE_DIR} -no-codesign
) )
@@ -244,11 +244,7 @@ ErrorCode XrayConfigurator::applyServerSettingsToRemote(const ServerCredentials
<< "container=" << static_cast<int>(container) << "host=" << credentials.hostName << "container=" << static_cast<int>(container) << "host=" << credentials.hostName
<< "transport=" << srv.transport << "security=" << srv.security << "port=" << srv.port << "transport=" << srv.transport << "security=" << srv.security << "port=" << srv.port
<< "appendClient=" << appendNewClient; << "appendClient=" << appendNewClient;
QString flowValue = srv.flow; const QString flowValue = srv.flow;
if (flowValue.isEmpty() && srv.security == QLatin1String("reality")) {
flowValue = QStringLiteral("xtls-rprx-vision");
}
QString realityPublicKey; QString realityPublicKey;
QString realityShortId; QString realityShortId;
if (srv.security == QLatin1String("reality")) { if (srv.security == QLatin1String("reality")) {
@@ -563,9 +559,12 @@ QJsonObject XrayConfigurator::buildStreamSettings(const XrayServerConfig &srv, c
if (pad.obfsMode) { if (pad.obfsMode) {
if (!pad.bytesMin.isEmpty() || !pad.bytesMax.isEmpty()) { if (!pad.bytesMin.isEmpty() || !pad.bytesMax.isEmpty()) {
QJsonObject br; QJsonObject br;
br[QStringLiteral("from")] = pad.bytesMin.isEmpty() ? 1 : pad.bytesMin.toInt(); const int fromV = pad.bytesMin.isEmpty() ? 1 : pad.bytesMin.toInt();
br[QStringLiteral("to")] = pad.bytesMax.isEmpty() ? (pad.bytesMin.isEmpty() ? 256 : pad.bytesMin.toInt()) int toV = pad.bytesMax.isEmpty() ? 256 : pad.bytesMax.toInt();
: pad.bytesMax.toInt(); if (toV < fromV)
toV = fromV;
br[QStringLiteral("from")] = fromV;
br[QStringLiteral("to")] = toV;
xo[QStringLiteral("xPaddingBytes")] = br; xo[QStringLiteral("xPaddingBytes")] = br;
} }
xo[QStringLiteral("xPaddingKey")] = pad.key.isEmpty() ? QStringLiteral("x_padding") : pad.key; xo[QStringLiteral("xPaddingKey")] = pad.key.isEmpty() ? QStringLiteral("x_padding") : pad.key;
@@ -106,7 +106,8 @@ ErrorCode ConnectionController::isConnectionSupported(const QString &serverId) c
return ErrorCode::AmneziaServiceNotRunning; return ErrorCode::AmneziaServiceNotRunning;
} }
if (serverConfigUtils::isLegacyApiSubscription(m_serversRepository->serverKind(serverId))) { const serverConfigUtils::ConfigType kind = m_serversRepository->serverKind(serverId);
if (serverConfigUtils::isLegacyApiSubscription(kind)) {
return ErrorCode::LegacyApiV1NotSupportedError; return ErrorCode::LegacyApiV1NotSupportedError;
} }
@@ -117,6 +118,9 @@ ErrorCode ConnectionController::isConnectionSupported(const QString &serverId) c
} }
if (container == DockerContainer::None) { if (container == DockerContainer::None) {
if (serverConfigUtils::isApiV2Subscription(kind)) {
return ErrorCode::NoError;
}
return ErrorCode::NoInstalledContainersError; return ErrorCode::NoInstalledContainersError;
} }
@@ -1,6 +1,7 @@
#include "coreSignalHandlers.h" #include "coreSignalHandlers.h"
#include <QTimer> #include <QTimer>
#include <QtConcurrent>
#include "core/utils/selfhosted/sshSession.h" #include "core/utils/selfhosted/sshSession.h"
#include "core/utils/errorCodes.h" #include "core/utils/errorCodes.h"
@@ -144,8 +145,10 @@ void CoreSignalHandlers::initExportControllerHandler()
}); });
connect(m_coreController->m_exportController, &ExportController::revokeClientRequested, this, connect(m_coreController->m_exportController, &ExportController::revokeClientRequested, this,
[this](const QString &serverId, int row, DockerContainer container) { [this](const QString &serverId, int row, DockerContainer container) {
QtConcurrent::run([this, serverId, row, container]() {
m_coreController->m_usersController->revokeClient(serverId, row, container); m_coreController->m_usersController->revokeClient(serverId, row, container);
}); });
});
connect(m_coreController->m_exportController, &ExportController::renameClientRequested, this, connect(m_coreController->m_exportController, &ExportController::renameClientRequested, this,
[this](const QString &serverId, int row, const QString &clientName, DockerContainer container) { [this](const QString &serverId, int row, const QString &clientName, DockerContainer container) {
m_coreController->m_usersController->renameClient(serverId, row, clientName, container); m_coreController->m_usersController->renameClient(serverId, row, clientName, container);
@@ -202,13 +205,15 @@ void CoreSignalHandlers::initAdminConfigRevokedHandler()
{ {
connect(m_coreController->m_installController, &InstallController::clientRevocationRequested, this, connect(m_coreController->m_installController, &InstallController::clientRevocationRequested, this,
[this](const QString &serverId, const ContainerConfig &containerConfig, DockerContainer container) { [this](const QString &serverId, const ContainerConfig &containerConfig, DockerContainer container) {
QtConcurrent::run([this, serverId, containerConfig, container]() {
m_coreController->m_usersController->revokeClient(serverId, containerConfig, container); m_coreController->m_usersController->revokeClient(serverId, containerConfig, container);
}); });
});
connect(m_coreController->m_installController, &InstallController::clientAppendRequested, this, connect(m_coreController->m_installController, &InstallController::clientAppendRequested, this,
[this](const QString &serverId, const QString &clientId, const QString &clientName, DockerContainer container) { [this](const QString &serverId, const QString &clientId, const QString &clientName, DockerContainer container) {
m_coreController->m_usersController->appendClient(serverId, clientId, clientName, container); m_coreController->m_usersController->appendClient(serverId, clientId, clientName, container);
}); }, Qt::DirectConnection);
connect(m_coreController->m_usersController, &UsersController::adminConfigRevoked, m_coreController->m_installController, connect(m_coreController->m_usersController, &UsersController::adminConfigRevoked, m_coreController->m_installController,
&InstallController::clearCachedProfile); &InstallController::clearCachedProfile);
@@ -285,6 +290,8 @@ void CoreSignalHandlers::initClientManagementModelUpdateHandler()
m_coreController->m_clientManagementModel, &ClientManagementModel::updateModel); m_coreController->m_clientManagementModel, &ClientManagementModel::updateModel);
connect(m_coreController->m_usersController, &UsersController::clientRenamed, connect(m_coreController->m_usersController, &UsersController::clientRenamed,
m_coreController->m_clientManagementModel, &ClientManagementModel::updateClientName); m_coreController->m_clientManagementModel, &ClientManagementModel::updateClientName);
connect(m_coreController->m_usersController, &UsersController::revokeFinished,
m_coreController->m_exportController, &ExportController::revokeFinished);
} }
void CoreSignalHandlers::initSitesModelUpdateHandler() void CoreSignalHandlers::initSitesModelUpdateHandler()
@@ -48,6 +48,7 @@ signals:
void appendClientRequested(const QString &serverId, const QString &clientId, const QString &clientName, DockerContainer container); void appendClientRequested(const QString &serverId, const QString &clientId, const QString &clientName, DockerContainer container);
void updateClientsRequested(const QString &serverId, DockerContainer container); void updateClientsRequested(const QString &serverId, DockerContainer container);
void revokeClientRequested(const QString &serverId, int row, DockerContainer container); void revokeClientRequested(const QString &serverId, int row, DockerContainer container);
void revokeFinished(ErrorCode errorCode);
void renameClientRequested(const QString &serverId, int row, const QString &clientName, DockerContainer container); void renameClientRequested(const QString &serverId, int row, const QString &clientName, DockerContainer container);
public slots: public slots:
@@ -234,7 +234,9 @@ ErrorCode InstallController::updateServerConfig(const QString &serverId, DockerC
} else if (container == DockerContainer::Telemt) { } else if (container == DockerContainer::Telemt) {
TelemtInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig); TelemtInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
} }
if (reinstallRequired) {
clearCachedProfile(serverId, container); clearCachedProfile(serverId, container);
}
adminConfig->updateContainerConfig(container, newConfig); adminConfig->updateContainerConfig(container, newConfig);
m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin); m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin);
} }
@@ -836,8 +838,8 @@ ErrorCode InstallController::installDockerWorker(const ServerCredentials &creden
qDebug().noquote() << "InstallController::installDockerWorker" << stdOut; qDebug().noquote() << "InstallController::installDockerWorker" << stdOut;
if (container == DockerContainer::Awg2) { if (container == DockerContainer::Awg2) {
QRegularExpression regex(R"(Linux\s+(\d+)\.(\d+)[^\d]*)"); QRegularExpression kernelVersionRegex(R"(Linux\s+(\d+)\.(\d+)[^\d]*)");
QRegularExpressionMatch match = regex.match(stdOut); QRegularExpressionMatch match = kernelVersionRegex.match(stdOut);
if (match.hasMatch()) { if (match.hasMatch()) {
int majorVersion = match.captured(1).toInt(); int majorVersion = match.captured(1).toInt();
int minorVersion = match.captured(2).toInt(); int minorVersion = match.captured(2).toInt();
@@ -850,8 +852,19 @@ ErrorCode InstallController::installDockerWorker(const ServerCredentials &creden
if (stdOut.contains("lock")) if (stdOut.contains("lock"))
return ErrorCode::ServerPacketManagerError; return ErrorCode::ServerPacketManagerError;
if (stdOut.contains("command not found")) if (stdOut.contains("Container runtime is not supported"))
return ErrorCode::ServerContainerRuntimeNotSupported;
QRegularExpression notFoundRegex(
R"(^.*(?:sudo:|docker:).*not found.*$)",
QRegularExpression::MultilineOption);
if (notFoundRegex.match(stdOut).hasMatch()) {
return ErrorCode::ServerDockerFailedError; return ErrorCode::ServerDockerFailedError;
}
if (stdOut.contains("Container runtime service not running"))
return ErrorCode::ContainerRuntimeServiceNotRunning;
return error; return error;
} }
@@ -888,7 +901,7 @@ ErrorCode InstallController::isUserInSudo(const ServerCredentials &credentials,
return ErrorCode::ServerUserNotInSudo; return ErrorCode::ServerUserNotInSudo;
if (stdOut.contains("can't cd to") || stdOut.contains("Permission denied") || stdOut.contains("No such file or directory")) if (stdOut.contains("can't cd to") || stdOut.contains("Permission denied") || stdOut.contains("No such file or directory"))
return ErrorCode::ServerUserDirectoryNotAccessible; return ErrorCode::ServerUserDirectoryNotAccessible;
if (stdOut.contains("sudoers") || stdOut.contains("is not allowed to run sudo on")) if (stdOut.contains(QRegularExpression(R"(\bsudoers\b)")) || stdOut.contains("is not allowed to") || stdOut.contains("can't do that"))
return ErrorCode::ServerUserNotAllowedInSudoers; return ErrorCode::ServerUserNotAllowedInSudoers;
if (stdOut.contains("password is required") || stdOut.contains("authentication is required")) if (stdOut.contains("password is required") || stdOut.contains("authentication is required"))
return ErrorCode::ServerUserPasswordRequired; return ErrorCode::ServerUserPasswordRequired;
@@ -758,14 +758,17 @@ ErrorCode UsersController::revokeClient(const QString &serverId, const int index
ContainerConfig containerCfg = adminConfig->containerConfig(container); ContainerConfig containerCfg = adminConfig->containerConfig(container);
QString containerClientId = containerCfg.protocolConfig.clientId(); QString containerClientId = containerCfg.protocolConfig.clientId();
if (!clientId.isEmpty() && !containerClientId.isEmpty() && containerClientId.contains(clientId)) { const bool isAdminMatch = !clientId.isEmpty() && !containerClientId.isEmpty() && containerClientId.contains(clientId);
if (isAdminMatch) {
emit adminConfigRevoked(serverId, container); emit adminConfigRevoked(serverId, container);
} }
emit clientRevoked(index); emit clientRevoked(index);
emit clientsUpdated(m_clientsTable);
} }
emit clientsUpdated(m_clientsTable);
emit revokeFinished(errorCode);
return errorCode; return errorCode;
} }
@@ -37,6 +37,7 @@ signals:
void clientAdded(const QJsonObject &client); void clientAdded(const QJsonObject &client);
void clientRenamed(int row, const QString &newName); void clientRenamed(int row, const QString &newName);
void clientRevoked(int row); void clientRevoked(int row);
void revokeFinished(ErrorCode errorCode);
void adminConfigRevoked(const QString &serverId, DockerContainer container); void adminConfigRevoked(const QString &serverId, DockerContainer container);
public slots: public slots:
@@ -32,7 +32,7 @@ XrayXPaddingConfig XrayXPaddingConfig::fromJson(const QJsonObject &json)
c.bytesMin = json.value(configKey::xPaddingBytesMin).toString(); c.bytesMin = json.value(configKey::xPaddingBytesMin).toString();
c.bytesMax = json.value(configKey::xPaddingBytesMax).toString(); c.bytesMax = json.value(configKey::xPaddingBytesMax).toString();
c.obfsMode = json.value(configKey::xPaddingObfsMode).toBool(true); c.obfsMode = json.value(configKey::xPaddingObfsMode).toBool(true);
c.key = json.value(configKey::xPaddingKey).toString(protocols::xray::defaultSite); c.key = json.value(configKey::xPaddingKey).toString();
c.header = json.value(configKey::xPaddingHeader).toString(); c.header = json.value(configKey::xPaddingHeader).toString();
c.placement = json.value(configKey::xPaddingPlacement).toString(protocols::xray::defaultXPaddingPlacement); c.placement = json.value(configKey::xPaddingPlacement).toString(protocols::xray::defaultXPaddingPlacement);
c.method = json.value(configKey::xPaddingMethod).toString(protocols::xray::defaultXPaddingMethod); c.method = json.value(configKey::xPaddingMethod).toString(protocols::xray::defaultXPaddingMethod);
@@ -365,6 +365,8 @@ XrayServerConfig XrayServerConfig::fromJson(const QJsonObject &json)
bool XrayServerConfig::hasEqualServerSettings(const XrayServerConfig &other) const bool XrayServerConfig::hasEqualServerSettings(const XrayServerConfig &other) const
{ {
return port == other.port return port == other.port
&& transportProto == other.transportProto
&& subnetAddress == other.subnetAddress
&& site == other.site && site == other.site
&& security == other.security && security == other.security
&& flow == other.flow && flow == other.flow
@@ -466,6 +468,17 @@ XrayProtocolConfig XrayProtocolConfig::fromJson(const QJsonObject &json)
} }
} }
} }
const QJsonArray outbounds = parsed.value(protocols::xray::outbounds).toArray();
if (!outbounds.isEmpty()) {
const QJsonObject settings = outbounds[0].toObject().value(protocols::xray::settings).toObject();
const QJsonArray vnext = settings.value(protocols::xray::vnext).toArray();
if (!vnext.isEmpty()) {
const QJsonArray users = vnext[0].toObject().value(protocols::xray::users).toArray();
if (!users.isEmpty()) {
clientCfg.id = users[0].toObject().value(protocols::xray::id).toString();
}
}
}
c.clientConfig = clientCfg; c.clientConfig = clientCfg;
} else { } else {
c.clientConfig = XrayClientConfig::fromJson(parsed); c.clientConfig = XrayClientConfig::fromJson(parsed);
+31 -176
View File
@@ -9,13 +9,10 @@
#include "ipc.h" #include "ipc.h"
#include <QCryptographicHash> #include <QCryptographicHash>
#include <QJsonArray>
#include <QJsonDocument> #include <QJsonDocument>
#include <QTimer> #include <QTimer>
#include <QJsonObject> #include <QJsonObject>
#include <QNetworkInterface> #include <QNetworkInterface>
#include <QNetworkProxy>
#include <QTcpSocket>
#include <QtCore/qlogging.h> #include <QtCore/qlogging.h>
#include <QtCore/qobjectdefs.h> #include <QtCore/qobjectdefs.h>
#include <QtCore/qprocess.h> #include <QtCore/qprocess.h>
@@ -59,28 +56,6 @@ XrayProtocol::XrayProtocol(const QJsonObject &configuration, QObject *parent) :
qWarning() << "Xray config string is not a valid JSON object"; qWarning() << "Xray config string is not a valid JSON object";
m_xrayConfig = {}; m_xrayConfig = {};
} }
m_serverPort = extractServerPort();
}
int XrayProtocol::extractServerPort() const
{
const QJsonArray outbounds = m_xrayConfig.value(amnezia::protocols::xray::outbounds).toArray();
if (outbounds.isEmpty())
return 0;
const QJsonObject settings = outbounds.first().toObject().value(amnezia::protocols::xray::settings).toObject();
QJsonArray servers;
if (settings.contains(amnezia::protocols::xray::vnext))
servers = settings.value(amnezia::protocols::xray::vnext).toArray();
else if (settings.contains(amnezia::protocols::xray::servers))
servers = settings.value(amnezia::protocols::xray::servers).toArray();
if (servers.isEmpty())
return 0;
return servers.first().toObject().value(amnezia::protocols::xray::port).toInt();
} }
XrayProtocol::~XrayProtocol() XrayProtocol::~XrayProtocol()
@@ -93,13 +68,6 @@ ErrorCode XrayProtocol::start()
{ {
qDebug() << "XrayProtocol::start()"; qDebug() << "XrayProtocol::start()";
m_connectivityProbeStarted = false;
if (!probeServerReachable()) {
qCritical() << "XrayProtocol: VPN server" << m_remoteAddress << "is unreachable";
return ErrorCode::XrayServerUnreachable;
}
// Inject SOCKS5 auth into the inbound before starting xray. // Inject SOCKS5 auth into the inbound before starting xray.
// Re-uses existing credentials if the config already has them (e.g. imported config). // Re-uses existing credentials if the config already has them (e.g. imported config).
amnezia::serialization::inbounds::InboundCredentials creds; amnezia::serialization::inbounds::InboundCredentials creds;
@@ -136,50 +104,22 @@ ErrorCode XrayProtocol::start()
qDebug() << "XrayProtocol: patched legacy inbound listen address to 127.0.0.1"; qDebug() << "XrayProtocol: patched legacy inbound listen address to 127.0.0.1";
} }
startTimeoutTimer();
return IpcClient::withInterface( return IpcClient::withInterface(
[&](QSharedPointer<IpcInterfaceReplica> iface) { [&](QSharedPointer<IpcInterfaceReplica> iface) {
auto xrayStart = iface->xrayStart(xrayConfigStr); auto xrayStart = iface->xrayStart(xrayConfigStr);
if (!xrayStart.waitForFinished() || !xrayStart.returnValue()) { if (!xrayStart.waitForFinished() || !xrayStart.returnValue()) {
qCritical() << "Failed to start xray"; qCritical() << "Failed to start xray";
stopTimeoutTimer();
return ErrorCode::XrayExecutableCrashed; return ErrorCode::XrayExecutableCrashed;
} }
return startTun2Socks(); return startTun2Socks();
}, },
[this]() { []() { return ErrorCode::AmneziaServiceConnectionFailed; });
stopTimeoutTimer();
return ErrorCode::AmneziaServiceConnectionFailed;
});
} }
void XrayProtocol::stop() void XrayProtocol::stop()
{ {
qDebug() << "XrayProtocol::stop()"; qDebug() << "XrayProtocol::stop()";
stopTimeoutTimer();
stopLivenessMonitor();
if (m_tun2socksProcess) {
m_tun2socksProcess->blockSignals(true);
#ifndef Q_OS_WIN
m_tun2socksProcess->terminate();
auto waitForFinished = m_tun2socksProcess->waitForFinished(1000);
if (!waitForFinished.waitForFinished() || !waitForFinished.returnValue()) {
qWarning() << "Failed to terminate tun2socks. Killing the process...";
m_tun2socksProcess->kill();
m_tun2socksProcess->waitForFinished(1000);
}
#else
m_tun2socksProcess->kill();
#endif
m_tun2socksProcess->close();
m_tun2socksProcess.reset();
}
IpcClient::withInterface([](QSharedPointer<IpcInterfaceReplica> iface) { IpcClient::withInterface([](QSharedPointer<IpcInterfaceReplica> iface) {
auto disableKillSwitch = iface->disableKillSwitch(); auto disableKillSwitch = iface->disableKillSwitch();
if (!disableKillSwitch.waitForFinished() || !disableKillSwitch.returnValue()) if (!disableKillSwitch.waitForFinished() || !disableKillSwitch.returnValue())
@@ -202,13 +142,31 @@ void XrayProtocol::stop()
qWarning() << "Failed to stop xray"; qWarning() << "Failed to stop xray";
}); });
if (m_tun2socksProcess) {
m_tun2socksProcess->blockSignals(true);
#ifndef Q_OS_WIN
m_tun2socksProcess->terminate();
auto waitForFinished = m_tun2socksProcess->waitForFinished(1000);
if (!waitForFinished.waitForFinished() || !waitForFinished.returnValue()) {
qWarning() << "Failed to terminate tun2socks. Killing the process...";
m_tun2socksProcess->kill();
}
#else
// terminate does not do anything useful on Windows
// so just kill the process
m_tun2socksProcess->kill();
#endif
m_tun2socksProcess->close();
m_tun2socksProcess.reset();
}
setConnectionState(Vpn::ConnectionState::Disconnected); setConnectionState(Vpn::ConnectionState::Disconnected);
} }
ErrorCode XrayProtocol::startTun2Socks() ErrorCode XrayProtocol::startTun2Socks()
{ {
m_tunResourceBusy = false;
m_tun2socksProcess = IpcClient::CreatePrivilegedProcess(); m_tun2socksProcess = IpcClient::CreatePrivilegedProcess();
if (!m_tun2socksProcess->waitForSource()) { if (!m_tun2socksProcess->waitForSource()) {
return ErrorCode::AmneziaServiceConnectionFailed; return ErrorCode::AmneziaServiceConnectionFailed;
@@ -233,31 +191,15 @@ ErrorCode XrayProtocol::startTun2Socks()
if (!line.contains("[TCP]") && !line.contains("[UDP]")) if (!line.contains("[TCP]") && !line.contains("[UDP]"))
qDebug() << "[tun2socks]:" << line; qDebug() << "[tun2socks]:" << line;
if (line.contains("resource busy")) if (line.contains("[STACK] tun://") && line.contains("<-> socks5://")) {
m_tunResourceBusy = true;
if (line.contains("[STACK] tun://") && line.contains("<-> socks5://") && !m_connectivityProbeStarted) {
m_connectivityProbeStarted = true;
disconnect(m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::readyReadStandardOutput, this, nullptr); disconnect(m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::readyReadStandardOutput, this, nullptr);
disconnect(m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::readyReadStandardError, this, nullptr);
runConnectivityProbe([this](bool ok) {
if (!ok) {
qCritical() << "Xray connectivity probe failed: no traffic flows through the tunnel";
stop();
setLastError(ErrorCode::XrayConnectivityCheckFailed);
return;
}
if (ErrorCode res = setupRouting(); res != ErrorCode::NoError) { if (ErrorCode res = setupRouting(); res != ErrorCode::NoError) {
stop(); stop();
setLastError(res); setLastError(res);
} else { } else {
stopTimeoutTimer();
setConnectionState(Vpn::ConnectionState::Connected); setConnectionState(Vpn::ConnectionState::Connected);
startLivenessMonitor();
} }
});
} }
}, },
Qt::QueuedConnection); Qt::QueuedConnection);
@@ -265,7 +207,15 @@ ErrorCode XrayProtocol::startTun2Socks()
connect( connect(
m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::finished, this, m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::finished, this,
[this](int exitCode, QProcess::ExitStatus exitStatus) { [this](int exitCode, QProcess::ExitStatus exitStatus) {
const bool resourceBusy = m_tunResourceBusy; // Check stdout for "resource busy" — the TUN device was not yet released
// by the previous tun2socks instance. Retry after a short delay.
bool resourceBusy = false;
if (m_tun2socksProcess) {
auto readOut = m_tun2socksProcess->readAllStandardOutput();
if (readOut.waitForFinished()) {
resourceBusy = readOut.returnValue().contains("resource busy");
}
}
if (resourceBusy && m_tun2socksRetryCount < maxTun2SocksRetries) { if (resourceBusy && m_tun2socksRetryCount < maxTun2SocksRetries) {
m_tun2socksRetryCount++; m_tun2socksRetryCount++;
@@ -381,98 +331,3 @@ ErrorCode XrayProtocol::setupRouting()
}, },
[]() { return ErrorCode::AmneziaServiceConnectionFailed; }); []() { return ErrorCode::AmneziaServiceConnectionFailed; });
} }
bool XrayProtocol::probeServerReachable()
{
if (m_remoteAddress.isEmpty() || m_serverPort <= 0) {
qWarning() << "XrayProtocol: skipping server reachability probe (address/port unknown)";
return true;
}
QTcpSocket sock;
sock.connectToHost(m_remoteAddress, static_cast<quint16>(m_serverPort));
const bool ok = sock.waitForConnected(m_serverProbeTimeoutMs);
if (!ok) {
qWarning() << "XrayProtocol: server" << m_remoteAddress << ":" << m_serverPort
<< "unreachable:" << sock.errorString();
}
sock.abort();
return ok;
}
void XrayProtocol::runConnectivityProbe(std::function<void(bool)> onResult)
{
if (m_remoteAddress.isEmpty() || m_serverPort <= 0) {
qWarning() << "XrayProtocol: connectivity probe skipped (server address/port unknown)";
onResult(true);
return;
}
auto *sock = new QTcpSocket(this);
QNetworkProxy proxy(QNetworkProxy::Socks5Proxy, QStringLiteral("127.0.0.1"),
static_cast<quint16>(m_socksPort), m_socksUser, m_socksPassword);
proxy.setCapabilities(QNetworkProxy::TunnelingCapability | QNetworkProxy::HostNameLookupCapability);
sock->setProxy(proxy);
auto *timeout = new QTimer(this);
timeout->setSingleShot(true);
auto done = QSharedPointer<bool>::create(false);
auto finish = [=](bool ok) {
if (*done)
return;
*done = true;
timeout->stop();
timeout->deleteLater();
sock->abort();
sock->deleteLater();
onResult(ok);
};
connect(sock, &QTcpSocket::connected, this, [=]() { finish(true); });
connect(sock, &QAbstractSocket::errorOccurred, this, [=](QAbstractSocket::SocketError) { finish(false); });
connect(timeout, &QTimer::timeout, this, [=]() { finish(false); });
timeout->start(m_connectivityProbeTimeoutMs);
sock->connectToHost(m_remoteAddress, static_cast<quint16>(m_serverPort));
}
void XrayProtocol::startLivenessMonitor()
{
if (!m_livenessTimer) {
m_livenessTimer = new QTimer(this);
connect(m_livenessTimer, &QTimer::timeout, this, [this]() {
if (connectionState() != Vpn::ConnectionState::Connected)
return;
runConnectivityProbe([this](bool ok) {
if (connectionState() != Vpn::ConnectionState::Connected)
return;
if (ok) {
m_livenessFailures = 0;
} else if (++m_livenessFailures >= m_maxLivenessFailures) {
qCritical() << "XrayProtocol: liveness check failed" << m_livenessFailures
<< "times in a row, the tunnel is dead";
stop();
setLastError(ErrorCode::XrayConnectionLost);
} else {
qWarning() << "XrayProtocol: liveness check failed (" << m_livenessFailures << "/"
<< m_maxLivenessFailures << ")";
}
});
});
}
m_livenessFailures = 0;
m_livenessTimer->start(m_livenessIntervalMs);
}
void XrayProtocol::stopLivenessMonitor()
{
if (m_livenessTimer)
m_livenessTimer->stop();
m_livenessFailures = 0;
}
-27
View File
@@ -6,16 +6,12 @@
#include <QHostAddress> #include <QHostAddress>
#include <QList> #include <QList>
#include <functional>
#include "core/utils/errorCodes.h" #include "core/utils/errorCodes.h"
#include "core/utils/routeModes.h" #include "core/utils/routeModes.h"
#include "core/utils/commonStructs.h" #include "core/utils/commonStructs.h"
#include "core/utils/ipcClient.h" #include "core/utils/ipcClient.h"
#include "vpnProtocol.h" #include "vpnProtocol.h"
class QTimer;
class XrayProtocol : public VpnProtocol class XrayProtocol : public VpnProtocol
{ {
public: public:
@@ -29,17 +25,10 @@ private:
ErrorCode setupRouting(); ErrorCode setupRouting();
ErrorCode startTun2Socks(); ErrorCode startTun2Socks();
bool probeServerReachable();
void runConnectivityProbe(std::function<void(bool)> onResult);
void startLivenessMonitor();
void stopLivenessMonitor();
int extractServerPort() const;
QJsonObject m_xrayConfig; QJsonObject m_xrayConfig;
amnezia::RouteMode m_routeMode; amnezia::RouteMode m_routeMode;
QList<QHostAddress> m_dnsServers; QList<QHostAddress> m_dnsServers;
QString m_remoteAddress; QString m_remoteAddress;
int m_serverPort = 0;
QString m_socksUser; QString m_socksUser;
QString m_socksPassword; QString m_socksPassword;
@@ -49,22 +38,6 @@ private:
int m_tun2socksRetryCount = 0; int m_tun2socksRetryCount = 0;
static constexpr int maxTun2SocksRetries = 5; static constexpr int maxTun2SocksRetries = 5;
static constexpr int tun2socksRetryDelayMs = 400; static constexpr int tun2socksRetryDelayMs = 400;
bool m_connectivityProbeStarted = false;
bool m_tunResourceBusy = false;
QTimer *m_livenessTimer = nullptr;
int m_livenessFailures = 0;
static constexpr int defaultServerProbeTimeoutMs = 5000;
static constexpr int defaultConnectivityProbeTimeoutMs = 7000;
static constexpr int defaultLivenessIntervalMs = 15000;
static constexpr int defaultMaxLivenessFailures = 3;
int m_serverProbeTimeoutMs = defaultServerProbeTimeoutMs;
int m_connectivityProbeTimeoutMs = defaultConnectivityProbeTimeoutMs;
int m_livenessIntervalMs = defaultLivenessIntervalMs;
int m_maxLivenessFailures = defaultMaxLivenessFailures;
}; };
#endif // XRAYPROTOCOL_H #endif // XRAYPROTOCOL_H
@@ -208,8 +208,8 @@ QString SecureServersRepository::nextAvailableServerName() const
int i = 0; int i = 0;
QString candidate; QString candidate;
do { do {
i++; ++i;
candidate = QStringLiteral("Server %1").arg(i); candidate = tr("Server") + QLatin1Char(' ') + QString::number(i);
} while (usedNames.contains(candidate)); } while (usedNames.contains(candidate));
return candidate; return candidate;
+2 -5
View File
@@ -38,6 +38,8 @@ namespace amnezia
XrayServerConfigInvalid = 215, XrayServerConfigInvalid = 215,
XrayServerNoVlessClients = 216, XrayServerNoVlessClients = 216,
XrayRealityKeysReadFailed = 217, XrayRealityKeysReadFailed = 217,
ServerContainerRuntimeNotSupported = 218,
ContainerRuntimeServiceNotRunning = 219,
// Ssh connection errors // Ssh connection errors
SshRequestDeniedError = 300, SshRequestDeniedError = 300,
@@ -71,9 +73,6 @@ namespace amnezia
OpenSslFailed = 800, OpenSslFailed = 800,
XrayExecutableCrashed = 803, XrayExecutableCrashed = 803,
Tun2SockExecutableCrashed = 804, Tun2SockExecutableCrashed = 804,
XrayServerUnreachable = 805,
XrayConnectivityCheckFailed = 806,
XrayConnectionLost = 807,
// import and install errors // import and install errors
ImportInvalidConfigError = 900, ImportInvalidConfigError = 900,
@@ -127,5 +126,3 @@ namespace amnezia
Q_DECLARE_METATYPE(amnezia::ErrorCode) Q_DECLARE_METATYPE(amnezia::ErrorCode)
#endif // ERRORCODES_H #endif // ERRORCODES_H
+2 -3
View File
@@ -39,6 +39,8 @@ QString errorString(ErrorCode code) {
case(ErrorCode::XrayRealityKeysReadFailed): case(ErrorCode::XrayRealityKeysReadFailed):
errorMessage = QObject::tr("Server error: failed to read XRay Reality keys from the server"); errorMessage = QObject::tr("Server error: failed to read XRay Reality keys from the server");
break; break;
case(ErrorCode::ServerContainerRuntimeNotSupported): errorMessage = QObject::tr("Server error: The default container runtime available for installation on this server is not supported.\n Install Docker Engine on the server manually and try again."); break;
case(ErrorCode::ContainerRuntimeServiceNotRunning): errorMessage = QObject::tr("Container runtime error: The container runtime service is not running.\n Check the container runtime service on the server, or wait about a minute and try again."); break;
// Libssh errors // Libssh errors
case(ErrorCode::SshRequestDeniedError): errorMessage = QObject::tr("SSH request was denied"); break; case(ErrorCode::SshRequestDeniedError): errorMessage = QObject::tr("SSH request was denied"); break;
@@ -59,9 +61,6 @@ QString errorString(ErrorCode code) {
case (ErrorCode::OpenVpnExecutableMissing): errorMessage = QObject::tr("OpenVPN executable missing"); break; case (ErrorCode::OpenVpnExecutableMissing): errorMessage = QObject::tr("OpenVPN executable missing"); break;
case (ErrorCode::AmneziaServiceConnectionFailed): errorMessage = QObject::tr("Amnezia helper service error"); break; case (ErrorCode::AmneziaServiceConnectionFailed): errorMessage = QObject::tr("Amnezia helper service error"); break;
case (ErrorCode::OpenSslFailed): errorMessage = QObject::tr("OpenSSL failed"); break; case (ErrorCode::OpenSslFailed): errorMessage = QObject::tr("OpenSSL failed"); break;
case (ErrorCode::XrayServerUnreachable): errorMessage = QObject::tr("Can't connect: the VPN server is unreachable"); break;
case (ErrorCode::XrayConnectivityCheckFailed): errorMessage = QObject::tr("Can't connect: no internet traffic flows through the tunnel"); break;
case (ErrorCode::XrayConnectionLost): errorMessage = QObject::tr("Connection lost: traffic stopped flowing through the tunnel"); break;
// VPN errors // VPN errors
case (ErrorCode::OpenVpnAdaptersInUseError): errorMessage = QObject::tr("Can't connect: another VPN connection is active"); break; case (ErrorCode::OpenVpnAdaptersInUseError): errorMessage = QObject::tr("Can't connect: another VPN connection is active"); break;
+2 -6
View File
@@ -8,7 +8,6 @@
#include <QFileInfo> #include <QFileInfo>
#include <QLocalSocket> #include <QLocalSocket>
#include "daemon.h"
#include "daemonlocalserverconnection.h" #include "daemonlocalserverconnection.h"
#include "leakdetector.h" #include "leakdetector.h"
#include "logger.h" #include "logger.h"
@@ -59,11 +58,8 @@ bool DaemonLocalServer::initialize() {
DaemonLocalServerConnection* connection = DaemonLocalServerConnection* connection =
new DaemonLocalServerConnection(&m_server, socket); new DaemonLocalServerConnection(&m_server, socket);
connect(socket, &QLocalSocket::disconnected, connection, [connection]() { connect(socket, &QLocalSocket::disconnected, connection,
logger.debug() << "Client connection dropped, deactivating daemon"; &DaemonLocalServerConnection::deleteLater);
Daemon::instance()->deactivate(true);
connection->deleteLater();
});
}); });
return true; return true;
@@ -25,6 +25,8 @@ set_target_properties(AmneziaVPNNetworkExtension PROPERTIES
XCODE_ATTRIBUTE_INFOPLIST_FILE ${CMAKE_CURRENT_SOURCE_DIR}/Info.plist.in XCODE_ATTRIBUTE_INFOPLIST_FILE ${CMAKE_CURRENT_SOURCE_DIR}/Info.plist.in
XCODE_ATTRIBUTE_LD_RUNPATH_SEARCH_PATHS "@executable_path/../../../../Frameworks @loader_path/../../../../Frameworks" XCODE_ATTRIBUTE_LD_RUNPATH_SEARCH_PATHS "@executable_path/../../../../Frameworks @loader_path/../../../../Frameworks"
XCODE_LINK_BUILD_PHASE_MODE KNOWN_LOCATION
) )
if(DEPLOY) if(DEPLOY)
@@ -118,10 +120,20 @@ target_include_directories(AmneziaVPNNetworkExtension PRIVATE ${CLIENT_ROOT_DIR}
target_include_directories(AmneziaVPNNetworkExtension PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) target_include_directories(AmneziaVPNNetworkExtension PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
find_package(openvpnadapter REQUIRED) find_package(openvpnadapter REQUIRED)
# FIXME(ygurov): https://github.com/conan-io/conan/issues/20034
set_property(TARGET amnezia::openvpnadapter APPEND PROPERTY IMPORTED_CONFIGURATIONS DEBUG)
set_property(TARGET amnezia::openvpnadapter APPEND PROPERTY IMPORTED_CONFIGURATIONS MINSIZEREL)
set_property(TARGET amnezia::openvpnadapter APPEND PROPERTY IMPORTED_CONFIGURATIONS RELEASE)
set_property(TARGET amnezia::openvpnadapter APPEND PROPERTY IMPORTED_CONFIGURATIONS RELWITHDEBINFO)
target_link_libraries(AmneziaVPNNetworkExtension PRIVATE amnezia::openvpnadapter) target_link_libraries(AmneziaVPNNetworkExtension PRIVATE amnezia::openvpnadapter)
find_package(awg-apple REQUIRED) find_package(awg-apple REQUIRED)
target_link_libraries(AmneziaVPNNetworkExtension PRIVATE amnezia::awg-apple) target_link_libraries(AmneziaVPNNetworkExtension PRIVATE amnezia::awg-apple)
find_package(hev-socks5-tunnel REQUIRED) find_package(hev-socks5-tunnel REQUIRED)
# FIXME(ygurov): https://github.com/conan-io/conan/issues/20034
set_property(TARGET heiher::hev-socks5-tunnel APPEND PROPERTY IMPORTED_CONFIGURATIONS DEBUG)
set_property(TARGET heiher::hev-socks5-tunnel APPEND PROPERTY IMPORTED_CONFIGURATIONS MINSIZEREL)
set_property(TARGET heiher::hev-socks5-tunnel APPEND PROPERTY IMPORTED_CONFIGURATIONS RELEASE)
set_property(TARGET heiher::hev-socks5-tunnel APPEND PROPERTY IMPORTED_CONFIGURATIONS RELWITHDEBINFO)
target_link_libraries(AmneziaVPNNetworkExtension PRIVATE heiher::hev-socks5-tunnel) target_link_libraries(AmneziaVPNNetworkExtension PRIVATE heiher::hev-socks5-tunnel)
@@ -1,7 +1,8 @@
if which apt-get > /dev/null 2>&1; then LOCK_CMD="fuser"; LOCK_FILE="/var/lib/dpkg/lock-frontend";\ if which apt-get > /dev/null 2>&1 || command -v apt-get > /dev/null 2>&1; then LOCK_CMD="fuser"; LOCK_FILE="/var/lib/dpkg/lock-frontend";\
elif which dnf > /dev/null 2>&1; then LOCK_CMD="fuser"; LOCK_FILE="/var/cache/dnf/* /var/run/dnf/* /var/lib/dnf/* /var/lib/rpm/*";\ elif which dnf > /dev/null 2>&1 || command -v dnf > /dev/null 2>&1; then LOCK_CMD="fuser"; LOCK_FILE="/var/cache/dnf/* /var/run/dnf/* /var/lib/dnf/* /var/lib/rpm/*";\
elif which yum > /dev/null 2>&1; then LOCK_CMD="cat"; LOCK_FILE="/var/run/yum.pid";\ elif which yum > /dev/null 2>&1 || command -v yum > /dev/null 2>&1; then LOCK_CMD="cat"; LOCK_FILE="/var/run/yum.pid";\
elif which zypper > /dev/null 2>&1; then LOCK_CMD="cat"; LOCK_FILE="/var/run/zypp.pid";\ elif which zypper > /dev/null 2>&1 || command -v zypper > /dev/null 2>&1; then LOCK_CMD="cat"; LOCK_FILE="/var/run/zypp.pid";\
elif which pacman > /dev/null 2>&1; then LOCK_CMD="fuser"; LOCK_FILE="/var/lib/pacman/db.lck";\ elif which pacman > /dev/null 2>&1 || command -v pacman > /dev/null 2>&1; then LOCK_CMD="fuser"; LOCK_FILE="/var/lib/pacman/db.lck";\
else echo "Packet manager not found"; echo "Internal error"; exit 1; fi;\ else echo "Packet manager not found"; echo "Internal error"; exit 1;\
if command -v $LOCK_CMD > /dev/null 2>&1; then sudo $LOCK_CMD $LOCK_FILE 2>/dev/null; else echo "$LOCK_CMD not installed"; fi fi;\
if sudo -n which $LOCK_CMD > /dev/null 2>&1 || command -v $LOCK_CMD > /dev/null 2>&1; then sudo -n $LOCK_CMD $LOCK_FILE 2>/dev/null; else echo "$LOCK_CMD not installed"; fi
+5 -5
View File
@@ -1,8 +1,8 @@
if which apt-get > /dev/null 2>&1; then pm=$(which apt-get); opt="--version";\ if pm=$(which apt-get 2>/dev/null || command -v apt-get 2>/dev/null); then opt="--version";\
elif which dnf > /dev/null 2>&1; then pm=$(which dnf); opt="--version";\ elif pm=$(which dnf 2>/dev/null || command -v dnf 2>/dev/null); then opt="--version";\
elif which yum > /dev/null 2>&1; then pm=$(which yum); opt="--version";\ elif pm=$(which yum 2>/dev/null || command -v yum 2>/dev/null); then opt="--version";\
elif which zypper > /dev/null 2>&1; then pm=$(which zypper); opt="--version";\ elif pm=$(which zypper 2>/dev/null || command -v zypper 2>/dev/null); then opt="--version";\
elif which pacman > /dev/null 2>&1; then pm=$(which pacman); opt="--version";\ elif pm=$(which pacman 2>/dev/null || command -v pacman 2>/dev/null); then opt="--version";\
else pm="uname"; opt="-a";\ else pm="uname"; opt="-a";\
fi;\ fi;\
CUR_USER=$(whoami 2>/dev/null || echo $HOME | sed 's/.*\///');\ CUR_USER=$(whoami 2>/dev/null || echo $HOME | sed 's/.*\///');\
+28 -19
View File
@@ -1,25 +1,34 @@
if which apt-get > /dev/null 2>&1; then pm=$(which apt-get); silent_inst="-yq install --install-recommends"; check_pkgs="-yq update"; docker_pkg="docker.io"; dist="debian";\ if pm=$(which apt-get 2>/dev/null || command -v apt-get 2>/dev/null); then silent_inst="-yq install --install-recommends"; what_pkg="-s install"; check_pkgs="-yq update"; docker_pkg="docker.io"; dist="debian";\
elif which dnf > /dev/null 2>&1; then pm=$(which dnf); silent_inst="-yq install"; check_pkgs="-yq check-update"; docker_pkg="docker"; dist="fedora";\ elif pm=$(which dnf 2>/dev/null || command -v dnf 2>/dev/null); then silent_inst="-yq install"; what_pkg="--assumeno install --setopt=tsflags=test"; check_pkgs="-yq check-update"; docker_pkg="docker"; dist="fedora";\
elif which yum > /dev/null 2>&1; then pm=$(which yum); silent_inst="-y -q install"; check_pkgs="-y -q check-update"; docker_pkg="docker"; dist="centos";\ elif pm=$(which yum 2>/dev/null || command -v yum 2>/dev/null); then silent_inst="-y -q install"; what_pkg="--assumeno install --setopt=tsflags=test"; check_pkgs="-y -q check-update"; docker_pkg="docker"; dist="centos";\
elif which zypper > /dev/null 2>&1; then pm=$(which zypper); silent_inst="-nq install"; check_pkgs="-nq refresh"; docker_pkg="docker"; dist="opensuse";\ elif pm=$(which zypper 2>/dev/null || command -v zypper 2>/dev/null); then silent_inst="-nq install"; what_pkg="--dry-run install"; check_pkgs="-nq refresh"; docker_pkg="docker"; dist="suse";\
elif which pacman > /dev/null 2>&1; then pm=$(which pacman); silent_inst="-S --noconfirm --noprogressbar --quiet"; check_pkgs="-Sup"; docker_pkg="docker"; dist="archlinux";\ elif pm=$(which pacman 2>/dev/null || command -v pacman 2>/dev/null); then silent_inst="-S --noconfirm --noprogressbar --quiet"; what_pkg="-Sp"; check_pkgs="-Sup"; docker_pkg="docker"; dist="archlinux";\
else echo "Packet manager not found"; exit 1; fi;\ fi;\
echo "Dist: $dist, Packet manager: $pm, Install command: $silent_inst, Check pkgs command: $check_pkgs, Docker pkg: $docker_pkg";\ echo "Dist: $dist, Packet manager: $pm, Install command: $silent_inst, What pkg command: $what_pkg, Check pkgs command: $check_pkgs, Docker pkg: $docker_pkg, Language: $LANG";\
echo $LANG | grep -qE '^(en_US.UTF-8|C.UTF-8|C)$' || export LC_ALL=C;\
if [ "$dist" = "debian" ]; then export DEBIAN_FRONTEND=noninteractive; fi;\ if [ "$dist" = "debian" ]; then export DEBIAN_FRONTEND=noninteractive; fi;\
if ! command -v sudo > /dev/null 2>&1; then $pm $check_pkgs; $pm $silent_inst sudo; fi;\ if ! command -v sudo > /dev/null 2>&1; then $pm $check_pkgs; $pm $silent_inst sudo; fi;\
if ! command -v fuser > /dev/null 2>&1; then sudo $pm $check_pkgs; sudo $pm $silent_inst psmisc; fi;\ if ! sudo -n sh -c 'command -v which > /dev/null 2>&1'; then sudo -n $pm $check_pkgs; sudo -n $pm $silent_inst which; fi;\
if ! command -v lsof > /dev/null 2>&1; then sudo $pm $check_pkgs; sudo $pm $silent_inst lsof; fi;\ if ! sudo -n sh -c 'command -v fuser > /dev/null 2>&1'; then sudo -n $pm $check_pkgs; sudo -n $pm $silent_inst psmisc; fi;\
if ! command -v docker > /dev/null 2>&1; then \ if ! sudo -n sh -c 'command -v lsof > /dev/null 2>&1'; then sudo -n $pm $check_pkgs; sudo -n $pm $silent_inst lsof; fi;\
sudo $pm $check_pkgs; sudo $pm $silent_inst $docker_pkg;\ if ! sudo -n sh -c 'command -v docker > /dev/null 2>&1'; then \
sleep 5; sudo systemctl enable --now docker; sleep 5;\ sudo -n $pm $check_pkgs;\
if ! sudo -n $pm $what_pkg $docker_pkg 2>/dev/null | grep -qi podman; then \
sudo -n $pm $silent_inst $docker_pkg;\
sleep 5; sudo -n systemctl enable --now docker; sleep 5;\
else \
echo "Container runtime is not supported";\
exit 1;\
fi;\ fi;\
if [ "$(cat /sys/module/apparmor/parameters/enabled 2>/dev/null)" = "Y" ]; then \
if ! command -v apparmor_parser > /dev/null 2>&1; then sudo $pm $check_pkgs; sudo $pm $silent_inst apparmor; fi;\
fi;\ fi;\
if [ "$(systemctl is-active docker)" != "active" ]; then \ if [ "$(sudo -n cat /sys/module/apparmor/parameters/enabled 2>/dev/null)" = "Y" ]; then \
sudo $pm $check_pkgs; sudo $pm $silent_inst $docker_pkg;\ if ! sudo -n sh -c 'command -v apparmor_parser > /dev/null 2>&1'; then \
sleep 5; sudo systemctl start docker; sleep 5;\ sudo -n $pm $check_pkgs; sudo -n $pm $silent_inst apparmor;\
fi;\ fi;\
if ! command -v sudo > /dev/null 2>&1; then echo "Failed to install sudo, command not found"; exit 1; fi;\ fi;\
docker --version;\ if [ "$(sudo -n systemctl is-active docker)" != "active" ]; then \
sleep 5; sudo -n systemctl start docker; sleep 5;\
if [ "$(sudo -n systemctl is-active docker)" != "active" ]; then echo "Container runtime service not running"; fi;\
fi;\
sudo -n docker --version || docker --version;\
uname -sr uname -sr
@@ -1,6 +1,6 @@
sudo docker ps -a | grep amnezia | awk '{print $1}' | xargs sudo docker stop;\ sudo docker ps -a | grep amnezia | awk '{print $1}' | xargs sudo docker stop;\
sudo docker ps -a | grep amnezia | awk '{print $1}' | xargs sudo docker rm -fv;\ sudo docker ps -a | grep amnezia | awk '{print $1}' | xargs sudo docker rm -fv;\
sudo docker images -a --format table | grep amnezia | awk '{print $3, $1 ":" $2}' | xargs sudo docker rmi;\ sudo docker images -a --format table | grep amnezia | awk '{print $3, $1 ":" $2}' | xargs sudo docker rmi;\
sudo docker volume ls | grep amnezia | awk '{print $2}' | xargs sudo docker volume rm -f;\ sudo docker volume ls --format '{{.Name}}' | grep '^amnezia-' | xargs -r sudo docker volume rm -f;\
sudo docker network ls | grep amnezia-dns-net | awk '{print $1}' | xargs sudo docker network rm;\ sudo docker network ls | grep amnezia-dns-net | awk '{print $1}' | xargs sudo docker network rm;\
sudo rm -frd /opt/amnezia sudo rm -frd /opt/amnezia
@@ -128,6 +128,11 @@ void PageController::showOnStartup()
} }
} }
bool PageController::shouldStartMinimized() const
{
return m_settingsController->isStartMinimizedEnabled();
}
bool PageController::isTriggeredByConnectButton() bool PageController::isTriggeredByConnectButton()
{ {
return m_isTriggeredByConnectButton; return m_isTriggeredByConnectButton;
@@ -123,6 +123,7 @@ public slots:
void updateNavigationBarColor(const int color); void updateNavigationBarColor(const int color);
void showOnStartup(); void showOnStartup();
bool shouldStartMinimized() const;
bool isTriggeredByConnectButton(); bool isTriggeredByConnectButton();
void setTriggeredByConnectButton(bool trigger); void setTriggeredByConnectButton(bool trigger);
@@ -9,6 +9,13 @@ ExportUiController::ExportUiController(ExportController* exportController, QObje
: QObject(parent), : QObject(parent),
m_exportController(exportController) m_exportController(exportController)
{ {
connect(m_exportController, &ExportController::revokeFinished, this, [this](ErrorCode errorCode) {
if (errorCode == ErrorCode::NoError) {
emit revokeConfigFinished();
} else {
emit exportErrorOccurred(errorCode);
}
});
} }
void ExportUiController::generateFullAccessConfig(const QString &serverId) void ExportUiController::generateFullAccessConfig(const QString &serverId)
@@ -92,7 +99,6 @@ void ExportUiController::updateClientManagementModel(const QString &serverId, in
void ExportUiController::revokeConfig(int row, const QString &serverId, int containerIndex) void ExportUiController::revokeConfig(int row, const QString &serverId, int containerIndex)
{ {
m_exportController->revokeConfig(row, serverId, containerIndex); m_exportController->revokeConfig(row, serverId, containerIndex);
emit revokeConfigFinished();
} }
void ExportUiController::renameClient(int row, const QString &clientName, const QString &serverId, int containerIndex) void ExportUiController::renameClient(int row, const QString &clientName, const QString &serverId, int containerIndex)
@@ -306,13 +306,16 @@ void InstallUiController::updateServerConfig(const QString &serverId, int contai
|| container == DockerContainer::Xray || container == DockerContainer::SSXray; || container == DockerContainer::Xray || container == DockerContainer::SSXray;
if (asyncUpdate) { if (asyncUpdate) {
const bool emitBusy = container == DockerContainer::MtProxy || container == DockerContainer::Telemt;
if (emitBusy)
emit serverIsBusy(true); emit serverIsBusy(true);
auto *watcher = new QFutureWatcher<ErrorCode>(this); auto *watcher = new QFutureWatcher<ErrorCode>(this);
const Proto protocolTypeCopy = protocolType; const Proto protocolTypeCopy = protocolType;
QObject::connect(watcher, &QFutureWatcher<ErrorCode>::finished, this, QObject::connect(watcher, &QFutureWatcher<ErrorCode>::finished, this,
[this, watcher, serverId, container, closePage, protocolTypeCopy]() { [this, watcher, serverId, container, closePage, protocolTypeCopy, emitBusy]() {
const ErrorCode errorCode = watcher->result(); const ErrorCode errorCode = watcher->result();
watcher->deleteLater(); watcher->deleteLater();
if (emitBusy)
emit serverIsBusy(false); emit serverIsBusy(false);
if (errorCode == ErrorCode::NoError) { if (errorCode == ErrorCode::NoError) {
+91 -3
View File
@@ -4,6 +4,10 @@
#include "core/protocols/protocolUtils.h" #include "core/protocols/protocolUtils.h"
#include "core/utils/constants/configKeys.h" #include "core/utils/constants/configKeys.h"
#include "core/utils/constants/protocolConstants.h" #include "core/utils/constants/protocolConstants.h"
#include "core/utils/networkUtilities.h"
#include <QHostAddress>
#include <QRegularExpression>
using namespace amnezia; using namespace amnezia;
using namespace ProtocolUtils; using namespace ProtocolUtils;
@@ -272,7 +276,7 @@ void XrayConfigModel::updateModel(amnezia::DockerContainer container, const amne
} }
if (!m_protocolConfig.serverConfig.isThirdPartyConfig) { if (!m_protocolConfig.serverConfig.isThirdPartyConfig) {
applyDefaultsToServerConfig(m_protocolConfig.serverConfig); applyDefaultsToServerConfig(m_protocolConfig.serverConfig, false);
} }
m_originalProtocolConfig = m_protocolConfig; m_originalProtocolConfig = m_protocolConfig;
@@ -283,7 +287,7 @@ void XrayConfigModel::updateModel(amnezia::DockerContainer container, const amne
} }
} }
void XrayConfigModel::applyDefaultsToServerConfig(amnezia::XrayServerConfig &config) void XrayConfigModel::applyDefaultsToServerConfig(amnezia::XrayServerConfig &config, bool fillFlowDefault)
{ {
if (config.port.isEmpty()) { if (config.port.isEmpty()) {
config.port = protocols::xray::defaultPort; config.port = protocols::xray::defaultPort;
@@ -306,7 +310,7 @@ void XrayConfigModel::applyDefaultsToServerConfig(amnezia::XrayServerConfig &con
config.security = protocols::xray::defaultSecurity; config.security = protocols::xray::defaultSecurity;
} }
if (config.flow.isEmpty()) { if (fillFlowDefault && config.flow.isEmpty()) {
config.flow = protocols::xray::defaultFlow; config.flow = protocols::xray::defaultFlow;
} }
@@ -585,3 +589,87 @@ QString XrayConfigModel::mkcpDefaultWriteBufferSize()
{ {
return QString::fromLatin1(protocols::xray::defaultMkcpWriteBufferSize); return QString::fromLatin1(protocols::xray::defaultMkcpWriteBufferSize);
} }
namespace {
bool isValidSingleHost(const QString &t)
{
if (t.isEmpty() || t.length() > 253) {
return false;
}
QHostAddress a(t);
if (a.protocol() == QHostAddress::IPv4Protocol) {
return NetworkUtilities::checkIPv4Format(t);
}
if (a.protocol() == QHostAddress::IPv6Protocol) {
return true;
}
static const QRegularExpression onlyDigits(QStringLiteral(R"(^\d+$)"));
if (onlyDigits.match(t).hasMatch()) {
return false;
}
QRegExp re = NetworkUtilities::domainRegExp();
re.setCaseSensitivity(Qt::CaseInsensitive);
return re.exactMatch(t);
}
}
bool XrayConfigModel::isValidHost(const QString &host)
{
const QString t = host.trimmed();
if (t.isEmpty()) {
return true;
}
return isValidSingleHost(t);
}
bool XrayConfigModel::isValidSni(const QString &sni)
{
const QString t = sni.trimmed();
if (t.isEmpty()) {
return true;
}
if (t.startsWith(QLatin1String("*."))) {
return isValidSingleHost(t.mid(2));
}
return isValidSingleHost(t);
}
bool XrayConfigModel::isValidPath(const QString &path)
{
const QString t = path.trimmed();
if (t.isEmpty()) {
return true;
}
return t.startsWith(QLatin1Char('/'));
}
QStringList XrayConfigModel::validationErrors() const
{
QStringList errs;
const auto &srv = m_protocolConfig.serverConfig;
if (!srv.port.isEmpty()) {
bool ok = false;
const int p = srv.port.toInt(&ok);
if (!ok || p < 1 || p > 65535) {
errs << tr("Port must be in the range of 1 to 65535");
}
}
if (srv.security == QLatin1String("tls") || srv.security == QLatin1String("reality")) {
if (!isValidSni(srv.sni)) {
errs << tr("SNI: enter a valid IP address or domain name");
}
}
if (srv.transport == QLatin1String("xhttp")) {
if (!isValidHost(srv.xhttp.host)) {
errs << tr("Host: enter a valid IP address or domain name");
}
if (!isValidPath(srv.xhttp.path)) {
errs << tr("Path must start with \"/\"");
}
}
return errs;
}
+6 -1
View File
@@ -118,6 +118,11 @@ public:
Q_INVOKABLE static QString mkcpDefaultReadBufferSize(); Q_INVOKABLE static QString mkcpDefaultReadBufferSize();
Q_INVOKABLE static QString mkcpDefaultWriteBufferSize(); Q_INVOKABLE static QString mkcpDefaultWriteBufferSize();
Q_INVOKABLE static bool isValidHost(const QString &host);
Q_INVOKABLE static bool isValidSni(const QString &sni);
Q_INVOKABLE static bool isValidPath(const QString &path);
Q_INVOKABLE QStringList validationErrors() const;
public slots: public slots:
void updateModel(amnezia::DockerContainer container, const amnezia::XrayProtocolConfig& protocolConfig); void updateModel(amnezia::DockerContainer container, const amnezia::XrayProtocolConfig& protocolConfig);
amnezia::XrayProtocolConfig getProtocolConfig(); amnezia::XrayProtocolConfig getProtocolConfig();
@@ -137,7 +142,7 @@ private:
amnezia::XrayProtocolConfig m_protocolConfig; amnezia::XrayProtocolConfig m_protocolConfig;
amnezia::XrayProtocolConfig m_originalProtocolConfig; amnezia::XrayProtocolConfig m_originalProtocolConfig;
void applyDefaultsToServerConfig(amnezia::XrayServerConfig& config); void applyDefaultsToServerConfig(amnezia::XrayServerConfig& config, bool fillFlowDefault = true);
}; };
#endif // XRAYCONFIGMODEL_H #endif // XRAYCONFIGMODEL_H
+11 -1
View File
@@ -42,6 +42,7 @@ Item {
property int rootButtonTextBottomMargin: 16 property int rootButtonTextBottomMargin: 16
property real drawerHeight: 0.9 property real drawerHeight: 0.9
property bool fitContent: false
property Item drawerParent property Item drawerParent
property Component listView property Component listView
@@ -219,12 +220,20 @@ Item {
parent: drawerParent parent: drawerParent
anchors.fill: parent anchors.fill: parent
expandedHeight: drawerParent.height * drawerHeight property real measuredContentHeight: 0
expandedHeight: (root.fitContent && measuredContentHeight > 0)
? Math.min(measuredContentHeight, drawerParent.height * root.drawerHeight)
: drawerParent.height * root.drawerHeight
expandedStateContent: Item { expandedStateContent: Item {
id: container id: container
implicitHeight: menu.expandedHeight implicitHeight: menu.expandedHeight
property real fitHeight: backButton.implicitHeight + titleLabel.implicitHeight
+ (listViewLoader.item ? listViewLoader.item.contentHeight : 0) + 48
onFitHeightChanged: menu.measuredContentHeight = fitHeight
Component.onCompleted: menu.measuredContentHeight = fitHeight
ColumnLayout { ColumnLayout {
id: header id: header
@@ -238,6 +247,7 @@ Item {
} }
Header2Type { Header2Type {
id: titleLabel
Layout.leftMargin: 16 Layout.leftMargin: 16
Layout.rightMargin: 16 Layout.rightMargin: 16
Layout.bottomMargin: 16 Layout.bottomMargin: 16
+101 -14
View File
@@ -12,8 +12,8 @@ import "../Controls2/TextTypes"
// MinMaxRowType { // MinMaxRowType {
// minValue: "0" // minValue: "0"
// maxValue: "0" // maxValue: "0"
// onMinChanged: someProperty = val // onMinChanged: function(val) { someProperty = val }
// onMaxChanged: someProperty = val // onMaxChanged: function(val) { someProperty = val }
// } // }
Item { Item {
id: root id: root
@@ -21,41 +21,128 @@ Item {
property string minValue: "0" property string minValue: "0"
property string maxValue: "0" property string maxValue: "0"
property int minLimit: 0
property int maxLimit: 2147483647
property string hintText: root.minLimit > 0
? (root.minLimit + "" + root.maxLimit)
: ("≤ " + root.maxLimit)
signal minChanged(string val) signal minChanged(string val)
signal maxChanged(string val) signal maxChanged(string val)
signal edited()
implicitHeight: row.implicitHeight implicitHeight: col.implicitHeight
implicitWidth: row.implicitWidth implicitWidth: col.implicitWidth
function clampValue(text) {
if (text === "")
return ""
var n = parseInt(text, 10)
if (isNaN(n))
return ""
if (n < root.minLimit)
n = root.minLimit
if (n > root.maxLimit)
n = root.maxLimit
return String(n)
}
function capEdit(tf, holder) {
if (tf.text !== "" && parseInt(tf.text, 10) > root.maxLimit) {
tf.text = holder.lastValid
tf.cursorPosition = tf.text.length
} else {
holder.lastValid = tf.text
}
}
ColumnLayout {
id: col
anchors.fill: parent
spacing: 4
RowLayout { RowLayout {
id: row id: row
anchors.fill: parent Layout.fillWidth: true
spacing: 10 spacing: 10
// Min field // Min field
TextFieldWithHeaderType { TextFieldWithHeaderType {
id: minField
property string lastValid: ""
Layout.fillWidth: true Layout.fillWidth: true
headerText: qsTr("Min") headerText: qsTr("Min")
textField.text: root.minValue textField.maximumLength: 10
textField.validator: IntValidator { bottom: 0 } textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ }
textField.onEditingFinished: { textField.onActiveFocusChanged: {
if (textField.text !== root.minValue) { if (minField.textField.activeFocus)
root.minChanged(textField.text) minField.lastValid = minField.textField.text
} }
textField.onTextEdited: { root.capEdit(minField.textField, minField); root.edited() }
textField.onEditingFinished: {
var v = root.clampValue(minField.textField.text)
if (v !== "" && root.maxValue !== "") {
var mx = parseInt(root.maxValue, 10)
if (!isNaN(mx) && parseInt(v, 10) > mx)
root.maxChanged(v)
}
if (v !== root.minValue)
root.minChanged(v)
else if (minField.textField.text !== v)
minField.textField.text = v
}
Binding {
target: minField.textField
property: "text"
value: root.minValue
when: !minField.textField.activeFocus
restoreMode: Binding.RestoreNone
} }
} }
// Max field // Max field
TextFieldWithHeaderType { TextFieldWithHeaderType {
id: maxField
property string lastValid: ""
Layout.fillWidth: true Layout.fillWidth: true
headerText: qsTr("Max") headerText: qsTr("Max")
textField.text: root.maxValue textField.maximumLength: 10
textField.validator: IntValidator { bottom: 0 } textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ }
textField.onActiveFocusChanged: {
if (maxField.textField.activeFocus)
maxField.lastValid = maxField.textField.text
}
textField.onTextEdited: { root.capEdit(maxField.textField, maxField); root.edited() }
textField.onEditingFinished: { textField.onEditingFinished: {
if (textField.text !== root.maxValue) { var v = root.clampValue(maxField.textField.text)
root.maxChanged(textField.text) if (v !== "" && root.minValue !== "") {
var mn = parseInt(root.minValue, 10)
if (!isNaN(mn) && parseInt(v, 10) < mn)
v = String(mn)
}
if (v !== root.maxValue)
root.maxChanged(v)
else if (maxField.textField.text !== v)
maxField.textField.text = v
}
Binding {
target: maxField.textField
property: "text"
value: root.maxValue
when: !maxField.textField.activeFocus
restoreMode: Binding.RestoreNone
} }
} }
} }
SmallTextType {
visible: root.hintText !== ""
text: root.hintText
color: AmneziaStyle.color.mutedGray
Layout.fillWidth: true
}
} }
} }
@@ -15,6 +15,8 @@ import "../Components"
PageType { PageType {
id: root id: root
property bool editDirty: false
BackButtonType { BackButtonType {
id: backButton id: backButton
anchors.top: parent.top anchors.top: parent.top
@@ -90,6 +92,7 @@ PageType {
DropDownType { DropDownType {
id: tlsAlpnDropDown id: tlsAlpnDropDown
fitContent: true
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 16 Layout.topMargin: 16
Layout.leftMargin: 16 Layout.leftMargin: 16
@@ -133,6 +136,7 @@ PageType {
DropDownType { DropDownType {
id: tlsFingerprintDropDown id: tlsFingerprintDropDown
fitContent: true
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 8 Layout.topMargin: 8
Layout.leftMargin: 16 Layout.leftMargin: 16
@@ -175,14 +179,21 @@ PageType {
} }
TextFieldWithHeaderType { TextFieldWithHeaderType {
id: sniFieldTls
Layout.fillWidth: true Layout.fillWidth: true
Layout.leftMargin: 16 Layout.leftMargin: 16
Layout.rightMargin: 16 Layout.rightMargin: 16
Layout.topMargin: 8 Layout.topMargin: 8
headerText: qsTr("Server Name (SNI)") headerText: qsTr("Server Name (SNI)")
textField.text: sni textField.text: sni
textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9.*_-]*$/ }
textField.onTextEdited: root.editDirty = (textField.text !== sni)
textField.onEditingFinished: { textField.onEditingFinished: {
if (textField.text !== sni) sni = textField.text var v = textField.text.trim()
if (v !== sni) sni = v
else if (textField.text !== v) textField.text = v
sniFieldTls.errorText = XrayConfigModel.isValidSni(v) ? "" : qsTr("Enter a valid IP address or domain name")
root.editDirty = false
} }
} }
} }
@@ -195,6 +206,7 @@ PageType {
DropDownType { DropDownType {
id: realityFingerprintDropDown id: realityFingerprintDropDown
fitContent: true
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 16 Layout.topMargin: 16
Layout.leftMargin: 16 Layout.leftMargin: 16
@@ -237,14 +249,21 @@ PageType {
} }
TextFieldWithHeaderType { TextFieldWithHeaderType {
id: sniFieldReality
Layout.fillWidth: true Layout.fillWidth: true
Layout.leftMargin: 16 Layout.leftMargin: 16
Layout.rightMargin: 16 Layout.rightMargin: 16
Layout.topMargin: 8 Layout.topMargin: 8
headerText: qsTr("Server Name (SNI)") headerText: qsTr("Server Name (SNI)")
textField.text: sni textField.text: sni
textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9.*_-]*$/ }
textField.onTextEdited: root.editDirty = (textField.text !== sni)
textField.onEditingFinished: { textField.onEditingFinished: {
if (textField.text !== sni) sni = textField.text var v = textField.text.trim()
if (v !== sni) sni = v
else if (textField.text !== v) textField.text = v
sniFieldReality.errorText = XrayConfigModel.isValidSni(v) ? "" : qsTr("Enter a valid IP address or domain name")
root.editDirty = false
} }
} }
} }
@@ -265,10 +284,15 @@ PageType {
anchors.rightMargin: 16 anchors.rightMargin: 16
anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin
visible: listView.enabled && XrayConfigModel.hasUnsavedChanges visible: listView.enabled && (XrayConfigModel.hasUnsavedChanges || root.editDirty)
enabled: visible enabled: visible
text: qsTr("Save") text: qsTr("Save")
clickedFunc: function () { clickedFunc: function () {
var errs = XrayConfigModel.validationErrors()
if (errs.length > 0) {
PageController.showErrorMessage(errs.join("\n"))
return
}
var headerText = qsTr("Save settings?") var headerText = qsTr("Save settings?")
var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.") var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.")
var yesButtonText = qsTr("Continue") var yesButtonText = qsTr("Continue")
@@ -109,6 +109,7 @@ PageType {
Layout.rightMargin: 16 Layout.rightMargin: 16
enabled: listView.enabled enabled: listView.enabled
headerText: qsTr("Port") headerText: qsTr("Port")
subtitleText: qsTr("165535")
Binding { Binding {
target: textFieldWithHeaderType.textField target: textFieldWithHeaderType.textField
@@ -119,8 +120,8 @@ PageType {
} }
textField.maximumLength: 5 textField.maximumLength: 5
textField.validator: IntValidator { textField.validator: RegularExpressionValidator {
bottom: 1; top: 65535 regularExpression: /^(|\d{1,4}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$/
} }
textField.onActiveFocusChanged: { textField.onActiveFocusChanged: {
if (textField.activeFocus && textField.text === "" && port !== "") { if (textField.activeFocus && textField.text === "" && port !== "") {
@@ -131,9 +132,19 @@ PageType {
root.portDirty = (textField.text !== port) root.portDirty = (textField.text !== port)
} }
textField.onEditingFinished: { textField.onEditingFinished: {
if (textField.text !== port) { var v = textFieldWithHeaderType.textField.text
port = textField.text if (v !== "") {
var n = parseInt(v, 10)
if (isNaN(n) || n < 1)
n = 1
if (n > 65535)
n = 65535
v = String(n)
if (textFieldWithHeaderType.textField.text !== v)
textFieldWithHeaderType.textField.text = v
} }
if (v !== port)
port = v
root.portDirty = false root.portDirty = false
} }
checkEmptyText: true checkEmptyText: true
@@ -198,6 +209,11 @@ PageType {
text: qsTr("Save") text: qsTr("Save")
onClicked: function() { onClicked: function() {
forceActiveFocus() forceActiveFocus()
var errs = XrayConfigModel.validationErrors()
if (errs.length > 0) {
PageController.showErrorMessage(errs.join("\n"))
return
}
var headerText = qsTr("Save settings?") var headerText = qsTr("Save settings?")
var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.") var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.")
var yesButtonText = qsTr("Continue") var yesButtonText = qsTr("Continue")
@@ -15,6 +15,21 @@ import "../Components"
PageType { PageType {
id: root id: root
property bool editDirty: false
function clampInt(text, lo, hi) {
if (text === "")
return ""
var n = parseInt(text, 10)
if (isNaN(n))
return ""
if (n < lo)
n = lo
if (n > hi)
n = hi
return String(n)
}
BackButtonType { BackButtonType {
id: backButton id: backButton
anchors.top: parent.top anchors.top: parent.top
@@ -108,10 +123,16 @@ PageType {
Layout.rightMargin: 16 Layout.rightMargin: 16
Layout.topMargin: 8 Layout.topMargin: 8
headerText: qsTr("TTI") headerText: qsTr("TTI")
subtitleText: qsTr("Default: %1 ms", "mKCP TTI").arg(XrayConfigModel.mkcpDefaultTti()) subtitleText: qsTr("Range 10100, default %1 ms", "mKCP TTI").arg(XrayConfigModel.mkcpDefaultTti())
textField.text: mkcpTti textField.text: mkcpTti
textField.maximumLength: 3
textField.validator: RegularExpressionValidator { regularExpression: /^(|\d{1,2}|100)$/ }
textField.onTextEdited: root.editDirty = (textField.text !== mkcpTti)
textField.onEditingFinished: { textField.onEditingFinished: {
if (textField.text !== mkcpTti) mkcpTti = textField.text var v = root.clampInt(textField.text, 10, 100)
if (v !== mkcpTti) mkcpTti = v
else if (textField.text !== v) textField.text = v
root.editDirty = false
} }
} }
@@ -121,10 +142,16 @@ PageType {
Layout.rightMargin: 16 Layout.rightMargin: 16
Layout.topMargin: 8 Layout.topMargin: 8
headerText: qsTr("uplinkCapacity") headerText: qsTr("uplinkCapacity")
subtitleText: qsTr("Default: %1 Mbit/s", "mKCP uplink").arg(XrayConfigModel.mkcpDefaultUplinkCapacity()) subtitleText: qsTr("≥ 0, default %1 MB/s", "mKCP uplink").arg(XrayConfigModel.mkcpDefaultUplinkCapacity())
textField.text: mkcpUplinkCapacity textField.text: mkcpUplinkCapacity
textField.maximumLength: 10
textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ }
textField.onTextEdited: root.editDirty = (textField.text !== mkcpUplinkCapacity)
textField.onEditingFinished: { textField.onEditingFinished: {
if (textField.text !== mkcpUplinkCapacity) mkcpUplinkCapacity = textField.text var v = root.clampInt(textField.text, 0, 2147483647)
if (v !== mkcpUplinkCapacity) mkcpUplinkCapacity = v
else if (textField.text !== v) textField.text = v
root.editDirty = false
} }
} }
@@ -134,10 +161,16 @@ PageType {
Layout.rightMargin: 16 Layout.rightMargin: 16
Layout.topMargin: 8 Layout.topMargin: 8
headerText: qsTr("downlinkCapacity") headerText: qsTr("downlinkCapacity")
subtitleText: qsTr("Default: %1 Mbit/s", "mKCP downlink").arg(XrayConfigModel.mkcpDefaultDownlinkCapacity()) subtitleText: qsTr("≥ 0, default %1 MB/s", "mKCP downlink").arg(XrayConfigModel.mkcpDefaultDownlinkCapacity())
textField.text: mkcpDownlinkCapacity textField.text: mkcpDownlinkCapacity
textField.maximumLength: 10
textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ }
textField.onTextEdited: root.editDirty = (textField.text !== mkcpDownlinkCapacity)
textField.onEditingFinished: { textField.onEditingFinished: {
if (textField.text !== mkcpDownlinkCapacity) mkcpDownlinkCapacity = textField.text var v = root.clampInt(textField.text, 0, 2147483647)
if (v !== mkcpDownlinkCapacity) mkcpDownlinkCapacity = v
else if (textField.text !== v) textField.text = v
root.editDirty = false
} }
} }
@@ -147,10 +180,16 @@ PageType {
Layout.rightMargin: 16 Layout.rightMargin: 16
Layout.topMargin: 8 Layout.topMargin: 8
headerText: qsTr("readBufferSize") headerText: qsTr("readBufferSize")
subtitleText: qsTr("Default: %1 MiB").arg(XrayConfigModel.mkcpDefaultReadBufferSize()) subtitleText: qsTr("≥ 1, default %1 MB").arg(XrayConfigModel.mkcpDefaultReadBufferSize())
textField.text: mkcpReadBufferSize textField.text: mkcpReadBufferSize
textField.maximumLength: 10
textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ }
textField.onTextEdited: root.editDirty = (textField.text !== mkcpReadBufferSize)
textField.onEditingFinished: { textField.onEditingFinished: {
if (textField.text !== mkcpReadBufferSize) mkcpReadBufferSize = textField.text var v = root.clampInt(textField.text, 1, 2147483647)
if (v !== mkcpReadBufferSize) mkcpReadBufferSize = v
else if (textField.text !== v) textField.text = v
root.editDirty = false
} }
} }
@@ -160,10 +199,16 @@ PageType {
Layout.rightMargin: 16 Layout.rightMargin: 16
Layout.topMargin: 8 Layout.topMargin: 8
headerText: qsTr("writeBufferSize") headerText: qsTr("writeBufferSize")
subtitleText: qsTr("Default: %1 MiB").arg(XrayConfigModel.mkcpDefaultWriteBufferSize()) subtitleText: qsTr("≥ 1, default %1 MB").arg(XrayConfigModel.mkcpDefaultWriteBufferSize())
textField.text: mkcpWriteBufferSize textField.text: mkcpWriteBufferSize
textField.maximumLength: 10
textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ }
textField.onTextEdited: root.editDirty = (textField.text !== mkcpWriteBufferSize)
textField.onEditingFinished: { textField.onEditingFinished: {
if (textField.text !== mkcpWriteBufferSize) mkcpWriteBufferSize = textField.text var v = root.clampInt(textField.text, 1, 2147483647)
if (v !== mkcpWriteBufferSize) mkcpWriteBufferSize = v
else if (textField.text !== v) textField.text = v
root.editDirty = false
} }
} }
@@ -187,6 +232,7 @@ PageType {
DropDownType { DropDownType {
id: modeDropDown id: modeDropDown
fitContent: true
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 16 Layout.topMargin: 16
Layout.leftMargin: 16 Layout.leftMargin: 16
@@ -239,31 +285,46 @@ PageType {
} }
TextFieldWithHeaderType { TextFieldWithHeaderType {
id: hostField
Layout.fillWidth: true Layout.fillWidth: true
Layout.leftMargin: 16 Layout.leftMargin: 16
Layout.rightMargin: 16 Layout.rightMargin: 16
Layout.topMargin: 8 Layout.topMargin: 8
headerText: qsTr("Host") headerText: qsTr("Host")
textField.text: xhttpHost textField.text: xhttpHost
textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9._:,-]*$/ }
textField.onTextEdited: root.editDirty = (textField.text !== xhttpHost)
textField.onEditingFinished: { textField.onEditingFinished: {
if (textField.text !== xhttpHost) xhttpHost = textField.text var v = textField.text.trim()
if (v !== xhttpHost) xhttpHost = v
else if (textField.text !== v) textField.text = v
hostField.errorText = XrayConfigModel.isValidHost(v) ? "" : qsTr("Enter a valid IP address or domain name")
root.editDirty = false
} }
} }
TextFieldWithHeaderType { TextFieldWithHeaderType {
id: pathField
Layout.fillWidth: true Layout.fillWidth: true
Layout.leftMargin: 16 Layout.leftMargin: 16
Layout.rightMargin: 16 Layout.rightMargin: 16
Layout.topMargin: 8 Layout.topMargin: 8
headerText: qsTr("Path") headerText: qsTr("Path")
textField.text: xhttpPath textField.text: xhttpPath
textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9\-._~:\/?#\[\]@!$&'()*+,;=%]*$/ }
textField.onTextEdited: root.editDirty = (textField.text !== xhttpPath)
textField.onEditingFinished: { textField.onEditingFinished: {
if (textField.text !== xhttpPath) xhttpPath = textField.text var v = textField.text.trim()
if (v !== xhttpPath) xhttpPath = v
else if (textField.text !== v) textField.text = v
pathField.errorText = XrayConfigModel.isValidPath(v) ? "" : qsTr("Path must start with \"/\"")
root.editDirty = false
} }
} }
DropDownType { DropDownType {
id: headersDropDown id: headersDropDown
fitContent: true
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 8 Layout.topMargin: 8
Layout.leftMargin: 16 Layout.leftMargin: 16
@@ -307,6 +368,7 @@ PageType {
DropDownType { DropDownType {
id: uplinkMethodDropDown id: uplinkMethodDropDown
fitContent: true
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 8 Layout.topMargin: 8
Layout.leftMargin: 16 Layout.leftMargin: 16
@@ -386,6 +448,7 @@ PageType {
DropDownType { DropDownType {
id: sessionPlacementDropDown id: sessionPlacementDropDown
fitContent: true
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 8 Layout.topMargin: 8
Layout.leftMargin: 16 Layout.leftMargin: 16
@@ -429,6 +492,7 @@ PageType {
DropDownType { DropDownType {
id: sessionKeyDropDown id: sessionKeyDropDown
fitContent: true
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 8 Layout.topMargin: 8
Layout.leftMargin: 16 Layout.leftMargin: 16
@@ -472,6 +536,7 @@ PageType {
DropDownType { DropDownType {
id: seqPlacementDropDown id: seqPlacementDropDown
fitContent: true
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 8 Layout.topMargin: 8
Layout.leftMargin: 16 Layout.leftMargin: 16
@@ -520,13 +585,19 @@ PageType {
Layout.topMargin: 8 Layout.topMargin: 8
headerText: qsTr("SeqKey") headerText: qsTr("SeqKey")
textField.text: xhttpSeqKey textField.text: xhttpSeqKey
textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9_-]*$/ }
textField.onTextEdited: root.editDirty = (textField.text !== xhttpSeqKey)
textField.onEditingFinished: { textField.onEditingFinished: {
if (textField.text !== xhttpSeqKey) xhttpSeqKey = textField.text var v = textField.text.trim()
if (v !== xhttpSeqKey) xhttpSeqKey = v
else if (textField.text !== v) textField.text = v
root.editDirty = false
} }
} }
DropDownType { DropDownType {
id: uplinkDataPlacementDropDown id: uplinkDataPlacementDropDown
fitContent: true
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 8 Layout.topMargin: 8
Layout.leftMargin: 16 Layout.leftMargin: 16
@@ -575,8 +646,13 @@ PageType {
Layout.topMargin: 8 Layout.topMargin: 8
headerText: qsTr("UplinkDataKey") headerText: qsTr("UplinkDataKey")
textField.text: xhttpUplinkDataKey textField.text: xhttpUplinkDataKey
textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9_-]*$/ }
textField.onTextEdited: root.editDirty = (textField.text !== xhttpUplinkDataKey)
textField.onEditingFinished: { textField.onEditingFinished: {
if (textField.text !== xhttpUplinkDataKey) xhttpUplinkDataKey = textField.text var v = textField.text.trim()
if (v !== xhttpUplinkDataKey) xhttpUplinkDataKey = v
else if (textField.text !== v) textField.text = v
root.editDirty = false
} }
} }
@@ -597,12 +673,16 @@ PageType {
Layout.rightMargin: 16 Layout.rightMargin: 16
Layout.topMargin: 8 Layout.topMargin: 8
headerText: qsTr("UplinkChunkSize") headerText: qsTr("UplinkChunkSize")
subtitleText: qsTr("≥ 0 (0 = off)")
textField.text: xhttpUplinkChunkSize textField.text: xhttpUplinkChunkSize
textField.validator: IntValidator { textField.maximumLength: 10
bottom: 0 textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ }
} textField.onTextEdited: root.editDirty = (textField.text !== xhttpUplinkChunkSize)
textField.onEditingFinished: { textField.onEditingFinished: {
if (textField.text !== xhttpUplinkChunkSize) xhttpUplinkChunkSize = textField.text var v = root.clampInt(textField.text, 0, 2147483647)
if (v !== xhttpUplinkChunkSize) xhttpUplinkChunkSize = v
else if (textField.text !== v) textField.text = v
root.editDirty = false
} }
} }
@@ -612,9 +692,16 @@ PageType {
Layout.rightMargin: 16 Layout.rightMargin: 16
Layout.topMargin: 8 Layout.topMargin: 8
headerText: qsTr("scMaxBufferedPosts") headerText: qsTr("scMaxBufferedPosts")
subtitleText: qsTr("≥ 0")
textField.text: xhttpScMaxBufferedPosts textField.text: xhttpScMaxBufferedPosts
textField.maximumLength: 10
textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ }
textField.onTextEdited: root.editDirty = (textField.text !== xhttpScMaxBufferedPosts)
textField.onEditingFinished: { textField.onEditingFinished: {
if (textField.text !== xhttpScMaxBufferedPosts) xhttpScMaxBufferedPosts = textField.text var v = root.clampInt(textField.text, 0, 2147483647)
if (v !== xhttpScMaxBufferedPosts) xhttpScMaxBufferedPosts = v
else if (textField.text !== v) textField.text = v
root.editDirty = false
} }
} }
@@ -633,8 +720,9 @@ PageType {
Layout.rightMargin: 16 Layout.rightMargin: 16
minValue: xhttpScMaxEachPostBytesMin minValue: xhttpScMaxEachPostBytesMin
maxValue: xhttpScMaxEachPostBytesMax maxValue: xhttpScMaxEachPostBytesMax
onMinChanged: xhttpScMaxEachPostBytesMin = val onMinChanged: function(val) { xhttpScMaxEachPostBytesMin = val; root.editDirty = false }
onMaxChanged: xhttpScMaxEachPostBytesMax = val onMaxChanged: function(val) { xhttpScMaxEachPostBytesMax = val; root.editDirty = false }
onEdited: root.editDirty = true
} }
CaptionTextType { CaptionTextType {
@@ -652,8 +740,9 @@ PageType {
Layout.rightMargin: 16 Layout.rightMargin: 16
minValue: xhttpScStreamUpServerSecsMin minValue: xhttpScStreamUpServerSecsMin
maxValue: xhttpScStreamUpServerSecsMax maxValue: xhttpScStreamUpServerSecsMax
onMinChanged: xhttpScStreamUpServerSecsMin = val onMinChanged: function(val) { xhttpScStreamUpServerSecsMin = val; root.editDirty = false }
onMaxChanged: xhttpScStreamUpServerSecsMax = val onMaxChanged: function(val) { xhttpScStreamUpServerSecsMax = val; root.editDirty = false }
onEdited: root.editDirty = true
} }
CaptionTextType { CaptionTextType {
@@ -671,8 +760,9 @@ PageType {
Layout.rightMargin: 16 Layout.rightMargin: 16
minValue: xhttpScMinPostsIntervalMsMin minValue: xhttpScMinPostsIntervalMsMin
maxValue: xhttpScMinPostsIntervalMsMax maxValue: xhttpScMinPostsIntervalMsMax
onMinChanged: xhttpScMinPostsIntervalMsMin = val onMinChanged: function(val) { xhttpScMinPostsIntervalMsMin = val; root.editDirty = false }
onMaxChanged: xhttpScMinPostsIntervalMsMax = val onMaxChanged: function(val) { xhttpScMinPostsIntervalMsMax = val; root.editDirty = false }
onEdited: root.editDirty = true
} }
// Padding and multiplexing // Padding and multiplexing
@@ -728,10 +818,15 @@ PageType {
anchors.rightMargin: 16 anchors.rightMargin: 16
anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin
visible: listView.enabled && XrayConfigModel.hasUnsavedChanges visible: listView.enabled && (XrayConfigModel.hasUnsavedChanges || root.editDirty)
enabled: visible enabled: visible
text: qsTr("Save") text: qsTr("Save")
clickedFunc: function () { clickedFunc: function () {
var errs = XrayConfigModel.validationErrors()
if (errs.length > 0) {
PageController.showErrorMessage(errs.join("\n"))
return
}
var headerText = qsTr("Save settings?") var headerText = qsTr("Save settings?")
var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.") var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.")
var yesButtonText = qsTr("Continue") var yesButtonText = qsTr("Continue")
@@ -15,6 +15,8 @@ import "../Components"
PageType { PageType {
id: root id: root
property bool editDirty: false
BackButtonType { BackButtonType {
id: backButton id: backButton
anchors.top: parent.top anchors.top: parent.top
@@ -61,8 +63,9 @@ PageType {
Layout.rightMargin: 16 Layout.rightMargin: 16
minValue: xPaddingBytesMin minValue: xPaddingBytesMin
maxValue: xPaddingBytesMax maxValue: xPaddingBytesMax
onMinChanged: xPaddingBytesMin = val onMinChanged: function(val) { xPaddingBytesMin = val; root.editDirty = false }
onMaxChanged: xPaddingBytesMax = val onMaxChanged: function(val) { xPaddingBytesMax = val; root.editDirty = false }
onEdited: root.editDirty = true
} }
Item { Item {
@@ -81,7 +84,7 @@ PageType {
anchors.rightMargin: 16 anchors.rightMargin: 16
anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin
visible: listView.enabled && XrayConfigModel.hasUnsavedChanges visible: listView.enabled && (XrayConfigModel.hasUnsavedChanges || root.editDirty)
enabled: visible enabled: visible
text: qsTr("Save") text: qsTr("Save")
clickedFunc: function () { clickedFunc: function () {
@@ -15,6 +15,8 @@ import "../Components"
PageType { PageType {
id: root id: root
property bool editDirty: false
BackButtonType { BackButtonType {
id: backButton id: backButton
anchors.top: parent.top anchors.top: parent.top
@@ -78,8 +80,13 @@ PageType {
Layout.topMargin: 16 Layout.topMargin: 16
headerText: qsTr("xPaddingKey") headerText: qsTr("xPaddingKey")
textField.text: xPaddingKey textField.text: xPaddingKey
textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9_-]*$/ }
textField.onTextEdited: root.editDirty = (textField.text !== xPaddingKey)
textField.onEditingFinished: { textField.onEditingFinished: {
if (textField.text !== xPaddingKey) xPaddingKey = textField.text var v = textField.text.trim()
if (v !== xPaddingKey) xPaddingKey = v
else if (textField.text !== v) textField.text = v
root.editDirty = false
} }
} }
@@ -90,13 +97,19 @@ PageType {
Layout.topMargin: 8 Layout.topMargin: 8
headerText: qsTr("xPaddingHeader") headerText: qsTr("xPaddingHeader")
textField.text: xPaddingHeader textField.text: xPaddingHeader
textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9_-]*$/ }
textField.onTextEdited: root.editDirty = (textField.text !== xPaddingHeader)
textField.onEditingFinished: { textField.onEditingFinished: {
if (textField.text !== xPaddingHeader) xPaddingHeader = textField.text var v = textField.text.trim()
if (v !== xPaddingHeader) xPaddingHeader = v
else if (textField.text !== v) textField.text = v
root.editDirty = false
} }
} }
DropDownType { DropDownType {
id: placementDropDown id: placementDropDown
fitContent: true
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 8 Layout.topMargin: 8
Layout.leftMargin: 16 Layout.leftMargin: 16
@@ -140,6 +153,7 @@ PageType {
DropDownType { DropDownType {
id: methodDropDown id: methodDropDown
fitContent: true
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 8 Layout.topMargin: 8
Layout.leftMargin: 16 Layout.leftMargin: 16
@@ -197,7 +211,7 @@ PageType {
anchors.rightMargin: 16 anchors.rightMargin: 16
anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin
visible: listView.enabled && XrayConfigModel.hasUnsavedChanges visible: listView.enabled && (XrayConfigModel.hasUnsavedChanges || root.editDirty)
enabled: visible enabled: visible
text: qsTr("Save") text: qsTr("Save")
clickedFunc: function () { clickedFunc: function () {
@@ -15,6 +15,21 @@ import "../Components"
PageType { PageType {
id: root id: root
property bool editDirty: false
function clampSigned(text) {
if (text === "" || text === "-")
return ""
var n = parseInt(text, 10)
if (isNaN(n))
return ""
if (n > 2147483647)
n = 2147483647
if (n < -2147483648)
n = -2147483648
return String(n)
}
BackButtonType { BackButtonType {
id: backButton id: backButton
anchors.top: parent.top anchors.top: parent.top
@@ -78,8 +93,9 @@ PageType {
Layout.rightMargin: 16 Layout.rightMargin: 16
minValue: xmuxMaxConcurrencyMin minValue: xmuxMaxConcurrencyMin
maxValue: xmuxMaxConcurrencyMax maxValue: xmuxMaxConcurrencyMax
onMinChanged: xmuxMaxConcurrencyMin = val onMinChanged: function(val) { xmuxMaxConcurrencyMin = val; root.editDirty = false }
onMaxChanged: xmuxMaxConcurrencyMax = val onMaxChanged: function(val) { xmuxMaxConcurrencyMax = val; root.editDirty = false }
onEdited: root.editDirty = true
} }
// maxConnections // maxConnections
@@ -98,8 +114,9 @@ PageType {
Layout.rightMargin: 16 Layout.rightMargin: 16
minValue: xmuxMaxConnectionsMin minValue: xmuxMaxConnectionsMin
maxValue: xmuxMaxConnectionsMax maxValue: xmuxMaxConnectionsMax
onMinChanged: xmuxMaxConnectionsMin = val onMinChanged: function(val) { xmuxMaxConnectionsMin = val; root.editDirty = false }
onMaxChanged: xmuxMaxConnectionsMax = val onMaxChanged: function(val) { xmuxMaxConnectionsMax = val; root.editDirty = false }
onEdited: root.editDirty = true
} }
// cMaxReuseTimes // cMaxReuseTimes
@@ -118,8 +135,9 @@ PageType {
Layout.rightMargin: 16 Layout.rightMargin: 16
minValue: xmuxCMaxReuseTimesMin minValue: xmuxCMaxReuseTimesMin
maxValue: xmuxCMaxReuseTimesMax maxValue: xmuxCMaxReuseTimesMax
onMinChanged: xmuxCMaxReuseTimesMin = val onMinChanged: function(val) { xmuxCMaxReuseTimesMin = val; root.editDirty = false }
onMaxChanged: xmuxCMaxReuseTimesMax = val onMaxChanged: function(val) { xmuxCMaxReuseTimesMax = val; root.editDirty = false }
onEdited: root.editDirty = true
} }
// hMaxRequestTimes // hMaxRequestTimes
@@ -138,8 +156,9 @@ PageType {
Layout.rightMargin: 16 Layout.rightMargin: 16
minValue: xmuxHMaxRequestTimesMin minValue: xmuxHMaxRequestTimesMin
maxValue: xmuxHMaxRequestTimesMax maxValue: xmuxHMaxRequestTimesMax
onMinChanged: xmuxHMaxRequestTimesMin = val onMinChanged: function(val) { xmuxHMaxRequestTimesMin = val; root.editDirty = false }
onMaxChanged: xmuxHMaxRequestTimesMax = val onMaxChanged: function(val) { xmuxHMaxRequestTimesMax = val; root.editDirty = false }
onEdited: root.editDirty = true
} }
// hMaxReusableSecs // hMaxReusableSecs
@@ -158,8 +177,9 @@ PageType {
Layout.rightMargin: 16 Layout.rightMargin: 16
minValue: xmuxHMaxReusableSecsMin minValue: xmuxHMaxReusableSecsMin
maxValue: xmuxHMaxReusableSecsMax maxValue: xmuxHMaxReusableSecsMax
onMinChanged: xmuxHMaxReusableSecsMin = val onMinChanged: function(val) { xmuxHMaxReusableSecsMin = val; root.editDirty = false }
onMaxChanged: xmuxHMaxReusableSecsMax = val onMaxChanged: function(val) { xmuxHMaxReusableSecsMax = val; root.editDirty = false }
onEdited: root.editDirty = true
} }
TextFieldWithHeaderType { TextFieldWithHeaderType {
@@ -168,12 +188,16 @@ PageType {
Layout.rightMargin: 16 Layout.rightMargin: 16
Layout.topMargin: 16 Layout.topMargin: 16
headerText: qsTr("hKeepAlivePeriod") headerText: qsTr("hKeepAlivePeriod")
subtitleText: qsTr("Integer, may be negative")
textField.text: xmuxHKeepAlivePeriod textField.text: xmuxHKeepAlivePeriod
textField.validator: IntValidator { textField.maximumLength: 11
bottom: 0 textField.validator: RegularExpressionValidator { regularExpression: /^-?\d*$/ }
} textField.onTextEdited: root.editDirty = (textField.text !== xmuxHKeepAlivePeriod)
textField.onEditingFinished: { textField.onEditingFinished: {
if (textField.text !== xmuxHKeepAlivePeriod) xmuxHKeepAlivePeriod = textField.text var v = root.clampSigned(textField.text)
if (v !== xmuxHKeepAlivePeriod) xmuxHKeepAlivePeriod = v
else if (textField.text !== v) textField.text = v
root.editDirty = false
} }
} }
} }
@@ -194,7 +218,7 @@ PageType {
anchors.rightMargin: 16 anchors.rightMargin: 16
anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin
visible: listView.enabled && XrayConfigModel.hasUnsavedChanges visible: listView.enabled && (XrayConfigModel.hasUnsavedChanges || root.editDirty)
enabled: visible enabled: visible
text: qsTr("Save") text: qsTr("Save")
clickedFunc: function () { clickedFunc: function () {
+1
View File
@@ -91,6 +91,7 @@ PageType {
} }
function onExportErrorOccurred(error) { function onExportErrorOccurred(error) {
PageController.showBusyIndicator(false)
PageController.showErrorMessage(error) PageController.showErrorMessage(error)
} }
} }
+1 -1
View File
@@ -53,7 +53,7 @@ Window {
} }
} }
visible: true visible: !GC.isDesktop()
width: GC.screenWidth width: GC.screenWidth
height: GC.screenHeight height: GC.screenHeight
minimumWidth: GC.isDesktop() ? 360 : 0 minimumWidth: GC.isDesktop() ? 360 : 0
+4 -4
View File
@@ -19,12 +19,12 @@ class AmneziaVPN(ConanFile):
if has_service: if has_service:
if os == "Windows": if os == "Windows":
self.requires("awg-windows/0.1.8") self.requires("awg-windows/0.1.9")
self.requires("tap-windows6/9.27.0") self.requires("tap-windows6/9.27.0")
self.requires("win-split-tunnel/1.2.5.0") self.requires("win-split-tunnel/1.2.5.0")
self.requires("wintun/0.14.1") self.requires("wintun/0.14.1")
else: else:
self.requires("awg-go/0.2.16") self.requires("awg-go/0.2.18")
self.requires("amnezia-xray-bindings/1.1.0") self.requires("amnezia-xray-bindings/1.1.0")
self.requires("tun2socks/2.6.0") self.requires("tun2socks/2.6.0")
@@ -32,13 +32,13 @@ class AmneziaVPN(ConanFile):
self.requires("v2ray-rules-dat/202603162227") self.requires("v2ray-rules-dat/202603162227")
if has_ne: if has_ne:
self.requires("awg-apple/2.0.1") self.requires("awg-apple/2.0.2")
self.requires("hev-socks5-tunnel/2.15.0", options={"as_framework": True}) self.requires("hev-socks5-tunnel/2.15.0", options={"as_framework": True})
self.requires("openvpnadapter/1.0.0") self.requires("openvpnadapter/1.0.0")
if os == "Android": if os == "Android":
self.requires("amnezia-libxray/1.0.0") self.requires("amnezia-libxray/1.0.0")
self.requires("awg-android/1.1.7") self.requires("awg-android/2.0.1")
self.requires("openvpn-pt-android/1.0.0") self.requires("openvpn-pt-android/1.0.0")
# expicitly use libssh@amnezia to prevent it from being downloaded from conan-center # expicitly use libssh@amnezia to prevent it from being downloaded from conan-center
-36
View File
@@ -28,42 +28,6 @@ IpcServer::IpcServer(QObject *parent) : IpcInterfaceSource(parent)
connect(&m_pingHelper, &PingHelper::connectionLose, this, &IpcServer::connectionLose); connect(&m_pingHelper, &PingHelper::connectionLose, this, &IpcServer::connectionLose);
} }
IpcServer::~IpcServer()
{
}
void IpcServer::resetServiceState()
{
qDebug() << "IpcServer::resetServiceState — tearing down active VPN state";
Xray::getInstance().stopXray();
for (auto it = m_processes.cbegin(); it != m_processes.cend(); ++it) {
const ProcessDescriptor &pd = it.value();
if (!pd.ipcProcess)
continue;
pd.ipcProcess->terminate();
if (!pd.ipcProcess->waitForFinished(1000)) {
pd.ipcProcess->kill();
pd.ipcProcess->waitForFinished(1000);
}
pd.ipcProcess->close();
}
m_processes.clear();
Utils::killProcessByName(Utils::tun2socksPath());
Utils::killProcessByName(Utils::openVpnExecPath());
KillSwitch::instance()->disableKillSwitch();
Router::restoreResolvers();
Router::clearSavedRoutes();
Router::StartRoutingIpv6();
Router::flushDns();
m_pingHelper.stop();
}
int IpcServer::createPrivilegedProcess() int IpcServer::createPrivilegedProcess()
{ {
#ifdef MZ_DEBUG #ifdef MZ_DEBUG
-4
View File
@@ -17,10 +17,6 @@ class IpcServer : public IpcInterfaceSource
{ {
public: public:
explicit IpcServer(QObject *parent = nullptr); explicit IpcServer(QObject *parent = nullptr);
virtual ~IpcServer();
void resetServiceState();
virtual int createPrivilegedProcess() override; virtual int createPrivilegedProcess() override;
virtual int routeAddList(const QString &gw, const QStringList &ips) override; virtual int routeAddList(const QString &gw, const QStringList &ips) override;
+1 -1
View File
@@ -9,7 +9,7 @@ import platform
class AwgAndroid(ConanFile): class AwgAndroid(ConanFile):
name = "awg-android" name = "awg-android"
version = "1.1.7" version = "2.0.1"
settings = "os", "arch", "build_type", "compiler" settings = "os", "arch", "build_type", "compiler"
def configure(self): def configure(self):
+2 -2
View File
@@ -9,7 +9,7 @@ import os
class AwgApple(ConanFile): class AwgApple(ConanFile):
name = "awg-apple" name = "awg-apple"
version = "2.0.1" version = "2.0.2"
settings = "os", "arch", "compiler" settings = "os", "arch", "compiler"
@property @property
@@ -39,7 +39,7 @@ class AwgApple(ConanFile):
def source(self): def source(self):
get(self, f"https://github.com/amnezia-vpn/amneziawg-apple/archive/refs/tags/v{self.version}.zip", get(self, f"https://github.com/amnezia-vpn/amneziawg-apple/archive/refs/tags/v{self.version}.zip",
sha256="9fe4f8cfbb6a751558b54b7979db3a5ea46e49731912aae99f093e84a1433e97", strip_root=True sha256="a04f49eac9f82bbf5dd9031bab188d44de2b3482efde1b6e970821de1d5a3c5d", strip_root=True
) )
def generate(self): def generate(self):
+2 -2
View File
@@ -8,7 +8,7 @@ import os
class AwgGo(ConanFile): class AwgGo(ConanFile):
name = "awg-go" name = "awg-go"
version = "0.2.16" version = "0.2.18"
package_type = "application" package_type = "application"
settings = "os", "arch" settings = "os", "arch"
@@ -42,7 +42,7 @@ class AwgGo(ConanFile):
def source(self): def source(self):
get(self, f"https://github.com/amnezia-vpn/amneziawg-go/archive/refs/tags/v{self.version}.zip", get(self, f"https://github.com/amnezia-vpn/amneziawg-go/archive/refs/tags/v{self.version}.zip",
sha256="34da7d4189f215f3930de441548bc2a0c89d54d347a4fb85cb9c715fce6413aa", strip_root=True sha256="58eefbd012e79bd1525f0e02d748979e9480acc1a339df8ceb3b9ffafcedb1ba", strip_root=True
) )
def generate(self): def generate(self):
+2 -2
View File
@@ -8,7 +8,7 @@ import os
class AwgWindows(ConanFile): class AwgWindows(ConanFile):
name = "awg-windows" name = "awg-windows"
version = "0.1.8" version = "0.1.9"
settings = "os", "arch" settings = "os", "arch"
@property @property
@@ -63,7 +63,7 @@ class AwgWindows(ConanFile):
def source(self): def source(self):
get(self, f"https://github.com/amnezia-vpn/amneziawg-windows/archive/refs/tags/v{self.version}.zip", get(self, f"https://github.com/amnezia-vpn/amneziawg-windows/archive/refs/tags/v{self.version}.zip",
sha256="1de472832b332515c96cdf14ea887edde42ed7ad173675280c51baa9a3ef62f2", strip_root=True) sha256="5c29a75cb2beae291cc51b64840a39f838da5f300b9e956f7964813a687ec74c", strip_root=True)
def generate(self): def generate(self):
tc = AutotoolsToolchain(self) tc = AutotoolsToolchain(self)
+3
View File
@@ -650,6 +650,9 @@ class OpenSSLConan(ConanFile):
if self._use_nmake: if self._use_nmake:
self.cpp_info.components["ssl"].libs = ["libssl"] self.cpp_info.components["ssl"].libs = ["libssl"]
self.cpp_info.components["crypto"].libs = ["libcrypto"] self.cpp_info.components["crypto"].libs = ["libcrypto"]
elif self.settings.os == "Android" and self.options.shared:
self.cpp_info.components["ssl"].libs = ["ssl_3"]
self.cpp_info.components["crypto"].libs = ["crypto_3"]
else: else:
self.cpp_info.components["ssl"].libs = ["ssl"] self.cpp_info.components["ssl"].libs = ["ssl"]
self.cpp_info.components["crypto"].libs = ["crypto"] self.cpp_info.components["crypto"].libs = ["crypto"]
+1 -1
View File
@@ -28,7 +28,7 @@ class Openvpn(ConanFile):
def build_requirements(self): def build_requirements(self):
if self._is_windows: if self._is_windows:
self.tool_requires("cmake/[>=3.14 <4]") self.tool_requires("cmake/[>=4.2]")
else: else:
self.tool_requires("libtool/2.4.7") self.tool_requires("libtool/2.4.7")
self.tool_requires("automake/1.16.5") self.tool_requires("automake/1.16.5")
+1 -14
View File
@@ -35,20 +35,7 @@ LocalServer::LocalServer(QObject *parent) : QObject(parent),
QObject::connect(m_server.data(), &QLocalServer::newConnection, this, [this]() { QObject::connect(m_server.data(), &QLocalServer::newConnection, this, [this]() {
qDebug() << "LocalServer new connection"; qDebug() << "LocalServer new connection";
m_serverNode.addHostSideConnection(m_server->nextPendingConnection());
QLocalSocket *socket = m_server->nextPendingConnection();
if (!socket)
return;
m_activeClientSocket = socket;
QObject::connect(socket, &QLocalSocket::disconnected, this, [this, socket]() {
qDebug() << "LocalServer: client disconnected";
if (m_activeClientSocket == socket)
m_ipcServer.resetServiceState();
});
m_serverNode.addHostSideConnection(socket);
if (!m_isRemotingEnabled) { if (!m_isRemotingEnabled) {
m_isRemotingEnabled = true; m_isRemotingEnabled = true;
-2
View File
@@ -41,8 +41,6 @@ public:
QRemoteObjectHost m_serverNode; QRemoteObjectHost m_serverNode;
bool m_isRemotingEnabled = false; bool m_isRemotingEnabled = false;
QPointer<QLocalSocket> m_activeClientSocket;
NetworkWatcher m_networkWatcher; NetworkWatcher m_networkWatcher;
#ifdef Q_OS_LINUX #ifdef Q_OS_LINUX
DaemonLocalServer server{qApp}; DaemonLocalServer server{qApp};