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"
- 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'
if: github.ref == 'refs/heads/dev'
+6
View File
@@ -119,7 +119,13 @@ void AmneziaApplication::init()
win->setPersistentSceneGraph(true);
win->setPersistentGraphics(true);
#endif
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
win->show();
#else
if (!m_coreController || !m_coreController->pageController()->shouldStartMinimized()) {
win->show();
}
#endif
}
},
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)
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
<< "transport=" << srv.transport << "security=" << srv.security << "port=" << srv.port
<< "appendClient=" << appendNewClient;
QString flowValue = srv.flow;
if (flowValue.isEmpty() && srv.security == QLatin1String("reality")) {
flowValue = QStringLiteral("xtls-rprx-vision");
}
const QString flowValue = srv.flow;
QString realityPublicKey;
QString realityShortId;
if (srv.security == QLatin1String("reality")) {
@@ -563,9 +559,12 @@ QJsonObject XrayConfigurator::buildStreamSettings(const XrayServerConfig &srv, c
if (pad.obfsMode) {
if (!pad.bytesMin.isEmpty() || !pad.bytesMax.isEmpty()) {
QJsonObject br;
br[QStringLiteral("from")] = pad.bytesMin.isEmpty() ? 1 : pad.bytesMin.toInt();
br[QStringLiteral("to")] = pad.bytesMax.isEmpty() ? (pad.bytesMin.isEmpty() ? 256 : pad.bytesMin.toInt())
: pad.bytesMax.toInt();
const int fromV = pad.bytesMin.isEmpty() ? 1 : pad.bytesMin.toInt();
int toV = pad.bytesMax.isEmpty() ? 256 : pad.bytesMax.toInt();
if (toV < fromV)
toV = fromV;
br[QStringLiteral("from")] = fromV;
br[QStringLiteral("to")] = toV;
xo[QStringLiteral("xPaddingBytes")] = br;
}
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;
}
if (serverConfigUtils::isLegacyApiSubscription(m_serversRepository->serverKind(serverId))) {
const serverConfigUtils::ConfigType kind = m_serversRepository->serverKind(serverId);
if (serverConfigUtils::isLegacyApiSubscription(kind)) {
return ErrorCode::LegacyApiV1NotSupportedError;
}
@@ -117,6 +118,9 @@ ErrorCode ConnectionController::isConnectionSupported(const QString &serverId) c
}
if (container == DockerContainer::None) {
if (serverConfigUtils::isApiV2Subscription(kind)) {
return ErrorCode::NoError;
}
return ErrorCode::NoInstalledContainersError;
}
+10 -3
View File
@@ -1,6 +1,7 @@
#include "coreSignalHandlers.h"
#include <QTimer>
#include <QtConcurrent>
#include "core/utils/selfhosted/sshSession.h"
#include "core/utils/errorCodes.h"
@@ -144,7 +145,9 @@ void CoreSignalHandlers::initExportControllerHandler()
});
connect(m_coreController->m_exportController, &ExportController::revokeClientRequested, this,
[this](const QString &serverId, int row, DockerContainer container) {
m_coreController->m_usersController->revokeClient(serverId, row, container);
QtConcurrent::run([this, serverId, row, container]() {
m_coreController->m_usersController->revokeClient(serverId, row, container);
});
});
connect(m_coreController->m_exportController, &ExportController::renameClientRequested, this,
[this](const QString &serverId, int row, const QString &clientName, DockerContainer container) {
@@ -202,13 +205,15 @@ void CoreSignalHandlers::initAdminConfigRevokedHandler()
{
connect(m_coreController->m_installController, &InstallController::clientRevocationRequested, this,
[this](const QString &serverId, const ContainerConfig &containerConfig, DockerContainer container) {
m_coreController->m_usersController->revokeClient(serverId, containerConfig, container);
QtConcurrent::run([this, serverId, containerConfig, container]() {
m_coreController->m_usersController->revokeClient(serverId, containerConfig, container);
});
});
connect(m_coreController->m_installController, &InstallController::clientAppendRequested, this,
[this](const QString &serverId, const QString &clientId, const QString &clientName, DockerContainer container) {
m_coreController->m_usersController->appendClient(serverId, clientId, clientName, container);
});
}, Qt::DirectConnection);
connect(m_coreController->m_usersController, &UsersController::adminConfigRevoked, m_coreController->m_installController,
&InstallController::clearCachedProfile);
@@ -285,6 +290,8 @@ void CoreSignalHandlers::initClientManagementModelUpdateHandler()
m_coreController->m_clientManagementModel, &ClientManagementModel::updateModel);
connect(m_coreController->m_usersController, &UsersController::clientRenamed,
m_coreController->m_clientManagementModel, &ClientManagementModel::updateClientName);
connect(m_coreController->m_usersController, &UsersController::revokeFinished,
m_coreController->m_exportController, &ExportController::revokeFinished);
}
void CoreSignalHandlers::initSitesModelUpdateHandler()
@@ -48,6 +48,7 @@ signals:
void appendClientRequested(const QString &serverId, const QString &clientId, const QString &clientName, DockerContainer container);
void updateClientsRequested(const QString &serverId, 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);
public slots:
@@ -234,7 +234,9 @@ ErrorCode InstallController::updateServerConfig(const QString &serverId, DockerC
} else if (container == DockerContainer::Telemt) {
TelemtInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
}
clearCachedProfile(serverId, container);
if (reinstallRequired) {
clearCachedProfile(serverId, container);
}
adminConfig->updateContainerConfig(container, newConfig);
m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin);
}
@@ -836,8 +838,8 @@ ErrorCode InstallController::installDockerWorker(const ServerCredentials &creden
qDebug().noquote() << "InstallController::installDockerWorker" << stdOut;
if (container == DockerContainer::Awg2) {
QRegularExpression regex(R"(Linux\s+(\d+)\.(\d+)[^\d]*)");
QRegularExpressionMatch match = regex.match(stdOut);
QRegularExpression kernelVersionRegex(R"(Linux\s+(\d+)\.(\d+)[^\d]*)");
QRegularExpressionMatch match = kernelVersionRegex.match(stdOut);
if (match.hasMatch()) {
int majorVersion = match.captured(1).toInt();
int minorVersion = match.captured(2).toInt();
@@ -850,8 +852,19 @@ ErrorCode InstallController::installDockerWorker(const ServerCredentials &creden
if (stdOut.contains("lock"))
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;
}
if (stdOut.contains("Container runtime service not running"))
return ErrorCode::ContainerRuntimeServiceNotRunning;
return error;
}
@@ -888,7 +901,7 @@ ErrorCode InstallController::isUserInSudo(const ServerCredentials &credentials,
return ErrorCode::ServerUserNotInSudo;
if (stdOut.contains("can't cd to") || stdOut.contains("Permission denied") || stdOut.contains("No such file or directory"))
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;
if (stdOut.contains("password is required") || stdOut.contains("authentication is required"))
return ErrorCode::ServerUserPasswordRequired;
@@ -698,7 +698,7 @@ ErrorCode UsersController::revokeXray(const int row,
QString restartScript = QString("sudo docker restart $CONTAINER_NAME");
error = sshSession->runScript(
credentials,
credentials,
sshSession->replaceVars(restartScript, amnezia::genBaseVars(credentials, container, QString(), QString()))
);
if (error != ErrorCode::NoError) {
@@ -758,14 +758,17 @@ ErrorCode UsersController::revokeClient(const QString &serverId, const int index
ContainerConfig containerCfg = adminConfig->containerConfig(container);
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 clientRevoked(index);
emit clientsUpdated(m_clientsTable);
}
emit clientsUpdated(m_clientsTable);
emit revokeFinished(errorCode);
return errorCode;
}
@@ -37,6 +37,7 @@ signals:
void clientAdded(const QJsonObject &client);
void clientRenamed(int row, const QString &newName);
void clientRevoked(int row);
void revokeFinished(ErrorCode errorCode);
void adminConfigRevoked(const QString &serverId, DockerContainer container);
public slots:
@@ -32,7 +32,7 @@ XrayXPaddingConfig XrayXPaddingConfig::fromJson(const QJsonObject &json)
c.bytesMin = json.value(configKey::xPaddingBytesMin).toString();
c.bytesMax = json.value(configKey::xPaddingBytesMax).toString();
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.placement = json.value(configKey::xPaddingPlacement).toString(protocols::xray::defaultXPaddingPlacement);
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
{
return port == other.port
&& transportProto == other.transportProto
&& subnetAddress == other.subnetAddress
&& site == other.site
&& security == other.security
&& 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;
} else {
c.clientConfig = XrayClientConfig::fromJson(parsed);
+37 -182
View File
@@ -9,13 +9,10 @@
#include "ipc.h"
#include <QCryptographicHash>
#include <QJsonArray>
#include <QJsonDocument>
#include <QTimer>
#include <QJsonObject>
#include <QNetworkInterface>
#include <QNetworkProxy>
#include <QTcpSocket>
#include <QtCore/qlogging.h>
#include <QtCore/qobjectdefs.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";
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()
@@ -93,13 +68,6 @@ ErrorCode 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.
// Re-uses existing credentials if the config already has them (e.g. imported config).
amnezia::serialization::inbounds::InboundCredentials creds;
@@ -136,50 +104,22 @@ ErrorCode XrayProtocol::start()
qDebug() << "XrayProtocol: patched legacy inbound listen address to 127.0.0.1";
}
startTimeoutTimer();
return IpcClient::withInterface(
[&](QSharedPointer<IpcInterfaceReplica> iface) {
auto xrayStart = iface->xrayStart(xrayConfigStr);
if (!xrayStart.waitForFinished() || !xrayStart.returnValue()) {
qCritical() << "Failed to start xray";
stopTimeoutTimer();
return ErrorCode::XrayExecutableCrashed;
}
return startTun2Socks();
},
[this]() {
stopTimeoutTimer();
return ErrorCode::AmneziaServiceConnectionFailed;
});
[]() { return ErrorCode::AmneziaServiceConnectionFailed; });
}
void 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) {
auto disableKillSwitch = iface->disableKillSwitch();
if (!disableKillSwitch.waitForFinished() || !disableKillSwitch.returnValue())
@@ -202,13 +142,31 @@ void XrayProtocol::stop()
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);
}
ErrorCode XrayProtocol::startTun2Socks()
{
m_tunResourceBusy = false;
m_tun2socksProcess = IpcClient::CreatePrivilegedProcess();
if (!m_tun2socksProcess->waitForSource()) {
return ErrorCode::AmneziaServiceConnectionFailed;
@@ -233,31 +191,15 @@ ErrorCode XrayProtocol::startTun2Socks()
if (!line.contains("[TCP]") && !line.contains("[UDP]"))
qDebug() << "[tun2socks]:" << line;
if (line.contains("resource busy"))
m_tunResourceBusy = true;
if (line.contains("[STACK] tun://") && line.contains("<-> socks5://") && !m_connectivityProbeStarted) {
m_connectivityProbeStarted = true;
if (line.contains("[STACK] tun://") && line.contains("<-> socks5://")) {
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) {
stop();
setLastError(res);
} else {
stopTimeoutTimer();
setConnectionState(Vpn::ConnectionState::Connected);
startLivenessMonitor();
}
});
if (ErrorCode res = setupRouting(); res != ErrorCode::NoError) {
stop();
setLastError(res);
} else {
setConnectionState(Vpn::ConnectionState::Connected);
}
}
},
Qt::QueuedConnection);
@@ -265,7 +207,15 @@ ErrorCode XrayProtocol::startTun2Socks()
connect(
m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::finished, this,
[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) {
m_tun2socksRetryCount++;
@@ -381,98 +331,3 @@ ErrorCode XrayProtocol::setupRouting()
},
[]() { 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 <QList>
#include <functional>
#include "core/utils/errorCodes.h"
#include "core/utils/routeModes.h"
#include "core/utils/commonStructs.h"
#include "core/utils/ipcClient.h"
#include "vpnProtocol.h"
class QTimer;
class XrayProtocol : public VpnProtocol
{
public:
@@ -29,17 +25,10 @@ private:
ErrorCode setupRouting();
ErrorCode startTun2Socks();
bool probeServerReachable();
void runConnectivityProbe(std::function<void(bool)> onResult);
void startLivenessMonitor();
void stopLivenessMonitor();
int extractServerPort() const;
QJsonObject m_xrayConfig;
amnezia::RouteMode m_routeMode;
QList<QHostAddress> m_dnsServers;
QString m_remoteAddress;
int m_serverPort = 0;
QString m_socksUser;
QString m_socksPassword;
@@ -49,22 +38,6 @@ private:
int m_tun2socksRetryCount = 0;
static constexpr int maxTun2SocksRetries = 5;
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
@@ -208,8 +208,8 @@ QString SecureServersRepository::nextAvailableServerName() const
int i = 0;
QString candidate;
do {
i++;
candidate = QStringLiteral("Server %1").arg(i);
++i;
candidate = tr("Server") + QLatin1Char(' ') + QString::number(i);
} while (usedNames.contains(candidate));
return candidate;
+2 -5
View File
@@ -38,6 +38,8 @@ namespace amnezia
XrayServerConfigInvalid = 215,
XrayServerNoVlessClients = 216,
XrayRealityKeysReadFailed = 217,
ServerContainerRuntimeNotSupported = 218,
ContainerRuntimeServiceNotRunning = 219,
// Ssh connection errors
SshRequestDeniedError = 300,
@@ -71,9 +73,6 @@ namespace amnezia
OpenSslFailed = 800,
XrayExecutableCrashed = 803,
Tun2SockExecutableCrashed = 804,
XrayServerUnreachable = 805,
XrayConnectivityCheckFailed = 806,
XrayConnectionLost = 807,
// import and install errors
ImportInvalidConfigError = 900,
@@ -127,5 +126,3 @@ namespace amnezia
Q_DECLARE_METATYPE(amnezia::ErrorCode)
#endif // ERRORCODES_H
+2 -3
View File
@@ -39,6 +39,8 @@ QString errorString(ErrorCode code) {
case(ErrorCode::XrayRealityKeysReadFailed):
errorMessage = QObject::tr("Server error: failed to read XRay Reality keys from the server");
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
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::AmneziaServiceConnectionFailed): errorMessage = QObject::tr("Amnezia helper service error"); 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
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 <QLocalSocket>
#include "daemon.h"
#include "daemonlocalserverconnection.h"
#include "leakdetector.h"
#include "logger.h"
@@ -59,11 +58,8 @@ bool DaemonLocalServer::initialize() {
DaemonLocalServerConnection* connection =
new DaemonLocalServerConnection(&m_server, socket);
connect(socket, &QLocalSocket::disconnected, connection, [connection]() {
logger.debug() << "Client connection dropped, deactivating daemon";
Daemon::instance()->deactivate(true);
connection->deleteLater();
});
connect(socket, &QLocalSocket::disconnected, connection,
&DaemonLocalServerConnection::deleteLater);
});
return true;
@@ -25,6 +25,8 @@ set_target_properties(AmneziaVPNNetworkExtension PROPERTIES
XCODE_ATTRIBUTE_INFOPLIST_FILE ${CMAKE_CURRENT_SOURCE_DIR}/Info.plist.in
XCODE_ATTRIBUTE_LD_RUNPATH_SEARCH_PATHS "@executable_path/../../../../Frameworks @loader_path/../../../../Frameworks"
XCODE_LINK_BUILD_PHASE_MODE KNOWN_LOCATION
)
if(DEPLOY)
@@ -118,10 +120,20 @@ target_include_directories(AmneziaVPNNetworkExtension PRIVATE ${CLIENT_ROOT_DIR}
target_include_directories(AmneziaVPNNetworkExtension PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
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)
find_package(awg-apple REQUIRED)
target_link_libraries(AmneziaVPNNetworkExtension PRIVATE amnezia::awg-apple)
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)
@@ -1,7 +1,8 @@
if which 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 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 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;\
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
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 || 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 || command -v yum > /dev/null 2>&1; then LOCK_CMD="cat"; LOCK_FILE="/var/run/yum.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 || 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;\
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";\
elif which dnf > /dev/null 2>&1; then pm=$(which dnf); opt="--version";\
elif which yum > /dev/null 2>&1; then pm=$(which yum); opt="--version";\
elif which zypper > /dev/null 2>&1; then pm=$(which zypper); opt="--version";\
elif which pacman > /dev/null 2>&1; then pm=$(which pacman); opt="--version";\
if pm=$(which apt-get 2>/dev/null || command -v apt-get 2>/dev/null); then opt="--version";\
elif pm=$(which dnf 2>/dev/null || command -v dnf 2>/dev/null); then opt="--version";\
elif pm=$(which yum 2>/dev/null || command -v yum 2>/dev/null); then opt="--version";\
elif pm=$(which zypper 2>/dev/null || command -v zypper 2>/dev/null); then opt="--version";\
elif pm=$(which pacman 2>/dev/null || command -v pacman 2>/dev/null); then opt="--version";\
else pm="uname"; opt="-a";\
fi;\
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";\
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 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 which zypper > /dev/null 2>&1; then pm=$(which zypper); silent_inst="-nq install"; check_pkgs="-nq refresh"; docker_pkg="docker"; dist="opensuse";\
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";\
else echo "Packet manager not found"; exit 1; fi;\
echo "Dist: $dist, Packet manager: $pm, Install command: $silent_inst, Check pkgs command: $check_pkgs, Docker pkg: $docker_pkg";\
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 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 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 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 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";\
fi;\
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 ! 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 ! command -v lsof > /dev/null 2>&1; then sudo $pm $check_pkgs; sudo $pm $silent_inst lsof; fi;\
if ! command -v docker > /dev/null 2>&1; then \
sudo $pm $check_pkgs; sudo $pm $silent_inst $docker_pkg;\
sleep 5; sudo systemctl enable --now docker; sleep 5;\
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 ! 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 ! sudo -n sh -c 'command -v lsof > /dev/null 2>&1'; then sudo -n $pm $check_pkgs; sudo -n $pm $silent_inst lsof; fi;\
if ! sudo -n sh -c 'command -v docker > /dev/null 2>&1'; then \
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;\
if [ "$(sudo -n cat /sys/module/apparmor/parameters/enabled 2>/dev/null)" = "Y" ]; then \
if ! sudo -n sh -c 'command -v apparmor_parser > /dev/null 2>&1'; then \
sudo -n $pm $check_pkgs; sudo -n $pm $silent_inst apparmor;\
fi;\
fi;\
if [ "$(systemctl is-active docker)" != "active" ]; then \
sudo $pm $check_pkgs; sudo $pm $silent_inst $docker_pkg;\
sleep 5; sudo systemctl start docker; sleep 5;\
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;\
if ! command -v sudo > /dev/null 2>&1; then echo "Failed to install sudo, command not found"; exit 1; fi;\
docker --version;\
sudo -n docker --version || docker --version;\
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 rm -fv;\
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 rm -frd /opt/amnezia
@@ -128,6 +128,11 @@ void PageController::showOnStartup()
}
}
bool PageController::shouldStartMinimized() const
{
return m_settingsController->isStartMinimizedEnabled();
}
bool PageController::isTriggeredByConnectButton()
{
return m_isTriggeredByConnectButton;
@@ -123,6 +123,7 @@ public slots:
void updateNavigationBarColor(const int color);
void showOnStartup();
bool shouldStartMinimized() const;
bool isTriggeredByConnectButton();
void setTriggeredByConnectButton(bool trigger);
@@ -9,6 +9,13 @@ ExportUiController::ExportUiController(ExportController* exportController, QObje
: QObject(parent),
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)
@@ -92,7 +99,6 @@ void ExportUiController::updateClientManagementModel(const QString &serverId, in
void ExportUiController::revokeConfig(int row, const QString &serverId, int containerIndex)
{
m_exportController->revokeConfig(row, serverId, containerIndex);
emit revokeConfigFinished();
}
void ExportUiController::renameClient(int row, const QString &clientName, const QString &serverId, int containerIndex)
@@ -306,14 +306,17 @@ void InstallUiController::updateServerConfig(const QString &serverId, int contai
|| container == DockerContainer::Xray || container == DockerContainer::SSXray;
if (asyncUpdate) {
emit serverIsBusy(true);
const bool emitBusy = container == DockerContainer::MtProxy || container == DockerContainer::Telemt;
if (emitBusy)
emit serverIsBusy(true);
auto *watcher = new QFutureWatcher<ErrorCode>(this);
const Proto protocolTypeCopy = protocolType;
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();
watcher->deleteLater();
emit serverIsBusy(false);
if (emitBusy)
emit serverIsBusy(false);
if (errorCode == ErrorCode::NoError) {
const ContainerConfig updatedConfig =
+91 -3
View File
@@ -4,6 +4,10 @@
#include "core/protocols/protocolUtils.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/constants/protocolConstants.h"
#include "core/utils/networkUtilities.h"
#include <QHostAddress>
#include <QRegularExpression>
using namespace amnezia;
using namespace ProtocolUtils;
@@ -272,7 +276,7 @@ void XrayConfigModel::updateModel(amnezia::DockerContainer container, const amne
}
if (!m_protocolConfig.serverConfig.isThirdPartyConfig) {
applyDefaultsToServerConfig(m_protocolConfig.serverConfig);
applyDefaultsToServerConfig(m_protocolConfig.serverConfig, false);
}
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()) {
config.port = protocols::xray::defaultPort;
@@ -306,7 +310,7 @@ void XrayConfigModel::applyDefaultsToServerConfig(amnezia::XrayServerConfig &con
config.security = protocols::xray::defaultSecurity;
}
if (config.flow.isEmpty()) {
if (fillFlowDefault && config.flow.isEmpty()) {
config.flow = protocols::xray::defaultFlow;
}
@@ -585,3 +589,87 @@ QString XrayConfigModel::mkcpDefaultWriteBufferSize()
{
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 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:
void updateModel(amnezia::DockerContainer container, const amnezia::XrayProtocolConfig& protocolConfig);
amnezia::XrayProtocolConfig getProtocolConfig();
@@ -137,7 +142,7 @@ private:
amnezia::XrayProtocolConfig m_protocolConfig;
amnezia::XrayProtocolConfig m_originalProtocolConfig;
void applyDefaultsToServerConfig(amnezia::XrayServerConfig& config);
void applyDefaultsToServerConfig(amnezia::XrayServerConfig& config, bool fillFlowDefault = true);
};
#endif // XRAYCONFIGMODEL_H
+11 -1
View File
@@ -42,6 +42,7 @@ Item {
property int rootButtonTextBottomMargin: 16
property real drawerHeight: 0.9
property bool fitContent: false
property Item drawerParent
property Component listView
@@ -219,12 +220,20 @@ Item {
parent: drawerParent
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 {
id: container
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 {
id: header
@@ -238,6 +247,7 @@ Item {
}
Header2Type {
id: titleLabel
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 16
+112 -25
View File
@@ -12,8 +12,8 @@ import "../Controls2/TextTypes"
// MinMaxRowType {
// minValue: "0"
// maxValue: "0"
// onMinChanged: someProperty = val
// onMaxChanged: someProperty = val
// onMinChanged: function(val) { someProperty = val }
// onMaxChanged: function(val) { someProperty = val }
// }
Item {
id: root
@@ -21,41 +21,128 @@ Item {
property string minValue: "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 maxChanged(string val)
signal edited()
implicitHeight: row.implicitHeight
implicitWidth: row.implicitWidth
implicitHeight: col.implicitHeight
implicitWidth: col.implicitWidth
RowLayout {
id: row
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: 10
spacing: 4
// Min field
TextFieldWithHeaderType {
RowLayout {
id: row
Layout.fillWidth: true
headerText: qsTr("Min")
textField.text: root.minValue
textField.validator: IntValidator { bottom: 0 }
textField.onEditingFinished: {
if (textField.text !== root.minValue) {
root.minChanged(textField.text)
spacing: 10
// Min field
TextFieldWithHeaderType {
id: minField
property string lastValid: ""
Layout.fillWidth: true
headerText: qsTr("Min")
textField.maximumLength: 10
textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ }
textField.onActiveFocusChanged: {
if (minField.textField.activeFocus)
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
TextFieldWithHeaderType {
id: maxField
property string lastValid: ""
Layout.fillWidth: true
headerText: qsTr("Max")
textField.maximumLength: 10
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: {
var v = root.clampValue(maxField.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
}
}
}
// Max field
TextFieldWithHeaderType {
SmallTextType {
visible: root.hintText !== ""
text: root.hintText
color: AmneziaStyle.color.mutedGray
Layout.fillWidth: true
headerText: qsTr("Max")
textField.text: root.maxValue
textField.validator: IntValidator { bottom: 0 }
textField.onEditingFinished: {
if (textField.text !== root.maxValue) {
root.maxChanged(textField.text)
}
}
}
}
}
@@ -15,6 +15,8 @@ import "../Components"
PageType {
id: root
property bool editDirty: false
BackButtonType {
id: backButton
anchors.top: parent.top
@@ -90,6 +92,7 @@ PageType {
DropDownType {
id: tlsAlpnDropDown
fitContent: true
Layout.fillWidth: true
Layout.topMargin: 16
Layout.leftMargin: 16
@@ -133,6 +136,7 @@ PageType {
DropDownType {
id: tlsFingerprintDropDown
fitContent: true
Layout.fillWidth: true
Layout.topMargin: 8
Layout.leftMargin: 16
@@ -175,14 +179,21 @@ PageType {
}
TextFieldWithHeaderType {
id: sniFieldTls
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 8
headerText: qsTr("Server Name (SNI)")
textField.text: sni
textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9.*_-]*$/ }
textField.onTextEdited: root.editDirty = (textField.text !== sni)
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 {
id: realityFingerprintDropDown
fitContent: true
Layout.fillWidth: true
Layout.topMargin: 16
Layout.leftMargin: 16
@@ -237,14 +249,21 @@ PageType {
}
TextFieldWithHeaderType {
id: sniFieldReality
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 8
headerText: qsTr("Server Name (SNI)")
textField.text: sni
textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9.*_-]*$/ }
textField.onTextEdited: root.editDirty = (textField.text !== sni)
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.bottomMargin: 16 + PageController.safeAreaBottomMargin
visible: listView.enabled && XrayConfigModel.hasUnsavedChanges
visible: listView.enabled && (XrayConfigModel.hasUnsavedChanges || root.editDirty)
enabled: visible
text: qsTr("Save")
clickedFunc: function () {
var errs = XrayConfigModel.validationErrors()
if (errs.length > 0) {
PageController.showErrorMessage(errs.join("\n"))
return
}
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 yesButtonText = qsTr("Continue")
@@ -109,6 +109,7 @@ PageType {
Layout.rightMargin: 16
enabled: listView.enabled
headerText: qsTr("Port")
subtitleText: qsTr("165535")
Binding {
target: textFieldWithHeaderType.textField
@@ -119,8 +120,8 @@ PageType {
}
textField.maximumLength: 5
textField.validator: IntValidator {
bottom: 1; top: 65535
textField.validator: RegularExpressionValidator {
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: {
if (textField.activeFocus && textField.text === "" && port !== "") {
@@ -131,9 +132,19 @@ PageType {
root.portDirty = (textField.text !== port)
}
textField.onEditingFinished: {
if (textField.text !== port) {
port = textField.text
var v = textFieldWithHeaderType.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
}
checkEmptyText: true
@@ -198,6 +209,11 @@ PageType {
text: qsTr("Save")
onClicked: function() {
forceActiveFocus()
var errs = XrayConfigModel.validationErrors()
if (errs.length > 0) {
PageController.showErrorMessage(errs.join("\n"))
return
}
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 yesButtonText = qsTr("Continue")
@@ -15,6 +15,21 @@ import "../Components"
PageType {
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 {
id: backButton
anchors.top: parent.top
@@ -108,10 +123,16 @@ PageType {
Layout.rightMargin: 16
Layout.topMargin: 8
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.maximumLength: 3
textField.validator: RegularExpressionValidator { regularExpression: /^(|\d{1,2}|100)$/ }
textField.onTextEdited: root.editDirty = (textField.text !== mkcpTti)
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.topMargin: 8
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.maximumLength: 10
textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ }
textField.onTextEdited: root.editDirty = (textField.text !== mkcpUplinkCapacity)
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.topMargin: 8
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.maximumLength: 10
textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ }
textField.onTextEdited: root.editDirty = (textField.text !== mkcpDownlinkCapacity)
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.topMargin: 8
headerText: qsTr("readBufferSize")
subtitleText: qsTr("Default: %1 MiB").arg(XrayConfigModel.mkcpDefaultReadBufferSize())
subtitleText: qsTr("≥ 1, default %1 MB").arg(XrayConfigModel.mkcpDefaultReadBufferSize())
textField.text: mkcpReadBufferSize
textField.maximumLength: 10
textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ }
textField.onTextEdited: root.editDirty = (textField.text !== mkcpReadBufferSize)
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.topMargin: 8
headerText: qsTr("writeBufferSize")
subtitleText: qsTr("Default: %1 MiB").arg(XrayConfigModel.mkcpDefaultWriteBufferSize())
subtitleText: qsTr("≥ 1, default %1 MB").arg(XrayConfigModel.mkcpDefaultWriteBufferSize())
textField.text: mkcpWriteBufferSize
textField.maximumLength: 10
textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ }
textField.onTextEdited: root.editDirty = (textField.text !== mkcpWriteBufferSize)
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 {
id: modeDropDown
fitContent: true
Layout.fillWidth: true
Layout.topMargin: 16
Layout.leftMargin: 16
@@ -239,31 +285,46 @@ PageType {
}
TextFieldWithHeaderType {
id: hostField
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 8
headerText: qsTr("Host")
textField.text: xhttpHost
textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9._:,-]*$/ }
textField.onTextEdited: root.editDirty = (textField.text !== xhttpHost)
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 {
id: pathField
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 8
headerText: qsTr("Path")
textField.text: xhttpPath
textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9\-._~:\/?#\[\]@!$&'()*+,;=%]*$/ }
textField.onTextEdited: root.editDirty = (textField.text !== xhttpPath)
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 {
id: headersDropDown
fitContent: true
Layout.fillWidth: true
Layout.topMargin: 8
Layout.leftMargin: 16
@@ -307,6 +368,7 @@ PageType {
DropDownType {
id: uplinkMethodDropDown
fitContent: true
Layout.fillWidth: true
Layout.topMargin: 8
Layout.leftMargin: 16
@@ -386,6 +448,7 @@ PageType {
DropDownType {
id: sessionPlacementDropDown
fitContent: true
Layout.fillWidth: true
Layout.topMargin: 8
Layout.leftMargin: 16
@@ -429,6 +492,7 @@ PageType {
DropDownType {
id: sessionKeyDropDown
fitContent: true
Layout.fillWidth: true
Layout.topMargin: 8
Layout.leftMargin: 16
@@ -472,6 +536,7 @@ PageType {
DropDownType {
id: seqPlacementDropDown
fitContent: true
Layout.fillWidth: true
Layout.topMargin: 8
Layout.leftMargin: 16
@@ -520,13 +585,19 @@ PageType {
Layout.topMargin: 8
headerText: qsTr("SeqKey")
textField.text: xhttpSeqKey
textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9_-]*$/ }
textField.onTextEdited: root.editDirty = (textField.text !== xhttpSeqKey)
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 {
id: uplinkDataPlacementDropDown
fitContent: true
Layout.fillWidth: true
Layout.topMargin: 8
Layout.leftMargin: 16
@@ -575,8 +646,13 @@ PageType {
Layout.topMargin: 8
headerText: qsTr("UplinkDataKey")
textField.text: xhttpUplinkDataKey
textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9_-]*$/ }
textField.onTextEdited: root.editDirty = (textField.text !== xhttpUplinkDataKey)
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.topMargin: 8
headerText: qsTr("UplinkChunkSize")
subtitleText: qsTr("≥ 0 (0 = off)")
textField.text: xhttpUplinkChunkSize
textField.validator: IntValidator {
bottom: 0
}
textField.maximumLength: 10
textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ }
textField.onTextEdited: root.editDirty = (textField.text !== xhttpUplinkChunkSize)
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.topMargin: 8
headerText: qsTr("scMaxBufferedPosts")
subtitleText: qsTr("≥ 0")
textField.text: xhttpScMaxBufferedPosts
textField.maximumLength: 10
textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ }
textField.onTextEdited: root.editDirty = (textField.text !== xhttpScMaxBufferedPosts)
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
minValue: xhttpScMaxEachPostBytesMin
maxValue: xhttpScMaxEachPostBytesMax
onMinChanged: xhttpScMaxEachPostBytesMin = val
onMaxChanged: xhttpScMaxEachPostBytesMax = val
onMinChanged: function(val) { xhttpScMaxEachPostBytesMin = val; root.editDirty = false }
onMaxChanged: function(val) { xhttpScMaxEachPostBytesMax = val; root.editDirty = false }
onEdited: root.editDirty = true
}
CaptionTextType {
@@ -652,8 +740,9 @@ PageType {
Layout.rightMargin: 16
minValue: xhttpScStreamUpServerSecsMin
maxValue: xhttpScStreamUpServerSecsMax
onMinChanged: xhttpScStreamUpServerSecsMin = val
onMaxChanged: xhttpScStreamUpServerSecsMax = val
onMinChanged: function(val) { xhttpScStreamUpServerSecsMin = val; root.editDirty = false }
onMaxChanged: function(val) { xhttpScStreamUpServerSecsMax = val; root.editDirty = false }
onEdited: root.editDirty = true
}
CaptionTextType {
@@ -671,8 +760,9 @@ PageType {
Layout.rightMargin: 16
minValue: xhttpScMinPostsIntervalMsMin
maxValue: xhttpScMinPostsIntervalMsMax
onMinChanged: xhttpScMinPostsIntervalMsMin = val
onMaxChanged: xhttpScMinPostsIntervalMsMax = val
onMinChanged: function(val) { xhttpScMinPostsIntervalMsMin = val; root.editDirty = false }
onMaxChanged: function(val) { xhttpScMinPostsIntervalMsMax = val; root.editDirty = false }
onEdited: root.editDirty = true
}
// ── Padding and multiplexing ──────────────────────────
@@ -728,10 +818,15 @@ PageType {
anchors.rightMargin: 16
anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin
visible: listView.enabled && XrayConfigModel.hasUnsavedChanges
visible: listView.enabled && (XrayConfigModel.hasUnsavedChanges || root.editDirty)
enabled: visible
text: qsTr("Save")
clickedFunc: function () {
var errs = XrayConfigModel.validationErrors()
if (errs.length > 0) {
PageController.showErrorMessage(errs.join("\n"))
return
}
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 yesButtonText = qsTr("Continue")
@@ -15,6 +15,8 @@ import "../Components"
PageType {
id: root
property bool editDirty: false
BackButtonType {
id: backButton
anchors.top: parent.top
@@ -61,8 +63,9 @@ PageType {
Layout.rightMargin: 16
minValue: xPaddingBytesMin
maxValue: xPaddingBytesMax
onMinChanged: xPaddingBytesMin = val
onMaxChanged: xPaddingBytesMax = val
onMinChanged: function(val) { xPaddingBytesMin = val; root.editDirty = false }
onMaxChanged: function(val) { xPaddingBytesMax = val; root.editDirty = false }
onEdited: root.editDirty = true
}
Item {
@@ -81,7 +84,7 @@ PageType {
anchors.rightMargin: 16
anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin
visible: listView.enabled && XrayConfigModel.hasUnsavedChanges
visible: listView.enabled && (XrayConfigModel.hasUnsavedChanges || root.editDirty)
enabled: visible
text: qsTr("Save")
clickedFunc: function () {
@@ -15,6 +15,8 @@ import "../Components"
PageType {
id: root
property bool editDirty: false
BackButtonType {
id: backButton
anchors.top: parent.top
@@ -78,8 +80,13 @@ PageType {
Layout.topMargin: 16
headerText: qsTr("xPaddingKey")
textField.text: xPaddingKey
textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9_-]*$/ }
textField.onTextEdited: root.editDirty = (textField.text !== xPaddingKey)
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
headerText: qsTr("xPaddingHeader")
textField.text: xPaddingHeader
textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9_-]*$/ }
textField.onTextEdited: root.editDirty = (textField.text !== xPaddingHeader)
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 {
id: placementDropDown
fitContent: true
Layout.fillWidth: true
Layout.topMargin: 8
Layout.leftMargin: 16
@@ -140,6 +153,7 @@ PageType {
DropDownType {
id: methodDropDown
fitContent: true
Layout.fillWidth: true
Layout.topMargin: 8
Layout.leftMargin: 16
@@ -197,7 +211,7 @@ PageType {
anchors.rightMargin: 16
anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin
visible: listView.enabled && XrayConfigModel.hasUnsavedChanges
visible: listView.enabled && (XrayConfigModel.hasUnsavedChanges || root.editDirty)
enabled: visible
text: qsTr("Save")
clickedFunc: function () {
@@ -15,6 +15,21 @@ import "../Components"
PageType {
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 {
id: backButton
anchors.top: parent.top
@@ -78,8 +93,9 @@ PageType {
Layout.rightMargin: 16
minValue: xmuxMaxConcurrencyMin
maxValue: xmuxMaxConcurrencyMax
onMinChanged: xmuxMaxConcurrencyMin = val
onMaxChanged: xmuxMaxConcurrencyMax = val
onMinChanged: function(val) { xmuxMaxConcurrencyMin = val; root.editDirty = false }
onMaxChanged: function(val) { xmuxMaxConcurrencyMax = val; root.editDirty = false }
onEdited: root.editDirty = true
}
// maxConnections
@@ -98,8 +114,9 @@ PageType {
Layout.rightMargin: 16
minValue: xmuxMaxConnectionsMin
maxValue: xmuxMaxConnectionsMax
onMinChanged: xmuxMaxConnectionsMin = val
onMaxChanged: xmuxMaxConnectionsMax = val
onMinChanged: function(val) { xmuxMaxConnectionsMin = val; root.editDirty = false }
onMaxChanged: function(val) { xmuxMaxConnectionsMax = val; root.editDirty = false }
onEdited: root.editDirty = true
}
// cMaxReuseTimes
@@ -118,8 +135,9 @@ PageType {
Layout.rightMargin: 16
minValue: xmuxCMaxReuseTimesMin
maxValue: xmuxCMaxReuseTimesMax
onMinChanged: xmuxCMaxReuseTimesMin = val
onMaxChanged: xmuxCMaxReuseTimesMax = val
onMinChanged: function(val) { xmuxCMaxReuseTimesMin = val; root.editDirty = false }
onMaxChanged: function(val) { xmuxCMaxReuseTimesMax = val; root.editDirty = false }
onEdited: root.editDirty = true
}
// hMaxRequestTimes
@@ -138,8 +156,9 @@ PageType {
Layout.rightMargin: 16
minValue: xmuxHMaxRequestTimesMin
maxValue: xmuxHMaxRequestTimesMax
onMinChanged: xmuxHMaxRequestTimesMin = val
onMaxChanged: xmuxHMaxRequestTimesMax = val
onMinChanged: function(val) { xmuxHMaxRequestTimesMin = val; root.editDirty = false }
onMaxChanged: function(val) { xmuxHMaxRequestTimesMax = val; root.editDirty = false }
onEdited: root.editDirty = true
}
// hMaxReusableSecs
@@ -158,8 +177,9 @@ PageType {
Layout.rightMargin: 16
minValue: xmuxHMaxReusableSecsMin
maxValue: xmuxHMaxReusableSecsMax
onMinChanged: xmuxHMaxReusableSecsMin = val
onMaxChanged: xmuxHMaxReusableSecsMax = val
onMinChanged: function(val) { xmuxHMaxReusableSecsMin = val; root.editDirty = false }
onMaxChanged: function(val) { xmuxHMaxReusableSecsMax = val; root.editDirty = false }
onEdited: root.editDirty = true
}
TextFieldWithHeaderType {
@@ -168,12 +188,16 @@ PageType {
Layout.rightMargin: 16
Layout.topMargin: 16
headerText: qsTr("hKeepAlivePeriod")
subtitleText: qsTr("Integer, may be negative")
textField.text: xmuxHKeepAlivePeriod
textField.validator: IntValidator {
bottom: 0
}
textField.maximumLength: 11
textField.validator: RegularExpressionValidator { regularExpression: /^-?\d*$/ }
textField.onTextEdited: root.editDirty = (textField.text !== xmuxHKeepAlivePeriod)
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.bottomMargin: 16 + PageController.safeAreaBottomMargin
visible: listView.enabled && XrayConfigModel.hasUnsavedChanges
visible: listView.enabled && (XrayConfigModel.hasUnsavedChanges || root.editDirty)
enabled: visible
text: qsTr("Save")
clickedFunc: function () {
+1
View File
@@ -91,6 +91,7 @@ PageType {
}
function onExportErrorOccurred(error) {
PageController.showBusyIndicator(false)
PageController.showErrorMessage(error)
}
}
+1 -1
View File
@@ -53,7 +53,7 @@ Window {
}
}
visible: true
visible: !GC.isDesktop()
width: GC.screenWidth
height: GC.screenHeight
minimumWidth: GC.isDesktop() ? 360 : 0
+4 -4
View File
@@ -19,12 +19,12 @@ class AmneziaVPN(ConanFile):
if has_service:
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("win-split-tunnel/1.2.5.0")
self.requires("wintun/0.14.1")
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("tun2socks/2.6.0")
@@ -32,13 +32,13 @@ class AmneziaVPN(ConanFile):
self.requires("v2ray-rules-dat/202603162227")
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("openvpnadapter/1.0.0")
if os == "Android":
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")
# 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);
}
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()
{
#ifdef MZ_DEBUG
-4
View File
@@ -17,10 +17,6 @@ class IpcServer : public IpcInterfaceSource
{
public:
explicit IpcServer(QObject *parent = nullptr);
virtual ~IpcServer();
void resetServiceState();
virtual int createPrivilegedProcess() override;
virtual int routeAddList(const QString &gw, const QStringList &ips) override;
+1 -1
View File
@@ -9,7 +9,7 @@ import platform
class AwgAndroid(ConanFile):
name = "awg-android"
version = "1.1.7"
version = "2.0.1"
settings = "os", "arch", "build_type", "compiler"
def configure(self):
+2 -2
View File
@@ -9,7 +9,7 @@ import os
class AwgApple(ConanFile):
name = "awg-apple"
version = "2.0.1"
version = "2.0.2"
settings = "os", "arch", "compiler"
@property
@@ -39,7 +39,7 @@ class AwgApple(ConanFile):
def source(self):
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):
+2 -2
View File
@@ -8,7 +8,7 @@ import os
class AwgGo(ConanFile):
name = "awg-go"
version = "0.2.16"
version = "0.2.18"
package_type = "application"
settings = "os", "arch"
@@ -42,7 +42,7 @@ class AwgGo(ConanFile):
def source(self):
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):
+2 -2
View File
@@ -8,7 +8,7 @@ import os
class AwgWindows(ConanFile):
name = "awg-windows"
version = "0.1.8"
version = "0.1.9"
settings = "os", "arch"
@property
@@ -63,7 +63,7 @@ class AwgWindows(ConanFile):
def source(self):
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):
tc = AutotoolsToolchain(self)
+3
View File
@@ -650,6 +650,9 @@ class OpenSSLConan(ConanFile):
if self._use_nmake:
self.cpp_info.components["ssl"].libs = ["libssl"]
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:
self.cpp_info.components["ssl"].libs = ["ssl"]
self.cpp_info.components["crypto"].libs = ["crypto"]
+1 -1
View File
@@ -28,7 +28,7 @@ class Openvpn(ConanFile):
def build_requirements(self):
if self._is_windows:
self.tool_requires("cmake/[>=3.14 <4]")
self.tool_requires("cmake/[>=4.2]")
else:
self.tool_requires("libtool/2.4.7")
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]() {
qDebug() << "LocalServer new connection";
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);
m_serverNode.addHostSideConnection(m_server->nextPendingConnection());
if (!m_isRemotingEnabled) {
m_isRemotingEnabled = true;
-2
View File
@@ -41,8 +41,6 @@ public:
QRemoteObjectHost m_serverNode;
bool m_isRemotingEnabled = false;
QPointer<QLocalSocket> m_activeClientSocket;
NetworkWatcher m_networkWatcher;
#ifdef Q_OS_LINUX
DaemonLocalServer server{qApp};