mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-06-25 03:27:55 +03:00
Compare commits
11 Commits
feature/lo
...
fix/valnur
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be95ff56df | ||
|
|
4047aa0ebc | ||
|
|
f75b239d69 | ||
|
|
5d16645b84 | ||
|
|
203a092dc9 | ||
|
|
d8b8590bc4 | ||
|
|
9b8bfaa6f8 | ||
|
|
890103a16a | ||
|
|
56ab82f87f | ||
|
|
3984acbb44 | ||
|
|
cc404378f9 |
2
.github/workflows/deploy.yml
vendored
2
.github/workflows/deploy.yml
vendored
@@ -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'
|
||||
|
||||
@@ -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);
|
||||
|
||||
10
client/android/res/drawable/ic_launcher_background.xml
Normal file
10
client/android/res/drawable/ic_launcher_background.xml
Normal file
@@ -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>
|
||||
7
client/android/res/drawable/ic_launcher_monochrome.xml
Normal file
7
client/android/res/drawable/ic_launcher_monochrome.xml
Normal file
@@ -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%" />
|
||||
6
client/android/res/mipmap-anydpi-v26/icon.xml
Normal file
6
client/android/res/mipmap-anydpi-v26/icon.xml
Normal file
@@ -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>
|
||||
6
client/android/res/mipmap-anydpi-v26/icon_round.xml
Normal file
6
client/android/res/mipmap-anydpi-v26/icon_round.xml
Normal file
@@ -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>
|
||||
BIN
client/android/res/mipmap-hdpi/ic_launcher_foreground.png
Normal file
BIN
client/android/res/mipmap-hdpi/ic_launcher_foreground.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
BIN
client/android/res/mipmap-mdpi/ic_launcher_foreground.png
Normal file
BIN
client/android/res/mipmap-mdpi/ic_launcher_foreground.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.6 KiB |
BIN
client/android/res/mipmap-xhdpi/ic_launcher_foreground.png
Normal file
BIN
client/android/res/mipmap-xhdpi/ic_launcher_foreground.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
BIN
client/android/res/mipmap-xxhdpi/ic_launcher_foreground.png
Normal file
BIN
client/android/res/mipmap-xxhdpi/ic_launcher_foreground.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
BIN
client/android/res/mipmap-xxxhdpi/ic_launcher_foreground.png
Normal file
BIN
client/android/res/mipmap-xxxhdpi/ic_launcher_foreground.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
16
client/android/res/values-v31/styles.xml
Normal file
16
client/android/res/values-v31/styles.xml
Normal 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
client/android/res/values/colors.xml
Normal file
4
client/android/res/values/colors.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#0E0E11</color>
|
||||
</resources>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -38,6 +38,8 @@ namespace amnezia
|
||||
XrayServerConfigInvalid = 215,
|
||||
XrayServerNoVlessClients = 216,
|
||||
XrayRealityKeysReadFailed = 217,
|
||||
ServerContainerRuntimeNotSupported = 218,
|
||||
ContainerRuntimeServiceNotRunning = 219,
|
||||
|
||||
// Ssh connection errors
|
||||
SshRequestDeniedError = 300,
|
||||
@@ -124,5 +126,3 @@ namespace amnezia
|
||||
Q_DECLARE_METATYPE(amnezia::ErrorCode)
|
||||
|
||||
#endif // ERRORCODES_H
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -286,7 +286,7 @@ QPair<QString, QNetworkInterface> NetworkUtilities::getGatewayAndIface()
|
||||
return { resGateway, QNetworkInterface::interfaceFromIndex(resIndex) };
|
||||
#endif
|
||||
#ifdef Q_OS_LINUX
|
||||
constexpr int BUFFER_SIZE = 100;
|
||||
constexpr int BUFFER_SIZE = 8192;
|
||||
int received_bytes = 0, msg_len = 0, route_attribute_len = 0;
|
||||
int sock = -1, msgseq = 0;
|
||||
struct nlmsghdr *nlh, *nlmsg;
|
||||
@@ -294,7 +294,7 @@ QPair<QString, QNetworkInterface> NetworkUtilities::getGatewayAndIface()
|
||||
// This struct contain route attributes (route type)
|
||||
struct rtattr *route_attribute;
|
||||
char gateway_address[INET_ADDRSTRLEN], interface[IF_NAMESIZE];
|
||||
char msgbuf[BUFFER_SIZE], buffer[BUFFER_SIZE];
|
||||
char msgbuf[100], buffer[BUFFER_SIZE];
|
||||
char *ptr = buffer;
|
||||
struct timeval tv;
|
||||
|
||||
@@ -339,8 +339,8 @@ QPair<QString, QNetworkInterface> NetworkUtilities::getGatewayAndIface()
|
||||
nlh = (struct nlmsghdr *) ptr;
|
||||
|
||||
/* Check if the header is valid */
|
||||
if((NLMSG_OK(nlmsg, received_bytes) == 0) ||
|
||||
(nlmsg->nlmsg_type == NLMSG_ERROR))
|
||||
if((NLMSG_OK(nlh, received_bytes) == 0) ||
|
||||
(nlh->nlmsg_type == NLMSG_ERROR))
|
||||
{
|
||||
perror("Error in received packet");
|
||||
return {};
|
||||
@@ -355,13 +355,15 @@ QPair<QString, QNetworkInterface> NetworkUtilities::getGatewayAndIface()
|
||||
}
|
||||
|
||||
/* Break if its not a multi part message */
|
||||
if ((nlmsg->nlmsg_flags & NLM_F_MULTI) == 0)
|
||||
if ((nlh->nlmsg_flags & NLM_F_MULTI) == 0)
|
||||
break;
|
||||
}
|
||||
while ((nlmsg->nlmsg_seq != msgseq) || (nlmsg->nlmsg_pid != getpid()));
|
||||
while ((nlh->nlmsg_seq != msgseq) || (nlh->nlmsg_pid != getpid()));
|
||||
|
||||
/* parse response */
|
||||
for ( ; NLMSG_OK(nlh, received_bytes); nlh = NLMSG_NEXT(nlh, received_bytes))
|
||||
int remaining = msg_len + received_bytes;
|
||||
nlh = (struct nlmsghdr *) buffer;
|
||||
for ( ; NLMSG_OK(nlh, remaining); nlh = NLMSG_NEXT(nlh, remaining))
|
||||
{
|
||||
/* Get the route data */
|
||||
route_entry = (struct rtmsg *) NLMSG_DATA(nlh);
|
||||
@@ -370,6 +372,10 @@ QPair<QString, QNetworkInterface> NetworkUtilities::getGatewayAndIface()
|
||||
if (route_entry->rtm_table != RT_TABLE_MAIN)
|
||||
continue;
|
||||
|
||||
/* Reset per-route to avoid cross-route state pollution */
|
||||
memset(gateway_address, 0, sizeof(gateway_address));
|
||||
memset(interface, 0, sizeof(interface));
|
||||
|
||||
route_attribute = (struct rtattr *) RTM_RTA(route_entry);
|
||||
route_attribute_len = RTM_PAYLOAD(nlh);
|
||||
|
||||
@@ -395,6 +401,8 @@ QPair<QString, QNetworkInterface> NetworkUtilities::getGatewayAndIface()
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!(*gateway_address) || !(*interface))
|
||||
qDebug() << "getGatewayAndIface: no gateway found";
|
||||
close(sock);
|
||||
return { gateway_address, QNetworkInterface::interfaceFromName(interface) };
|
||||
#endif
|
||||
|
||||
@@ -613,7 +613,7 @@ void Daemon::checkHandshake() {
|
||||
pendingHandshakes++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Check again if there were connections that haven't completed a handshake.
|
||||
if (pendingHandshakes > 0) {
|
||||
m_handshakeTimer.start(HANDSHAKE_POLL_MSEC);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -7,8 +7,11 @@
|
||||
#include <net/if.h>
|
||||
|
||||
#include <QDBusVariant>
|
||||
#include <QNetworkInterface>
|
||||
#include <QTimer>
|
||||
#include <QtDBus/QtDBus>
|
||||
|
||||
#include "core/networkUtilities.h"
|
||||
#include "leakdetector.h"
|
||||
#include "logger.h"
|
||||
|
||||
@@ -27,24 +30,56 @@ DnsUtilsLinux::DnsUtilsLinux(QObject* parent) : DnsUtils(parent) {
|
||||
logger.debug() << "DnsUtilsLinux created.";
|
||||
|
||||
QDBusConnection conn = QDBusConnection::systemBus();
|
||||
m_resolver = new QDBusInterface(DBUS_RESOLVE_SERVICE, DBUS_RESOLVE_PATH,
|
||||
DBUS_RESOLVE_MANAGER, conn, this);
|
||||
auto* watcher = new QDBusServiceWatcher(
|
||||
DBUS_RESOLVE_SERVICE, conn,
|
||||
QDBusServiceWatcher::WatchForRegistration |
|
||||
QDBusServiceWatcher::WatchForUnregistration, this);
|
||||
|
||||
connect(watcher, &QDBusServiceWatcher::serviceRegistered,
|
||||
this, &DnsUtilsLinux::onResolverRegistered);
|
||||
connect(watcher, &QDBusServiceWatcher::serviceUnregistered,
|
||||
this, &DnsUtilsLinux::onResolverUnregistered);
|
||||
|
||||
if (conn.interface()->isServiceRegistered(DBUS_RESOLVE_SERVICE)) {
|
||||
onResolverRegistered();
|
||||
}
|
||||
}
|
||||
|
||||
void DnsUtilsLinux::onResolverRegistered() {
|
||||
m_resolver.reset(new QDBusInterface(DBUS_RESOLVE_SERVICE, DBUS_RESOLVE_PATH,
|
||||
DBUS_RESOLVE_MANAGER,
|
||||
QDBusConnection::systemBus()));
|
||||
logger.debug() << "systemd-resolved available, DNS resolver initialized";
|
||||
|
||||
if (!m_pendingIfname.isEmpty()) {
|
||||
logger.debug() << "Re-applying DNS configuration for" << m_pendingIfname;
|
||||
updateResolvers(m_pendingIfname, m_pendingResolvers);
|
||||
}
|
||||
}
|
||||
|
||||
void DnsUtilsLinux::onResolverUnregistered() {
|
||||
logger.debug() << "systemd-resolved disappeared, dropping DNS resolver";
|
||||
m_resolver.reset();
|
||||
}
|
||||
|
||||
DnsUtilsLinux::~DnsUtilsLinux() {
|
||||
MZ_COUNT_DTOR(DnsUtilsLinux);
|
||||
|
||||
for (auto iterator = m_linkDomains.constBegin();
|
||||
iterator != m_linkDomains.constEnd(); ++iterator) {
|
||||
QList<QVariant> argumentList;
|
||||
argumentList << QVariant::fromValue(iterator.key());
|
||||
argumentList << QVariant::fromValue(iterator.value());
|
||||
m_resolver->asyncCallWithArgumentList(QStringLiteral("SetLinkDomains"),
|
||||
argumentList);
|
||||
}
|
||||
if (m_revertOnDestroy && m_resolver) {
|
||||
if (m_gatewayIfindex > 0)
|
||||
setLinkDefaultRoute(m_gatewayIfindex, true);
|
||||
|
||||
if (m_ifindex > 0) {
|
||||
m_resolver->asyncCall(QStringLiteral("RevertLink"), m_ifindex);
|
||||
for (auto iterator = m_linkDomains.constBegin();
|
||||
iterator != m_linkDomains.constEnd(); ++iterator) {
|
||||
QList<QVariant> argumentList;
|
||||
argumentList << QVariant::fromValue(iterator.key());
|
||||
argumentList << QVariant::fromValue(iterator.value());
|
||||
m_resolver->asyncCallWithArgumentList(QStringLiteral("SetLinkDomains"),
|
||||
argumentList);
|
||||
}
|
||||
if (m_ifindex > 0) {
|
||||
m_resolver->asyncCall(QStringLiteral("RevertLink"), m_ifindex);
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug() << "DnsUtilsLinux destroyed.";
|
||||
@@ -52,12 +87,31 @@ DnsUtilsLinux::~DnsUtilsLinux() {
|
||||
|
||||
bool DnsUtilsLinux::updateResolvers(const QString& ifname,
|
||||
const QList<QHostAddress>& resolvers) {
|
||||
if (m_gatewayIfindex > 0) {
|
||||
setLinkDefaultRoute(m_gatewayIfindex, true);
|
||||
m_gatewayIfindex = 0;
|
||||
}
|
||||
|
||||
m_ifindex = if_nametoindex(qPrintable(ifname));
|
||||
if (m_ifindex <= 0) {
|
||||
logger.error() << "Unable to resolve ifindex for" << ifname;
|
||||
return false;
|
||||
}
|
||||
|
||||
m_pendingIfname = ifname;
|
||||
m_pendingResolvers = resolvers;
|
||||
|
||||
if (!m_resolver) {
|
||||
logger.debug() << "systemd-resolved not ready, queuing DNS configuration";
|
||||
return true;
|
||||
}
|
||||
|
||||
const int gwIdx = NetworkUtilities::getGatewayAndIface().second.index();
|
||||
if (gwIdx > 0 && gwIdx != m_ifindex && gwIdx != m_gatewayIfindex) {
|
||||
m_gatewayIfindex = gwIdx;
|
||||
setLinkDefaultRoute(gwIdx, false);
|
||||
}
|
||||
|
||||
setLinkDNS(m_ifindex, resolvers);
|
||||
setLinkDefaultRoute(m_ifindex, true);
|
||||
updateLinkDomains();
|
||||
@@ -65,6 +119,15 @@ bool DnsUtilsLinux::updateResolvers(const QString& ifname,
|
||||
}
|
||||
|
||||
bool DnsUtilsLinux::restoreResolvers() {
|
||||
m_revertOnDestroy = true;
|
||||
m_pendingIfname.clear();
|
||||
m_pendingResolvers.clear();
|
||||
|
||||
if (m_gatewayIfindex > 0) {
|
||||
setLinkDefaultRoute(m_gatewayIfindex, true);
|
||||
m_gatewayIfindex = 0;
|
||||
}
|
||||
|
||||
for (auto iterator = m_linkDomains.constBegin();
|
||||
iterator != m_linkDomains.constEnd(); ++iterator) {
|
||||
setLinkDomains(iterator.key(), iterator.value());
|
||||
@@ -72,7 +135,7 @@ bool DnsUtilsLinux::restoreResolvers() {
|
||||
m_linkDomains.clear();
|
||||
|
||||
/* Revert the VPN interface's DNS configuration */
|
||||
if (m_ifindex > 0) {
|
||||
if (m_ifindex > 0 && m_resolver) {
|
||||
QList<QVariant> argumentList = {QVariant::fromValue(m_ifindex)};
|
||||
QDBusPendingReply<> reply = m_resolver->asyncCallWithArgumentList(
|
||||
QStringLiteral("RevertLink"), argumentList);
|
||||
@@ -90,13 +153,14 @@ bool DnsUtilsLinux::restoreResolvers() {
|
||||
void DnsUtilsLinux::dnsCallCompleted(QDBusPendingCallWatcher* call) {
|
||||
QDBusPendingReply<> reply = *call;
|
||||
if (reply.isError()) {
|
||||
logger.error() << "Error received from the DBus service";
|
||||
logger.debug() << "DBus call failed (may be transient after systemd-resolved restart)";
|
||||
}
|
||||
delete call;
|
||||
}
|
||||
|
||||
void DnsUtilsLinux::setLinkDNS(int ifindex,
|
||||
const QList<QHostAddress>& resolvers) {
|
||||
if (!m_resolver) return;
|
||||
QList<DnsResolver> resolverList;
|
||||
char ifnamebuf[IF_NAMESIZE];
|
||||
const char* ifname = if_indextoname(ifindex, ifnamebuf);
|
||||
@@ -121,6 +185,7 @@ void DnsUtilsLinux::setLinkDNS(int ifindex,
|
||||
|
||||
void DnsUtilsLinux::setLinkDomains(int ifindex,
|
||||
const QList<DnsLinkDomain>& domains) {
|
||||
if (!m_resolver) return;
|
||||
char ifnamebuf[IF_NAMESIZE];
|
||||
const char* ifname = if_indextoname(ifindex, ifnamebuf);
|
||||
if (ifname) {
|
||||
@@ -144,6 +209,7 @@ void DnsUtilsLinux::setLinkDomains(int ifindex,
|
||||
}
|
||||
|
||||
void DnsUtilsLinux::setLinkDefaultRoute(int ifindex, bool enable) {
|
||||
if (!m_resolver) return;
|
||||
QList<QVariant> argumentList;
|
||||
argumentList << QVariant::fromValue(ifindex);
|
||||
argumentList << QVariant::fromValue(enable);
|
||||
@@ -156,6 +222,7 @@ void DnsUtilsLinux::setLinkDefaultRoute(int ifindex, bool enable) {
|
||||
}
|
||||
|
||||
void DnsUtilsLinux::updateLinkDomains() {
|
||||
if (!m_resolver) return;
|
||||
/* Get the list of search domains, and remove any others that might conspire
|
||||
* to satisfy DNS resolution. Unfortunately, this is a pain because Qt doesn't
|
||||
* seem to be able to demarshall complex property types.
|
||||
@@ -174,11 +241,20 @@ void DnsUtilsLinux::updateLinkDomains() {
|
||||
|
||||
void DnsUtilsLinux::dnsDomainsReceived(QDBusPendingCallWatcher* call) {
|
||||
QDBusPendingReply<QVariant> reply = *call;
|
||||
call->deleteLater();
|
||||
if (reply.isError()) {
|
||||
logger.error() << "Error retrieving the DNS domains from the DBus service";
|
||||
delete call;
|
||||
// systemd-resolved may still be starting up after a restart — retry a few times
|
||||
if (m_ifindex > 0 && m_domainRetries++ < 5) {
|
||||
logger.debug() << "systemd-resolved not ready yet, retrying DNS setup ("
|
||||
<< m_domainRetries << "/5)";
|
||||
QTimer::singleShot(500, this, &DnsUtilsLinux::updateLinkDomains);
|
||||
} else {
|
||||
logger.warning() << "Failed to configure DNS after 5 retries";
|
||||
m_domainRetries = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
m_domainRetries = 0;
|
||||
|
||||
/* Update the state of the DNS domains */
|
||||
m_linkDomains.clear();
|
||||
@@ -204,9 +280,17 @@ void DnsUtilsLinux::dnsDomainsReceived(QDBusPendingCallWatcher* call) {
|
||||
}
|
||||
|
||||
/* Add a root search domain for the new interface. */
|
||||
QList<DnsLinkDomain> newlist = {root};
|
||||
setLinkDomains(m_ifindex, newlist);
|
||||
delete call;
|
||||
if (m_ifindex > 0) {
|
||||
setLinkDomains(m_ifindex, {root});
|
||||
|
||||
/* Disable DefaultRoute on the physical gateway so systemd-resolved
|
||||
* routes all DNS through the VPN interface. */
|
||||
const int gwIdx = NetworkUtilities::getGatewayAndIface().second.index();
|
||||
if (gwIdx > 0 && gwIdx != m_ifindex && gwIdx != m_gatewayIfindex) {
|
||||
m_gatewayIfindex = gwIdx;
|
||||
setLinkDefaultRoute(gwIdx, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static DnsMetatypeRegistrationProxy s_dnsMetatypeProxy;
|
||||
|
||||
@@ -6,7 +6,12 @@
|
||||
#define DNSUTILSLINUX_H
|
||||
|
||||
#include <QDBusInterface>
|
||||
#include <QScopedPointer>
|
||||
#include <QDBusPendingCallWatcher>
|
||||
#include <QDBusServiceWatcher>
|
||||
#include <QHostAddress>
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
|
||||
#include "daemon/dnsutils.h"
|
||||
#include "dbustypeslinux.h"
|
||||
@@ -29,13 +34,20 @@ class DnsUtilsLinux final : public DnsUtils {
|
||||
void updateLinkDomains();
|
||||
|
||||
private slots:
|
||||
void onResolverRegistered();
|
||||
void onResolverUnregistered();
|
||||
void dnsCallCompleted(QDBusPendingCallWatcher*);
|
||||
void dnsDomainsReceived(QDBusPendingCallWatcher*);
|
||||
|
||||
private:
|
||||
int m_ifindex = 0;
|
||||
int m_gatewayIfindex = 0;
|
||||
int m_domainRetries = 0;
|
||||
bool m_revertOnDestroy = false;
|
||||
QMap<int, DnsLinkDomainList> m_linkDomains;
|
||||
QDBusInterface* m_resolver = nullptr;
|
||||
QScopedPointer<QDBusInterface> m_resolver;
|
||||
QString m_pendingIfname;
|
||||
QList<QHostAddress> m_pendingResolvers;
|
||||
};
|
||||
|
||||
#endif // DNSUTILSLINUX_H
|
||||
|
||||
@@ -454,16 +454,33 @@ void LinuxFirewall::updateDNSServers(const QStringList& servers)
|
||||
static QStringList existingServers {};
|
||||
|
||||
existingServers = servers;
|
||||
execute(QStringLiteral("iptables -F %1.320.allowDNS").arg(kAnchorName));
|
||||
for (const QString& rule : getDNSRules(servers))
|
||||
execute(QStringLiteral("iptables -A %1.320.allowDNS %2").arg(kAnchorName, rule));
|
||||
const QString chain = QStringLiteral("%1.320.allowDNS").arg(kAnchorName);
|
||||
executeIptables(QStringLiteral("iptables"), {QStringLiteral("-F"), chain});
|
||||
const QStringList ifaces = {
|
||||
QStringLiteral("amn0+"), QStringLiteral("tun0+"), QStringLiteral("tun2+")
|
||||
};
|
||||
for (const QString& server : servers) {
|
||||
for (const QString& iface : ifaces) {
|
||||
executeIptables(QStringLiteral("iptables"),
|
||||
{QStringLiteral("-A"), chain, QStringLiteral("-o"), iface,
|
||||
QStringLiteral("-d"), server, QStringLiteral("-p"), QStringLiteral("udp"),
|
||||
QStringLiteral("--dport"), QStringLiteral("53"), QStringLiteral("-j"), QStringLiteral("ACCEPT")});
|
||||
executeIptables(QStringLiteral("iptables"),
|
||||
{QStringLiteral("-A"), chain, QStringLiteral("-o"), iface,
|
||||
QStringLiteral("-d"), server, QStringLiteral("-p"), QStringLiteral("tcp"),
|
||||
QStringLiteral("--dport"), QStringLiteral("53"), QStringLiteral("-j"), QStringLiteral("ACCEPT")});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LinuxFirewall::updateAllowNets(const QStringList& servers)
|
||||
{
|
||||
execute(QStringLiteral("iptables -F %1.110.allowNets").arg(kAnchorName));
|
||||
for (const QString& rule : getAllowRule(servers))
|
||||
execute(QStringLiteral("iptables -A %1.110.allowNets %2").arg(kAnchorName, rule));
|
||||
const QString chain = QStringLiteral("%1.110.allowNets").arg(kAnchorName);
|
||||
executeIptables(QStringLiteral("iptables"), {QStringLiteral("-F"), chain});
|
||||
for (const QString& server : servers)
|
||||
executeIptables(QStringLiteral("iptables"),
|
||||
{QStringLiteral("-A"), chain, QStringLiteral("-d"), server,
|
||||
QStringLiteral("-j"), QStringLiteral("ACCEPT")});
|
||||
}
|
||||
|
||||
void LinuxFirewall::updateBlockNets(const QStringList& servers)
|
||||
@@ -471,9 +488,12 @@ void LinuxFirewall::updateBlockNets(const QStringList& servers)
|
||||
static QStringList existingServers {};
|
||||
|
||||
existingServers = servers;
|
||||
execute(QStringLiteral("iptables -F %1.120.blockNets").arg(kAnchorName));
|
||||
for (const QString& rule : getBlockRule(servers))
|
||||
execute(QStringLiteral("iptables -A %1.120.blockNets %2").arg(kAnchorName, rule));
|
||||
const QString chain = QStringLiteral("%1.120.blockNets").arg(kAnchorName);
|
||||
executeIptables(QStringLiteral("iptables"), {QStringLiteral("-F"), chain});
|
||||
for (const QString& server : servers)
|
||||
executeIptables(QStringLiteral("iptables"),
|
||||
{QStringLiteral("-A"), chain, QStringLiteral("-d"), server,
|
||||
QStringLiteral("-j"), QStringLiteral("REJECT")});
|
||||
}
|
||||
|
||||
int waitForExitCode(QProcess& process)
|
||||
@@ -506,6 +526,24 @@ int LinuxFirewall::execute(const QString &command, bool ignoreErrors)
|
||||
return exitCode;
|
||||
}
|
||||
|
||||
int LinuxFirewall::executeIptables(const QString &program, const QStringList &args, bool ignoreErrors)
|
||||
{
|
||||
QProcess p;
|
||||
p.start(program, args, QProcess::ReadOnly);
|
||||
p.closeWriteChannel();
|
||||
|
||||
int exitCode = waitForExitCode(p);
|
||||
auto out = p.readAllStandardOutput().trimmed();
|
||||
auto err = p.readAllStandardError().trimmed();
|
||||
if ((exitCode != 0 || !err.isEmpty()) && !ignoreErrors)
|
||||
logger.warning() << "(" << exitCode << ") $ " << program << args.join(QLatin1Char(' '));
|
||||
if (!out.isEmpty())
|
||||
logger.info() << out;
|
||||
if (!err.isEmpty())
|
||||
logger.warning() << err;
|
||||
return exitCode;
|
||||
}
|
||||
|
||||
void LinuxFirewall::setupTrafficSplitting()
|
||||
{
|
||||
auto cGroupDir = "/sys/fs/cgroup/net_cls/" BRAND_CODE "vpnexclusions/";
|
||||
|
||||
@@ -85,6 +85,7 @@ private:
|
||||
static void setupTrafficSplitting();
|
||||
static void teardownTrafficSplitting();
|
||||
static int execute(const QString& command, bool ignoreErrors = false);
|
||||
static int executeIptables(const QString& program, const QStringList& args, bool ignoreErrors = false);
|
||||
private:
|
||||
// Chain names
|
||||
static QString kOutputChain, kRootChain, kPostRoutingChain, kPreRoutingChain;
|
||||
|
||||
@@ -237,7 +237,11 @@ bool WireguardUtilsLinux::updatePeer(const InterfaceConfig& config) {
|
||||
// Exclude the server address, except for multihop exit servers.
|
||||
if ((config.m_hopType != InterfaceConfig::MultiHopExit) &&
|
||||
(m_rtmonitor != nullptr)) {
|
||||
m_rtmonitor->addExclusionRoute(IPAddress(config.m_serverIpv4AddrIn));
|
||||
if (!config.m_serverIpv4AddrIn.isEmpty() &&
|
||||
!m_rtmonitor->addExclusionRoute(IPAddress(config.m_serverIpv4AddrIn))) {
|
||||
logger.error() << "No gateway — cannot add server exclusion route";
|
||||
return false;
|
||||
}
|
||||
m_rtmonitor->addExclusionRoute(IPAddress(config.m_serverIpv6AddrIn));
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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/.*\///');\
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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("1–65535")
|
||||
|
||||
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 10–100, 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 () {
|
||||
|
||||
@@ -91,6 +91,7 @@ PageType {
|
||||
}
|
||||
|
||||
function onExportErrorOccurred(error) {
|
||||
PageController.showBusyIndicator(false)
|
||||
PageController.showErrorMessage(error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ Window {
|
||||
}
|
||||
}
|
||||
|
||||
visible: true
|
||||
visible: !GC.isDesktop()
|
||||
width: GC.screenWidth
|
||||
height: GC.screenHeight
|
||||
minimumWidth: GC.isDesktop() ? 360 : 0
|
||||
|
||||
@@ -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
|
||||
|
||||
51
ipc/ipc.h
51
ipc/ipc.h
@@ -3,6 +3,8 @@
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QRegularExpression>
|
||||
#include <QSet>
|
||||
|
||||
#include "../client/core/utils/utilities.h"
|
||||
|
||||
@@ -15,7 +17,8 @@ enum PermittedProcess {
|
||||
OpenVPN,
|
||||
Wireguard,
|
||||
Tun2Socks,
|
||||
CertUtil
|
||||
CertUtil,
|
||||
_Count
|
||||
};
|
||||
|
||||
inline QString permittedProcessPath(PermittedProcess pid)
|
||||
@@ -57,16 +60,56 @@ inline QStringList sanitizeArguments(PermittedProcess proc, const QStringList &a
|
||||
QList<Validator> positionalArgs;
|
||||
|
||||
switch (proc) {
|
||||
case OpenVPN: {
|
||||
static const QSet<QString> blocked = {
|
||||
QStringLiteral("--script-security"),
|
||||
QStringLiteral("--up"),
|
||||
QStringLiteral("--down"),
|
||||
QStringLiteral("--route-up"),
|
||||
QStringLiteral("--ipchange"),
|
||||
QStringLiteral("--tls-verify"),
|
||||
QStringLiteral("--plugin"),
|
||||
QStringLiteral("--auth-user-pass-verify"),
|
||||
QStringLiteral("--learn-address"),
|
||||
QStringLiteral("--client-connect"),
|
||||
QStringLiteral("--client-disconnect"),
|
||||
QStringLiteral("--management"),
|
||||
QStringLiteral("--management-external-key")
|
||||
};
|
||||
QStringList out;
|
||||
for (int i = 0; i < args.size(); ++i) {
|
||||
if (blocked.contains(args[i])) {
|
||||
qWarning() << "IPC: blocked OpenVPN argument:" << args[i];
|
||||
++i; // skip following value
|
||||
continue;
|
||||
}
|
||||
out << args[i];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
case Wireguard: {
|
||||
static const QRegularExpression hookRe(
|
||||
QStringLiteral(R"((?i)(PostUp|PreUp|PostDown|PreDown)\s*=)"));
|
||||
QStringList out;
|
||||
for (const QString& a : args) {
|
||||
if (hookRe.match(a).hasMatch()) {
|
||||
qWarning() << "IPC: blocked WireGuard hook argument:" << a;
|
||||
continue;
|
||||
}
|
||||
out << a;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
case Tun2Socks:
|
||||
namedArgs["-device"] = [](const QString& v) { return v.startsWith("tun://"); };
|
||||
namedArgs["-proxy"] = [](const QString& v) { return v.startsWith("socks5://"); };
|
||||
break;
|
||||
default:
|
||||
//FIXME
|
||||
case CertUtil:
|
||||
return args;
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
QStringList sanitized;
|
||||
|
||||
for (int i = 0, pos = 0; i < args.size(); i++) {
|
||||
|
||||
@@ -22,6 +22,27 @@
|
||||
#include "tapcontroller_win.h"
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_LINUX
|
||||
#include <sys/socket.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
extern uid_t g_allowedUid;
|
||||
extern bool g_allowedUidSet;
|
||||
|
||||
static bool checkPrivPeerCredentials(QLocalSocket *socket) {
|
||||
struct ucred cred{};
|
||||
socklen_t len = sizeof(cred);
|
||||
if (getsockopt(socket->socketDescriptor(), SOL_SOCKET, SO_PEERCRED, &cred, &len) != 0) {
|
||||
qWarning() << "IpcServer: SO_PEERCRED failed, rejecting privileged process connection";
|
||||
return false;
|
||||
}
|
||||
if (cred.uid == 0) return true;
|
||||
if (g_allowedUidSet && cred.uid == g_allowedUid) return true;
|
||||
qWarning() << "IpcServer: rejected privileged process connection from unauthorized UID" << cred.uid;
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
IpcServer::IpcServer(QObject *parent) : IpcInterfaceSource(parent)
|
||||
{
|
||||
@@ -48,8 +69,16 @@ int IpcServer::createPrivilegedProcess()
|
||||
// Make sure any connections are handed to QtRO
|
||||
QObject::connect(pd.localServer.data(), &QLocalServer::newConnection, this, [pd]() {
|
||||
qDebug() << "IpcServer new connection";
|
||||
QLocalSocket *conn = pd.localServer->nextPendingConnection();
|
||||
#ifdef Q_OS_LINUX
|
||||
if (!checkPrivPeerCredentials(conn)) {
|
||||
conn->close();
|
||||
conn->deleteLater();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
if (pd.serverNode) {
|
||||
pd.serverNode->addHostSideConnection(pd.localServer->nextPendingConnection());
|
||||
pd.serverNode->addHostSideConnection(conn);
|
||||
pd.serverNode->enableRemoting(pd.ipcProcess.data());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -77,6 +77,11 @@ void IpcServerProcess::setProcessChannelMode(QProcess::ProcessChannelMode mode)
|
||||
|
||||
void IpcServerProcess::setProgram(int programId)
|
||||
{
|
||||
if (programId <= static_cast<int>(amnezia::PermittedProcess::Invalid) ||
|
||||
programId >= static_cast<int>(amnezia::PermittedProcess::_Count)) {
|
||||
qWarning() << "IPC: invalid programId" << programId << ", ignoring";
|
||||
return;
|
||||
}
|
||||
m_program = static_cast<amnezia::PermittedProcess>(programId);
|
||||
m_process->setProgram(amnezia::permittedProcessPath(m_program));
|
||||
m_process->setArguments({});
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include <QApplication>
|
||||
#include <QHostAddress>
|
||||
#include <QRegularExpression>
|
||||
|
||||
#include "../client/core/utils/protocolEnum.h"
|
||||
#include "../client/core/protocols/protocolUtils.h"
|
||||
@@ -11,6 +12,37 @@
|
||||
#include "qjsonarray.h"
|
||||
#include "version.h"
|
||||
|
||||
#ifdef Q_OS_LINUX
|
||||
static bool isValidIpOrCidr(const QString &value) {
|
||||
static const QRegularExpression re(
|
||||
QStringLiteral(R"(^(\d{1,3}\.){3}\d{1,3}(/\d{1,2})?$)"));
|
||||
if (!re.match(value).hasMatch()) return false;
|
||||
const QStringList ipParts = value.split(QLatin1Char('/'))[0].split(QLatin1Char('.'));
|
||||
for (const QString &part : ipParts) {
|
||||
bool ok;
|
||||
int octet = part.toInt(&ok);
|
||||
if (!ok || octet < 0 || octet > 255) return false;
|
||||
}
|
||||
if (value.contains(QLatin1Char('/'))) {
|
||||
bool ok;
|
||||
int prefix = value.split(QLatin1Char('/'))[1].toInt(&ok);
|
||||
if (!ok || prefix < 0 || prefix > 32) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static QStringList filterIpList(const QStringList &values) {
|
||||
QStringList safe;
|
||||
for (const QString &v : values) {
|
||||
if (isValidIpOrCidr(v))
|
||||
safe << v;
|
||||
else
|
||||
qWarning() << "IPC: rejected invalid IP/CIDR value:" << v;
|
||||
}
|
||||
return safe;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
#include "../client/platforms/windows/daemon/windowsfirewall.h"
|
||||
#include "../client/platforms/windows/daemon/windowsdaemon.h"
|
||||
@@ -166,7 +198,11 @@ bool KillSwitch::disableAllTraffic() {
|
||||
|
||||
bool KillSwitch::resetAllowedRange(const QStringList &ranges) {
|
||||
|
||||
#ifdef Q_OS_LINUX
|
||||
m_allowedRanges = filterIpList(ranges);
|
||||
#else
|
||||
m_allowedRanges = ranges;
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_LINUX
|
||||
LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("110.allowNets"), true);
|
||||
@@ -189,7 +225,12 @@ bool KillSwitch::resetAllowedRange(const QStringList &ranges) {
|
||||
}
|
||||
|
||||
bool KillSwitch::addAllowedRange(const QStringList &ranges) {
|
||||
for (const QString &range : ranges) {
|
||||
#ifdef Q_OS_LINUX
|
||||
const QStringList safeRanges = filterIpList(ranges);
|
||||
#else
|
||||
const QStringList &safeRanges = ranges;
|
||||
#endif
|
||||
for (const QString &range : safeRanges) {
|
||||
if (!range.isEmpty() && !m_allowedRanges.contains(range)) {
|
||||
m_allowedRanges.append(range);
|
||||
}
|
||||
@@ -317,9 +358,9 @@ bool KillSwitch::enableKillSwitch(const QJsonObject &configStr, int vpnAdapterIn
|
||||
LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("000.allowLoopback"), true);
|
||||
LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("100.blockAll"), blockAll);
|
||||
LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("110.allowNets"), allowNets);
|
||||
LinuxFirewall::updateAllowNets(allownets);
|
||||
LinuxFirewall::updateAllowNets(filterIpList(allownets));
|
||||
LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("120.blockNets"), blockAll);
|
||||
LinuxFirewall::updateBlockNets(blocknets);
|
||||
LinuxFirewall::updateBlockNets(filterIpList(blocknets));
|
||||
LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("130.allowMarkedXray"), true);
|
||||
LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("200.allowVPN"), true);
|
||||
LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv6, QStringLiteral("250.blockIPv6"), true);
|
||||
@@ -328,23 +369,36 @@ bool KillSwitch::enableKillSwitch(const QJsonObject &configStr, int vpnAdapterIn
|
||||
LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("310.blockDNS"), true);
|
||||
QStringList dnsServers;
|
||||
|
||||
dnsServers.append(configStr.value(amnezia::configKey::dns1).toString());
|
||||
const QString dns1 = configStr.value(amnezia::configKey::dns1).toString();
|
||||
if (isValidIpOrCidr(dns1))
|
||||
dnsServers.append(dns1);
|
||||
else if (!dns1.isEmpty())
|
||||
qWarning() << "IPC: rejected invalid dns1:" << dns1;
|
||||
|
||||
// We don't use secondary DNS if primary DNS is AmneziaDNS
|
||||
if (!configStr.value(amnezia::configKey::dns1).toString().contains(amnezia::protocols::dns::amneziaDnsIp)) {
|
||||
dnsServers.append(configStr.value(amnezia::configKey::dns2).toString());
|
||||
if (!dns1.contains(amnezia::protocols::dns::amneziaDnsIp)) {
|
||||
const QString dns2 = configStr.value(amnezia::configKey::dns2).toString();
|
||||
if (isValidIpOrCidr(dns2))
|
||||
dnsServers.append(dns2);
|
||||
else if (!dns2.isEmpty())
|
||||
qWarning() << "IPC: rejected invalid dns2:" << dns2;
|
||||
}
|
||||
|
||||
dnsServers.append("127.0.0.1");
|
||||
dnsServers.append("127.0.0.53");
|
||||
|
||||
|
||||
|
||||
for (auto dns : configStr.value(amnezia::configKey::allowedDnsServers).toArray()) {
|
||||
if (!dns.isString()) {
|
||||
break;
|
||||
}
|
||||
dnsServers.append(dns.toString());
|
||||
const QString dnsStr = dns.toString();
|
||||
if (isValidIpOrCidr(dnsStr))
|
||||
dnsServers.append(dnsStr);
|
||||
else if (!dnsStr.isEmpty())
|
||||
qWarning() << "IPC: rejected invalid allowedDnsServer:" << dnsStr;
|
||||
}
|
||||
|
||||
|
||||
LinuxFirewall::updateDNSServers(dnsServers);
|
||||
LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("320.allowDNS"), true);
|
||||
LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("400.allowPIA"), true);
|
||||
|
||||
@@ -17,6 +17,35 @@
|
||||
#include "tapcontroller_win.h"
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_LINUX
|
||||
#include <sys/socket.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
|
||||
uid_t g_allowedUid = static_cast<uid_t>(-1);
|
||||
bool g_allowedUidSet = false;
|
||||
|
||||
static bool checkPeerCredentials(QLocalSocket *socket) {
|
||||
struct ucred cred{};
|
||||
socklen_t len = sizeof(cred);
|
||||
if (getsockopt(socket->socketDescriptor(), SOL_SOCKET, SO_PEERCRED, &cred, &len) != 0) {
|
||||
qWarning() << "LocalServer: SO_PEERCRED failed, rejecting connection";
|
||||
return false;
|
||||
}
|
||||
if (cred.uid == 0) return true;
|
||||
if (!g_allowedUidSet) {
|
||||
g_allowedUid = cred.uid;
|
||||
g_allowedUidSet = true;
|
||||
qDebug() << "LocalServer: registered session UID" << g_allowedUid;
|
||||
}
|
||||
if (cred.uid != g_allowedUid) {
|
||||
qWarning() << "LocalServer: rejected connection from unauthorized UID" << cred.uid;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
Logger logger("WgDaemonServer");
|
||||
}
|
||||
@@ -35,7 +64,15 @@ LocalServer::LocalServer(QObject *parent) : QObject(parent),
|
||||
|
||||
QObject::connect(m_server.data(), &QLocalServer::newConnection, this, [this]() {
|
||||
qDebug() << "LocalServer new connection";
|
||||
m_serverNode.addHostSideConnection(m_server->nextPendingConnection());
|
||||
QLocalSocket *conn = m_server->nextPendingConnection();
|
||||
#ifdef Q_OS_LINUX
|
||||
if (!checkPeerCredentials(conn)) {
|
||||
conn->close();
|
||||
conn->deleteLater();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
m_serverNode.addHostSideConnection(conn);
|
||||
|
||||
if (!m_isRemotingEnabled) {
|
||||
m_isRemotingEnabled = true;
|
||||
|
||||
@@ -167,22 +167,27 @@ bool RouterLinux::flushDns()
|
||||
|
||||
//check what the dns manager use
|
||||
if (isServiceActive("nscd.service")) {
|
||||
qDebug() << "Restarting nscd.service";
|
||||
p.start("systemctl", { "restart", "nscd" });
|
||||
qDebug() << "Flushing nscd cache";
|
||||
p.start("nscd", { "--invalidate=hosts" });
|
||||
} else if (isServiceActive("systemd-resolved.service")) {
|
||||
qDebug() << "Restarting systemd-resolved.service";
|
||||
p.start("systemctl", { "restart", "systemd-resolved" });
|
||||
qDebug() << "Flushing systemd-resolved DNS cache";
|
||||
p.start("resolvectl", { "flush-caches" });
|
||||
} else {
|
||||
qDebug() << "No suitable DNS manager found.";
|
||||
return false;
|
||||
}
|
||||
|
||||
p.waitForFinished();
|
||||
QByteArray output(p.readAll());
|
||||
QByteArray output = p.readAll();
|
||||
if ((p.exitStatus() != QProcess::NormalExit) || (p.exitCode() != 0)) {
|
||||
qDebug().noquote() << "Failed to flush DNS: " + output;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (output.isEmpty())
|
||||
qDebug().noquote() << "Flush dns completed";
|
||||
else
|
||||
qDebug().noquote() << "OUTPUT systemctl restart nscd/systemd-resolved: " + output;
|
||||
qDebug().noquote() << "OUTPUT dns flush: " + output;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user