Compare commits

...

23 Commits

Author SHA1 Message Date
Pokamest Nikak
4fbad76d81 OpenVPN import fix 2025-05-06 16:12:38 +01:00
aiamnezia
b86356b0cc bugfix: fix ListViewType scrolling (#1550)
* Fix ListViewType scrolling on country selection page

* Disable highlightFollowsCurrentItem for country selection page

* Fix scrolling on container DropDown

* Fix ListView height

* Fix listview layout in DropDownType

* Remove unnecessary MouseArea from country selection page
2025-05-03 13:56:50 +07:00
Mykola Baibuz
f6d7552b58 feature: fillswitch strict mode (#1333)
* Add allowed DNS list for killswitch

* Windows killswitch strict mode backend part

* Killswitch strict mode for Linux and MacOS

* Windows fixes

* feature: Add Kill Switch settings page with strict mode option

* fix windows build after merge

* Refresh killswitch mode when it toggled

* Use HLM to store strictMode flag

* Some Linux updates

* feat: Enhance VerticalRadioButton with improved styling and disabled states

* Refresh killSwitch state update

* Fix build

* refactor: Modularize header components

* Change kill switch radio button styling

* Fix strict kill switch mode handling

* Refactor: Replace HeaderType with new Types for headers in QML pages

* Remove deprecated HeaderType QML component

* Refresh strict mode killswitch after global toggle change

* Implement model, controller and UI for killswitch dns exceptions

* Connect backend part and UI

* Change label text to DNS exceptions

* Remove HeaderType from PageSettingsApiDevices

* Some pretty fixes

* Fix problem with definition sequence of PageSettingsKillSwitchExceptions.pml elements

* Add exclusion method for Windows firewall

* Change ubuntu version in deploy script

* Update ubuntu version in GH actions

* Add confirmation popup for strict killswitch mode

* Add qt standard path for build script

* Add method to killswitch for expanding strickt mode exceptions list and fix allowTrafficTo() for Windows. Also Added cache in KillSwitch class for exceptions

* Add insertion of gateway address to strict killswitch exceptions

* Review fixes

* buildfix and naming

---------

Co-authored-by: aiamnezia <ai@amnezia.org>
2025-05-03 13:54:36 +07:00
Mykola Baibuz
5bd88ac2e9 bugfix: check IPv6 support before IPv6 setup for OpenVPN (#1552) 2025-05-03 13:52:59 +07:00
Mykola Baibuz
94fa5b59f3 bugfix: awg/wg protocol with system disabled IPv6 (#1536)
* fix: AWG/WG protocol with system disabled IPv6

* add check for route prefix type

* fix: ignore IPv6 setup error for Linux

This error can be cased by system disabled IPv6
2025-05-03 13:51:49 +07:00
lunardunno
7169480999 feature: error handling for cgroup (#1486)
* Error for cgroup mountpoint

Added handling of message: cgroup mountpoint does not exist.

* Case for error cgroup

Added case and case description for: Cgroup Mountpoint Does Not Exist

* Case for Runc

Added error handling for Runc, which does not work in cgroup v2.
Changed numbering of new errors.

* stdErr handling fot run_container

Enabling stdErr handling fot run_container.sh

* change for stdErr handling

* Another place to handle the error 211

Another place to handle the error: ServerRuncNotWorkOnCgroupsV2

* test_1

* test 2

* test 3

* Moving error handling

Moving error handling to the right place in the controller.

* Polishing

* Еext correction

Сorrection of description text.
2025-04-23 12:12:23 +07:00
Mikhail Kiselev
c44ce0d77c fix: add missing include (#1541) 2025-04-19 23:21:10 +07:00
Nethius
7fd71a8408 feature: retrieving support info from gateway (#1483)
* feature: retrieving support info from gateway

* feature: added "external-premium" service-type

* chore: fixed external premium visability
2025-04-16 09:58:44 +07:00
DarthSidious007
68db721089 add S3 deploy (#1530) 2025-04-16 09:35:53 +07:00
MrMirDan
a180e12bdf chore: updated ru translation (#1531) 2025-04-12 22:04:34 +07:00
Yaroslav
f3a4a1b1be feat: improve post uninstall script for macos to properly remove application and its components (#1521) 2025-04-11 23:09:12 +07:00
Nethius
6977a8ecbc chore: bump version and update translation files (#1526) 2025-04-11 12:59:06 +07:00
Nethius
d00f64e6ad feature: added export logs button on start page (#1525) 2025-04-11 12:29:28 +07:00
Mykola Baibuz
d5b3da6ba3 Use older Ubuntu version for build job (#1523) 2025-04-11 08:57:56 +07:00
aiamnezia
c245318339 bugfix: empty split tunneling list (#1520)
* Disable split tunneling with empty list

* Fix bug with Amnezia DNS in split tunneling list

* update ubuntu version for linux deploy pipeline

* Fix deploy script
2025-04-10 14:24:33 +07:00
Nethius
b3b0fec2e1 feature: additional logs for proxy bypass (#1518) 2025-04-09 10:47:33 +07:00
Nethius
9d571a4c71 feature: new mirrors support (#1519) 2025-04-08 12:07:31 +07:00
pokamest
f283858490 Merge pull request #1517 from amnezia-vpn/chore/update-go-version
Update go version in actions to 1.24
2025-04-07 21:53:05 +01:00
pokamest
76fe203767 Update go version in actions to 1.24 2025-04-07 18:05:08 +01:00
pokamest
b9a47f2f50 Merge pull request #1516 from amnezia-vpn/feature/openvpn-warning
feature: warning when importing openvpn configurations
2025-04-07 17:59:37 +01:00
vladimir.kuznetsov
27cb17c640 chore: clear warning text before extract 2025-04-07 23:35:24 +08:00
vladimir.kuznetsov
ef8fb89eb3 feature: warning when importing openvpn configurations 2025-04-07 23:30:11 +08:00
Nethius
f1b045f8a8 fixed selecting the default button on PageSetupWizardEasy (#1502) 2025-03-30 12:53:26 +07:00
123 changed files with 8554 additions and 3230 deletions

View File

@@ -10,7 +10,7 @@ env:
jobs:
Build-Linux-Ubuntu:
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
env:
QT_VERSION: 6.6.2
@@ -190,7 +190,7 @@ jobs:
- name: 'Install go'
uses: actions/setup-go@v5
with:
go-version: '1.22.1'
go-version: '1.24'
cache: false
- name: 'Setup gomobile'

View File

@@ -1,64 +1,41 @@
name: 'Upload a new version'
on:
push:
tags:
- '[0-9]+.[0-9]+.[0-9]+.[0-9]+'
workflow_dispatch:
inputs:
RELEASE_VERSION:
description: 'Release version (e.g. 1.2.3.4)'
required: true
type: string
jobs:
upload:
Upload-S3:
runs-on: ubuntu-latest
name: upload
steps:
- name: Checkout CMakeLists.txt
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.ref_name }}
ref: ${{ inputs.RELEASE_VERSION }}
sparse-checkout: |
CMakeLists.txt
deploy/deploy_s3.sh
sparse-checkout-cone-mode: false
- name: Verify git tag
run: |
GIT_TAG=${{ github.ref_name }}
TAG_NAME=${{ inputs.RELEASE_VERSION }}
CMAKE_TAG=$(grep 'project.*VERSION' CMakeLists.txt | sed -E 's/.* ([0-9]+.[0-9]+.[0-9]+.[0-9]+)$/\1/')
if [[ "$GIT_TAG" == "$CMAKE_TAG" ]]; then
echo "Git tag ($GIT_TAG) and version in CMakeLists.txt ($CMAKE_TAG) are the same. Continuing..."
if [[ "$TAG_NAME" == "$CMAKE_TAG" ]]; then
echo "Git tag ($TAG_NAME) matches CMakeLists.txt version ($CMAKE_TAG)."
else
echo "Git tag ($GIT_TAG) and version in CMakeLists.txt ($CMAKE_TAG) are not the same! Cancelling..."
echo "::error::Mismatch: Git tag ($TAG_NAME) != CMakeLists.txt version ($CMAKE_TAG). Exiting with error..."
exit 1
fi
- name: Download artifacts from the "${{ github.ref_name }}" tag
uses: robinraju/release-downloader@v1.8
- name: Setup Rclone
uses: AnimMouse/setup-rclone@v1
with:
tag: ${{ github.ref_name }}
fileName: "AmneziaVPN_(Linux_|)${{ github.ref_name }}*"
out-file-path: ${{ github.ref_name }}
rclone_config: ${{ secrets.RCLONE_CONFIG }}
- name: Upload beta version
uses: jakejarvis/s3-sync-action@master
if: contains(github.event.base_ref, 'dev')
with:
args: --include "AmneziaVPN*" --delete
env:
AWS_S3_BUCKET: updates
AWS_ACCESS_KEY_ID: ${{ secrets.CF_R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_R2_SECRET_ACCESS_KEY }}
AWS_S3_ENDPOINT: https://${{ vars.CF_ACCOUNT_ID }}.r2.cloudflarestorage.com
SOURCE_DIR: ${{ github.ref_name }}
DEST_DIR: beta/${{ github.ref_name }}
- name: Upload stable version
uses: jakejarvis/s3-sync-action@master
if: contains(github.event.base_ref, 'master')
with:
args: --include "AmneziaVPN*" --delete
env:
AWS_S3_BUCKET: updates
AWS_ACCESS_KEY_ID: ${{ secrets.CF_R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_R2_SECRET_ACCESS_KEY }}
AWS_S3_ENDPOINT: https://${{ vars.CF_ACCOUNT_ID }}.r2.cloudflarestorage.com
SOURCE_DIR: ${{ github.ref_name }}
DEST_DIR: stable/${{ github.ref_name }}
- name: Send dist to S3
run: bash deploy/deploy_s3.sh ${{ inputs.RELEASE_VERSION }}

View File

@@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
set(PROJECT AmneziaVPN)
project(${PROJECT} VERSION 4.8.5.0
project(${PROJECT} VERSION 4.8.6.0
DESCRIPTION "AmneziaVPN"
HOMEPAGE_URL "https://amnezia.org/"
)
@@ -11,7 +11,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d")
set(RELEASE_DATE "${CURRENT_DATE}")
set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
set(APP_ANDROID_VERSION_CODE 2082)
set(APP_ANDROID_VERSION_CODE 2083)
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
set(MZ_PLATFORM_NAME "linux")

View File

@@ -13,10 +13,10 @@
#include <QApplication>
#endif
#include "core/networkUtilities.h"
#include "containers/containers_defs.h"
#include "core/controllers/serverController.h"
#include "core/scripts_registry.h"
#include "core/server_defs.h"
#include "settings.h"
#include "utilities.h"
@@ -24,6 +24,7 @@
#include <openssl/rsa.h>
#include <openssl/x509.h>
OpenVpnConfigurator::OpenVpnConfigurator(std::shared_ptr<Settings> settings, const QSharedPointer<ServerController> &serverController,
QObject *parent)
: ConfiguratorBase(settings, serverController, parent)
@@ -122,12 +123,14 @@ QString OpenVpnConfigurator::processConfigWithLocalSettings(const QPair<QString,
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
// Prevent ipv6 leak
config.append("ifconfig-ipv6 fd15:53b6:dead::2/64 fd15:53b6:dead::1\n");
if (NetworkUtilities::checkIpv6Enabled()) {
config.append("ifconfig-ipv6 fd15:53b6:dead::2/64 fd15:53b6:dead::1\n");
}
#endif
config.append("block-ipv6\n");
} else if (m_settings->routeMode() == Settings::VpnOnlyForwardSites) {
// no redirect-gateway
// no redirect-gateway
} else if (m_settings->routeMode() == Settings::VpnAllExceptSites) {
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
config.append("\nredirect-gateway ipv6 !ipv4 bypass-dhcp\n");

View File

@@ -10,7 +10,8 @@ namespace apiDefs
AmneziaFreeV3,
AmneziaPremiumV1,
AmneziaPremiumV2,
SelfHosted
SelfHosted,
ExternalPremium
};
enum ConfigSource {
@@ -43,6 +44,13 @@ namespace apiDefs
constexpr QLatin1String maxDeviceCount("max_device_count");
constexpr QLatin1String subscriptionEndDate("subscription_end_date");
constexpr QLatin1String issuedConfigs("issued_configs");
constexpr QLatin1String supportInfo("support_info");
constexpr QLatin1String email("email");
constexpr QLatin1String billingEmail("billing_email");
constexpr QLatin1String website("website");
constexpr QLatin1String websiteName("website_name");
constexpr QLatin1String telegram("telegram");
}
const int requestTimeoutMsecs = 12 * 1000; // 12 secs

View File

@@ -32,15 +32,17 @@ apiDefs::ConfigType apiUtils::getConfigType(const QJsonObject &serverConfigObjec
constexpr QLatin1String servicePremium("amnezia-premium");
constexpr QLatin1String serviceFree("amnezia-free");
constexpr QLatin1String serviceExternalPremium("external-premium");
auto apiConfigObject = serverConfigObject.value(apiDefs::key::apiConfig).toObject();
auto stackType = apiConfigObject.value(apiDefs::key::stackType).toString();
auto serviceType = apiConfigObject.value(apiDefs::key::serviceType).toString();
if (serviceType == servicePremium || stackType == stackPremium) {
if (serviceType == servicePremium) {
return apiDefs::ConfigType::AmneziaPremiumV2;
} else if (serviceType == serviceFree || stackType == stackFree) {
} else if (serviceType == serviceFree) {
return apiDefs::ConfigType::AmneziaFreeV3;
} else if (serviceType == serviceExternalPremium) {
return apiDefs::ConfigType::ExternalPremium;
}
}
default: {
@@ -66,6 +68,7 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
return amnezia::ErrorCode::NoError;
} else if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError
|| reply->error() == QNetworkReply::NetworkError::TimeoutError) {
qDebug() << reply->error();
return amnezia::ErrorCode::ApiConfigTimeoutError;
} else {
QString err = reply->errorString();
@@ -85,3 +88,10 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
qDebug() << "something went wrong";
return amnezia::ErrorCode::InternalError;
}
bool apiUtils::isPremiumServer(const QJsonObject &serverConfigObject)
{
static const QSet<apiDefs::ConfigType> premiumTypes = { apiDefs::ConfigType::AmneziaPremiumV1, apiDefs::ConfigType::AmneziaPremiumV2,
apiDefs::ConfigType::ExternalPremium };
return premiumTypes.contains(getConfigType(serverConfigObject));
}

View File

@@ -13,6 +13,8 @@ namespace apiUtils
bool isSubscriptionExpired(const QString &subscriptionEndDate);
bool isPremiumServer(const QJsonObject &serverConfigObject);
apiDefs::ConfigType getConfigType(const QJsonObject &serverConfigObject);
apiDefs::ConfigSource getConfigSource(const QJsonObject &serverConfigObject);

View File

@@ -48,6 +48,9 @@ void CoreController::initModels()
m_sitesModel.reset(new SitesModel(m_settings, this));
m_engine->rootContext()->setContextProperty("SitesModel", m_sitesModel.get());
m_allowedDnsModel.reset(new AllowedDnsModel(m_settings, this));
m_engine->rootContext()->setContextProperty("AllowedDnsModel", m_allowedDnsModel.get());
m_appSplitTunnelingModel.reset(new AppSplitTunnelingModel(m_settings, this));
m_engine->rootContext()->setContextProperty("AppSplitTunnelingModel", m_appSplitTunnelingModel.get());
@@ -130,6 +133,9 @@ void CoreController::initControllers()
m_sitesController.reset(new SitesController(m_settings, m_vpnConnection, m_sitesModel));
m_engine->rootContext()->setContextProperty("SitesController", m_sitesController.get());
m_allowedDnsController.reset(new AllowedDnsController(m_settings, m_allowedDnsModel));
m_engine->rootContext()->setContextProperty("AllowedDnsController", m_allowedDnsController.get());
m_appSplitTunnelingController.reset(new AppSplitTunnelingController(m_settings, m_appSplitTunnelingModel));
m_engine->rootContext()->setContextProperty("AppSplitTunnelingController", m_appSplitTunnelingController.get());
@@ -214,6 +220,7 @@ void CoreController::initSignalHandlers()
initAutoConnectHandler();
initAmneziaDnsToggledHandler();
initPrepareConfigHandler();
initStrictKillSwitchHandler();
}
void CoreController::initNotificationHandler()
@@ -356,6 +363,12 @@ void CoreController::initPrepareConfigHandler()
});
}
void CoreController::initStrictKillSwitchHandler()
{
connect(m_settingsController.get(), &SettingsController::strictKillSwitchEnabledChanged,
m_vpnConnection.get(), &VpnConnection::onKillSwitchModeChanged);
}
QSharedPointer<PageController> CoreController::pageController() const
{
return m_pageController;

View File

@@ -8,6 +8,7 @@
#include "ui/controllers/api/apiConfigsController.h"
#include "ui/controllers/api/apiSettingsController.h"
#include "ui/controllers/appSplitTunnelingController.h"
#include "ui/controllers/allowedDnsController.h"
#include "ui/controllers/connectionController.h"
#include "ui/controllers/exportController.h"
#include "ui/controllers/focusController.h"
@@ -18,6 +19,7 @@
#include "ui/controllers/sitesController.h"
#include "ui/controllers/systemController.h"
#include "ui/models/allowed_dns_model.h"
#include "ui/models/containers_model.h"
#include "ui/models/languageModel.h"
#include "ui/models/protocols/cloakConfigModel.h"
@@ -80,6 +82,7 @@ private:
void initAutoConnectHandler();
void initAmneziaDnsToggledHandler();
void initPrepareConfigHandler();
void initStrictKillSwitchHandler();
QQmlApplicationEngine *m_engine {}; // TODO use parent child system here?
std::shared_ptr<Settings> m_settings;
@@ -102,6 +105,7 @@ private:
QScopedPointer<SitesController> m_sitesController;
QScopedPointer<SystemController> m_systemController;
QScopedPointer<AppSplitTunnelingController> m_appSplitTunnelingController;
QScopedPointer<AllowedDnsController> m_allowedDnsController;
QScopedPointer<ApiSettingsController> m_apiSettingsController;
QScopedPointer<ApiConfigsController> m_apiConfigsController;
@@ -112,6 +116,7 @@ private:
QSharedPointer<LanguageModel> m_languageModel;
QSharedPointer<ProtocolsModel> m_protocolsModel;
QSharedPointer<SitesModel> m_sitesModel;
QSharedPointer<AllowedDnsModel> m_allowedDnsModel;
QSharedPointer<AppSplitTunnelingModel> m_appSplitTunnelingModel;
QSharedPointer<ClientManagementModel> m_clientManagementModel;

View File

@@ -7,6 +7,7 @@
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkReply>
#include <QUrl>
#include "QBlockCipher.h"
#include "QRsa.h"
@@ -14,6 +15,11 @@
#include "amnezia_application.h"
#include "core/api/apiUtils.h"
#include "utilities.h"
#include "core/networkUtilities.h"
#ifdef AMNEZIA_DESKTOP
#include "core/ipcclient.h"
#endif
namespace
{
@@ -50,6 +56,17 @@ ErrorCode GatewayController::get(const QString &endpoint, QByteArray &responseBo
request.setUrl(QString(endpoint).arg(m_gatewayEndpoint));
// bypass killSwitch exceptions for API-gateway
#ifdef AMNEZIA_DESKTOP
{
QString host = QUrl(request.url()).host();
QString ip = NetworkUtilities::getIPAddress(host);
if (!ip.isEmpty()) {
IpcClient::Interface()->addKillSwitchAllowedRange(QStringList{ip});
}
}
#endif
QNetworkReply *reply;
reply = amnApp->networkManager()->get(request);
@@ -101,6 +118,17 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
request.setUrl(endpoint.arg(m_gatewayEndpoint));
// bypass killSwitch exceptions for API-gateway
#ifdef AMNEZIA_DESKTOP
{
QString host = QUrl(request.url()).host();
QString ip = NetworkUtilities::getIPAddress(host);
if (!ip.isEmpty()) {
IpcClient::Interface()->addKillSwitchAllowedRange(QStringList{ip});
}
}
#endif
QSimpleCrypto::QBlockCipher blockCipher;
QByteArray key = blockCipher.generatePrivateSalt(32);
QByteArray iv = blockCipher.generatePrivateSalt(32);
@@ -251,6 +279,9 @@ QStringList GatewayController::getProxyUrls()
}
return endpoints;
} else {
apiUtils::checkNetworkReplyErrors(sslErrors, reply);
qDebug() << "go to the next storage endpoint";
reply->deleteLater();
}
}
@@ -261,26 +292,29 @@ bool GatewayController::shouldBypassProxy(QNetworkReply *reply, const QByteArray
const QByteArray &iv, const QByteArray &salt)
{
if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError || reply->error() == QNetworkReply::NetworkError::TimeoutError) {
qDebug() << "Timeout occurred";
qDebug() << "timeout occurred";
qDebug() << reply->error();
return true;
} else if (responseBody.contains("html")) {
qDebug() << "The response contains an html tag";
qDebug() << "the response contains an html tag";
return true;
} else if (reply->error() == QNetworkReply::NetworkError::ContentNotFoundError) {
if (responseBody.contains(errorResponsePattern1) || responseBody.contains(errorResponsePattern2)
|| responseBody.contains(errorResponsePattern3)) {
return false;
} else {
qDebug() << reply->error();
return true;
}
} else if (reply->error() != QNetworkReply::NetworkError::NoError) {
qDebug() << reply->error();
return true;
} else if (checkEncryption) {
try {
QSimpleCrypto::QBlockCipher blockCipher;
static_cast<void>(blockCipher.decryptAesBlockCipher(responseBody, key, iv, "", salt));
} catch (...) {
qDebug() << "Failed to decrypt the data";
qDebug() << "failed to decrypt the data";
return true;
}
}
@@ -301,7 +335,7 @@ void GatewayController::bypassProxy(const QString &endpoint, QNetworkReply *repl
QByteArray responseBody;
for (const QString &proxyUrl : proxyUrls) {
qDebug() << "Go to the next endpoint";
qDebug() << "go to the next proxy endpoint";
reply->deleteLater(); // delete the previous reply
reply = requestFunction(endpoint.arg(proxyUrl));

View File

@@ -439,15 +439,22 @@ ErrorCode ServerController::buildContainerWorker(const ServerCredentials &creden
stdOut += data + "\n";
return ErrorCode::NoError;
};
auto cbReadStdErr = [&](const QString &data, libssh::Client &) {
stdOut += data + "\n";
return ErrorCode::NoError;
};
errorCode =
ErrorCode error =
runScript(credentials,
replaceVars(amnezia::scriptData(SharedScriptType::build_container), genVarsForScript(credentials, container, config)),
cbReadStdOut);
if (errorCode)
return errorCode;
cbReadStdOut, cbReadStdErr);
if (stdOut.contains("doesn't work on cgroups v2"))
return ErrorCode::ServerDockerOnCgroupsV2;
if (stdOut.contains("cgroup mountpoint does not exist"))
return ErrorCode::ServerCgroupMountpoint;
return errorCode;
return error;
}
ErrorCode ServerController::runContainerWorker(const ServerCredentials &credentials, DockerContainer container, QJsonObject &config)

View File

@@ -58,6 +58,8 @@ namespace amnezia
ServerUserDirectoryNotAccessible = 208,
ServerUserNotAllowedInSudoers = 209,
ServerUserPasswordRequired = 210,
ServerDockerOnCgroupsV2 = 211,
ServerCgroupMountpoint = 212,
// Ssh connection errors
SshRequestDeniedError = 300,

View File

@@ -26,6 +26,8 @@ QString errorString(ErrorCode code) {
case(ErrorCode::ServerUserDirectoryNotAccessible): errorMessage = QObject::tr("The server user's home directory is not accessible"); break;
case(ErrorCode::ServerUserNotAllowedInSudoers): errorMessage = QObject::tr("Action not allowed in sudoers"); break;
case(ErrorCode::ServerUserPasswordRequired): errorMessage = QObject::tr("The user's password is required"); break;
case(ErrorCode::ServerDockerOnCgroupsV2): errorMessage = QObject::tr("Docker error: runc doesn't work on cgroups v2"); break;
case(ErrorCode::ServerCgroupMountpoint): errorMessage = QObject::tr("Server error: cgroup mountpoint does not exist"); break;
// Libssh errors
case(ErrorCode::SshRequestDeniedError): errorMessage = QObject::tr("SSH request was denied"); break;

View File

@@ -12,6 +12,7 @@
#include <winsock.h>
#include <QNetworkInterface>
#include "qendian.h"
#include <QSettings>
#endif
#ifdef Q_OS_LINUX
#include <arpa/inet.h>
@@ -185,6 +186,17 @@ int NetworkUtilities::AdapterIndexTo(const QHostAddress& dst) {
return 0;
}
bool NetworkUtilities::checkIpv6Enabled() {
#ifdef Q_OS_WIN
QSettings RegHLM("HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Services\\Tcpip6\\Parameters",
QSettings::NativeFormat);
int ret = RegHLM.value("DisabledComponents", 0).toInt();
qDebug() << "Check for Windows disabled IPv6 return " << ret;
return (ret != 255);
#endif
return true;
}
#ifdef Q_OS_WIN
DWORD GetAdaptersAddressesWrapper(const ULONG Family,
const ULONG Flags,

View File

@@ -16,6 +16,7 @@ public:
static QString getStringBetween(const QString &s, const QString &a, const QString &b);
static bool checkIPv4Format(const QString &ip);
static bool checkIpSubnetFormat(const QString &ip);
static bool checkIpv6Enabled();
static QString getGatewayAndIface();
// Returns the Interface Index that could Route to dst
static int AdapterIndexTo(const QHostAddress& dst);
@@ -29,7 +30,6 @@ public:
static QString netMaskFromIpWithSubnet(const QString ip);
static QString ipAddressFromIpWithSubnet(const QString ip);
static QStringList summarizeRoutes(const QStringList &ips, const QString cidr);
};

View File

@@ -371,6 +371,9 @@ bool Daemon::parseConfig(const QJsonObject& obj, InterfaceConfig& config) {
if (!parseStringList(obj, "vpnDisabledApps", config.m_vpnDisabledApps)) {
return false;
}
if (!parseStringList(obj, "allowedDnsServers", config.m_allowedDnsServers)) {
return false;
}
config.m_killSwitchEnabled = QVariant(obj.value("killSwitchOption").toString()).toBool();

View File

@@ -48,6 +48,13 @@ QJsonObject InterfaceConfig::toJson() const {
}
json.insert("excludedAddresses", jsExcludedAddresses);
QJsonArray jsAllowedDnsServers;
for (const QString& i : m_allowedDnsServers) {
jsAllowedDnsServers.append(QJsonValue(i));
}
json.insert("allowedDnsServers", jsAllowedDnsServers);
QJsonArray disabledApps;
for (const QString& i : m_vpnDisabledApps) {
disabledApps.append(QJsonValue(i));

View File

@@ -6,6 +6,7 @@
#define INTERFACECONFIG_H
#include <QList>
#include <QMap>
#include <QString>
#include "ipaddress.h"
@@ -37,6 +38,7 @@ class InterfaceConfig {
QList<IPAddress> m_allowedIPAddressRanges;
QStringList m_excludedAddresses;
QStringList m_vpnDisabledApps;
QStringList m_allowedDnsServers;
bool m_killSwitchEnabled;
#if defined(MZ_ANDROID) || defined(MZ_IOS)
QString m_installationId;

View File

@@ -123,6 +123,7 @@ void LocalSocketController::activate(const QJsonObject &rawConfig) {
int appSplitTunnelType = rawConfig.value(amnezia::config_key::appSplitTunnelType).toInt();
QJsonArray splitTunnelApps = rawConfig.value(amnezia::config_key::splitTunnelApps).toArray();
QJsonArray allowedDns = rawConfig.value(amnezia::config_key::allowedDnsServers).toArray();
QJsonObject wgConfig = rawConfig.value(protocolName + "_config_data").toObject();
@@ -226,6 +227,8 @@ void LocalSocketController::activate(const QJsonObject &rawConfig) {
json.insert("vpnDisabledApps", splitTunnelApps);
json.insert("allowedDnsServers", allowedDns);
json.insert(amnezia::config_key::killSwitchOption, rawConfig.value(amnezia::config_key::killSwitchOption));
if (protocolName == amnezia::config_key::awg) {

View File

@@ -31,7 +31,9 @@ IPUtilsLinux::~IPUtilsLinux() {
}
bool IPUtilsLinux::addInterfaceIPs(const InterfaceConfig& config) {
return addIP4AddressToDevice(config) && addIP6AddressToDevice(config);
bool ret = addIP4AddressToDevice(config);
addIP6AddressToDevice(config);
return ret;
}
bool IPUtilsLinux::setMTUAndUp(const InterfaceConfig& config) {

View File

@@ -455,9 +455,6 @@ void LinuxFirewall::updateDNSServers(const QStringList& servers)
void LinuxFirewall::updateAllowNets(const QStringList& servers)
{
static QStringList existingServers {};
existingServers = 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));

View File

@@ -17,6 +17,8 @@
#include "leakdetector.h"
#include "logger.h"
#include "killswitch.h"
constexpr const int WG_TUN_PROC_TIMEOUT = 5000;
constexpr const char* WG_RUNTIME_DIR = "/var/run/amneziawg";
@@ -182,7 +184,7 @@ bool WireguardUtilsLinux::deleteInterface() {
QFile::remove(wgRuntimeDir.filePath(QString(WG_INTERFACE) + ".name"));
// double-check + ensure our firewall is installed and enabled
LinuxFirewall::uninstall();
KillSwitch::instance()->disableKillSwitch();
return true;
}

View File

@@ -16,6 +16,8 @@
#include "leakdetector.h"
#include "logger.h"
#include "killswitch.h"
constexpr const int WG_TUN_PROC_TIMEOUT = 5000;
constexpr const char* WG_RUNTIME_DIR = "/var/run/amneziawg";
@@ -180,7 +182,7 @@ bool WireguardUtilsMacos::deleteInterface() {
QFile::remove(wgRuntimeDir.filePath(QString(WG_INTERFACE) + ".name"));
// double-check + ensure our firewall is installed and enabled
MacOSFirewall::uninstall();
KillSwitch::instance()->disableKillSwitch();
return true;
}

View File

@@ -29,6 +29,8 @@
#include "logger.h"
#include "platforms/windows/windowsutils.h"
#include "killswitch.h"
#define IPV6_ADDRESS_SIZE 16
// ID for the Firewall Sublayer
@@ -180,16 +182,29 @@ bool WindowsFirewall::enableInterface(int vpnAdapterIndex) {
} \
}
logger.info() << "Enabling firewall Using Adapter:" << vpnAdapterIndex;
logger.info() << "Enabling Killswitch Using Adapter:" << vpnAdapterIndex;
if (vpnAdapterIndex < 0)
{
IPAddress allv4("0.0.0.0/0");
if (!blockTrafficTo(allv4, MED_WEIGHT,
"Block Internet", "killswitch")) {
return false;
}
IPAddress allv6("::/0");
if (!blockTrafficTo(allv6, MED_WEIGHT,
"Block Internet", "killswitch")) {
return false;
}
} else
FW_OK(allowTrafficOfAdapter(vpnAdapterIndex, MED_WEIGHT,
"Allow usage of VPN Adapter"));
"Allow usage of VPN Adapter"));
FW_OK(allowDHCPTraffic(MED_WEIGHT, "Allow DHCP Traffic"));
FW_OK(allowHyperVTraffic(MED_WEIGHT, "Allow Hyper-V Traffic"));
FW_OK(allowHyperVTraffic(MAX_WEIGHT, "Allow Hyper-V Traffic"));
FW_OK(allowTrafficForAppOnAll(getCurrentPath(), MAX_WEIGHT,
"Allow all for AmneziaVPN.exe"));
FW_OK(blockTrafficOnPort(53, MED_WEIGHT, "Block all DNS"));
FW_OK(
allowLoopbackTraffic(MED_WEIGHT, "Allow Loopback traffic on device %1"));
FW_OK(allowLoopbackTraffic(MED_WEIGHT,
"Allow Loopback traffic on device %1"));
logger.debug() << "Killswitch on! Rules:" << m_activeRules.length();
return true;
@@ -226,6 +241,37 @@ bool WindowsFirewall::enableLanBypass(const QList<IPAddress>& ranges) {
return true;
}
// Allow unprotected traffic sent to the following address ranges.
bool WindowsFirewall::allowTrafficRange(const QStringList& ranges) {
// Start the firewall transaction
auto result = FwpmTransactionBegin(m_sessionHandle, NULL);
if (result != ERROR_SUCCESS) {
disableKillSwitch();
return false;
}
auto cleanup = qScopeGuard([&] {
FwpmTransactionAbort0(m_sessionHandle);
disableKillSwitch();
});
for (const QString& addr : ranges) {
logger.debug() << "Allow killswitch exclude: " << addr;
if (!allowTrafficTo(QHostAddress(addr), LOW_WEIGHT + 1, "Allow killswitch bypass traffic")) {
return false;
}
}
result = FwpmTransactionCommit0(m_sessionHandle);
if (result != ERROR_SUCCESS) {
logger.error() << "FwpmTransactionCommit0 failed with error:" << result;
return false;
}
cleanup.dismiss();
return true;
}
bool WindowsFirewall::enablePeerTraffic(const InterfaceConfig& config) {
// Start the firewall transaction
auto result = FwpmTransactionBegin(m_sessionHandle, NULL);
@@ -262,12 +308,20 @@ bool WindowsFirewall::enablePeerTraffic(const InterfaceConfig& config) {
}
}
for (const QString& dns : config.m_allowedDnsServers) {
logger.debug() << "Allow DNS: " << dns;
if (!allowTrafficTo(QHostAddress(dns), 53, HIGH_WEIGHT,
"Allow DNS-Server", config.m_serverPublicKey)) {
return false;
}
}
if (!config.m_excludedAddresses.empty()) {
for (const QString& i : config.m_excludedAddresses) {
logger.debug() << "excludedAddresses range: " << i;
if (!allowTrafficTo(i, HIGH_WEIGHT,
"Allow Ecxlude route", config.m_serverPublicKey)) {
"Allow Ecxlude route", config.m_serverPublicKey)) {
return false;
}
}
@@ -313,37 +367,41 @@ bool WindowsFirewall::disablePeerTraffic(const QString& pubkey) {
}
bool WindowsFirewall::disableKillSwitch() {
auto result = FwpmTransactionBegin(m_sessionHandle, NULL);
auto cleanup = qScopeGuard([&] {
return KillSwitch::instance()->disableKillSwitch();
}
bool WindowsFirewall::allowAllTraffic() {
auto result = FwpmTransactionBegin(m_sessionHandle, NULL);
auto cleanup = qScopeGuard([&] {
if (result != ERROR_SUCCESS) {
FwpmTransactionAbort0(m_sessionHandle);
}
});
if (result != ERROR_SUCCESS) {
FwpmTransactionAbort0(m_sessionHandle);
logger.error() << "FwpmTransactionBegin0 failed. Return value:.\n"
<< result;
return false;
}
});
if (result != ERROR_SUCCESS) {
logger.error() << "FwpmTransactionBegin0 failed. Return value:.\n"
<< result;
return false;
}
for (const auto& filterID : m_peerRules.values()) {
FwpmFilterDeleteById0(m_sessionHandle, filterID);
}
for (const auto& filterID : m_peerRules.values()) {
FwpmFilterDeleteById0(m_sessionHandle, filterID);
}
for (const auto& filterID : qAsConst(m_activeRules)) {
FwpmFilterDeleteById0(m_sessionHandle, filterID);
}
for (const auto& filterID : qAsConst(m_activeRules)) {
FwpmFilterDeleteById0(m_sessionHandle, filterID);
}
// Commit!
result = FwpmTransactionCommit0(m_sessionHandle);
if (result != ERROR_SUCCESS) {
logger.error() << "FwpmTransactionCommit0 failed. Return value:.\n"
<< result;
return false;
}
m_peerRules.clear();
m_activeRules.clear();
logger.debug() << "Firewall Disabled!";
return true;
// Commit!
result = FwpmTransactionCommit0(m_sessionHandle);
if (result != ERROR_SUCCESS) {
logger.error() << "FwpmTransactionCommit0 failed. Return value:.\n"
<< result;
return false;
}
m_peerRules.clear();
m_activeRules.clear();
logger.debug() << "Firewall Disabled!";
return true;
}
bool WindowsFirewall::allowTrafficForAppOnAll(const QString& exePath,

View File

@@ -43,6 +43,8 @@ class WindowsFirewall final : public QObject {
bool enablePeerTraffic(const InterfaceConfig& config);
bool disablePeerTraffic(const QString& pubkey);
bool disableKillSwitch();
bool allowAllTraffic();
bool allowTrafficRange(const QStringList& ranges);
private:
static bool initSublayer();

View File

@@ -14,8 +14,6 @@
#include "leakdetector.h"
#include "logger.h"
#include "platforms/windows/windowscommons.h"
#include "windowsdaemon.h"
#include "windowsfirewall.h"
#pragma comment(lib, "iphlpapi.lib")
@@ -269,6 +267,13 @@ bool WireguardUtilsWindows::updateRoutePrefix(const IPAddress& prefix) {
if (result == ERROR_OBJECT_ALREADY_EXISTS) {
return true;
}
// Case for ipv6 route with disabled ipv6
if (prefix.address().protocol() == QAbstractSocket::IPv6Protocol
&& result == ERROR_NOT_FOUND) {
return true;
}
if (result != NO_ERROR) {
logger.error() << "Failed to create route to"
<< prefix.toString()

View File

@@ -171,6 +171,11 @@ ErrorCode OpenVpnProtocol::start()
return lastError();
}
#if defined(Q_OS_LINUX) || defined(Q_OS_MACOS)
IpcClient::Interface()->addKillSwitchAllowedRange(QStringList(NetworkUtilities::getIPAddress(
m_configData.value(amnezia::config_key::hostName).toString())));
#endif
// Detect default gateway
#ifdef Q_OS_MAC
QProcess p;

View File

@@ -95,6 +95,8 @@ namespace amnezia
constexpr char splitTunnelApps[] = "splitTunnelApps";
constexpr char appSplitTunnelType[] = "appSplitTunnelType";
constexpr char allowedDnsServers[] = "allowedDnsServers";
constexpr char killSwitchOption[] = "killSwitchOption";
constexpr char crc[] = "crc";

View File

@@ -129,6 +129,7 @@
<file>ui/qml/Components/SettingsContainersListView.qml</file>
<file>ui/qml/Components/ShareConnectionDrawer.qml</file>
<file>ui/qml/Components/TransportProtoSelector.qml</file>
<file>ui/qml/Components/AddSitePanel.qml</file>
<file>ui/qml/Config/GlobalConfig.qml</file>
<file>ui/qml/Config/qmldir</file>
<file>ui/qml/Controls2/BackButtonType.qml</file>
@@ -143,7 +144,9 @@
<file>ui/qml/Controls2/DropDownType.qml</file>
<file>ui/qml/Controls2/FlickableType.qml</file>
<file>ui/qml/Controls2/Header2Type.qml</file>
<file>ui/qml/Controls2/HeaderType.qml</file>
<file>ui/qml/Controls2/BaseHeaderType.qml</file>
<file>ui/qml/Controls2/HeaderTypeWithButton.qml</file>
<file>ui/qml/Controls2/HeaderTypeWithSwitcher.qml</file>
<file>ui/qml/Controls2/HorizontalRadioButton.qml</file>
<file>ui/qml/Controls2/ImageButtonType.qml</file>
<file>ui/qml/Controls2/LabelWithButtonType.qml</file>
@@ -199,6 +202,8 @@
<file>ui/qml/Pages2/PageSettingsBackup.qml</file>
<file>ui/qml/Pages2/PageSettingsConnection.qml</file>
<file>ui/qml/Pages2/PageSettingsDns.qml</file>
<file>ui/qml/Pages2/PageSettingsKillSwitch.qml</file>
<file>ui/qml/Pages2/PageSettingsKillSwitchExceptions.qml</file>
<file>ui/qml/Pages2/PageSettingsLogging.qml</file>
<file>ui/qml/Pages2/PageSettingsServerData.qml</file>
<file>ui/qml/Pages2/PageSettingsServerInfo.qml</file>

View File

@@ -1,7 +1,7 @@
#include "secure_qsettings.h"
#include "QAead.h"
#include "QBlockCipher.h"
#include "../client/3rd/QSimpleCrypto/src/include/QAead.h"
#include "../client/3rd/QSimpleCrypto/src/include/QBlockCipher.h"
#include "utilities.h"
#include <QDataStream>
#include <QDebug>

View File

@@ -6,7 +6,7 @@
#include <QObject>
#include <QSettings>
#include "keychain.h"
#include "../client/3rd/qtkeychain/qtkeychain/keychain.h"
class SecureQSettings : public QObject
{

View File

@@ -443,6 +443,16 @@ void Settings::setKillSwitchEnabled(bool enabled)
setValue("Conf/killSwitchEnabled", enabled);
}
bool Settings::isStrictKillSwitchEnabled() const
{
return value("Conf/strictKillSwitchEnabled", false).toBool();
}
void Settings::setStrictKillSwitchEnabled(bool enabled)
{
setValue("Conf/strictKillSwitchEnabled", enabled);
}
QString Settings::getInstallationUuid(const bool needCreate)
{
auto uuid = value("Conf/installationUuid", "").toString();
@@ -548,3 +558,13 @@ void Settings::disableHomeAdLabel()
{
setValue("Conf/homeAdLabelVisible", false);
}
QStringList Settings::allowedDnsServers() const
{
return value("Conf/allowedDnsServers").toStringList();
}
void Settings::setAllowedDnsServers(const QStringList &servers)
{
setValue("Conf/allowedDnsServers", servers);
}

View File

@@ -213,6 +213,10 @@ public:
bool isKillSwitchEnabled() const;
void setKillSwitchEnabled(bool enabled);
bool isStrictKillSwitchEnabled() const;
void setStrictKillSwitchEnabled(bool enabled);
QString getInstallationUuid(const bool needCreate);
void resetGatewayEndpoint();
@@ -225,6 +229,9 @@ public:
bool isHomeAdLabelVisible();
void disableHomeAdLabel();
QStringList allowedDnsServers() const;
void setAllowedDnsServers(const QStringList &servers);
signals:
void saveLogsChanged(bool enabled);
void screenshotsEnabledChanged(bool enabled);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -4,9 +4,13 @@
<context>
<name>AdLabel</name>
<message>
<location filename="../ui/qml/Components/AdLabel.qml" line="57"/>
<source>Amnezia Premium - for access to any website</source>
<translation>Amnezia Premium - для доступа к любым сайтам</translation>
<translation type="vanished">Amnezia Premium - для доступа к любым сайтам</translation>
</message>
<message>
<location filename="../ui/qml/Components/AdLabel.qml" line="57"/>
<source>Amnezia Premium - for access to all websites and online resources</source>
<translation>Amnezia Premium - доступ ко всем сайтам и онлайн ресурсам</translation>
</message>
</context>
<context>
@@ -56,12 +60,12 @@
<translation>%1 успешно установлен.</translation>
</message>
<message>
<location filename="../ui/controllers/api/apiConfigsController.cpp" line="258"/>
<location filename="../ui/controllers/api/apiConfigsController.cpp" line="257"/>
<source>API config reloaded</source>
<translation>Конфигурация API перезагружена</translation>
</message>
<message>
<location filename="../ui/controllers/api/apiConfigsController.cpp" line="262"/>
<location filename="../ui/controllers/api/apiConfigsController.cpp" line="261"/>
<source>Successfully changed the country of connection to %1</source>
<translation>Страна подключения изменена на %1</translation>
</message>
@@ -90,9 +94,8 @@
<translation type="vanished">Amnezia Free - это бесплатный VPN для обхода блокировок в странах с высоким уровнем интернет-цензуры</translation>
</message>
<message>
<location filename="../ui/models/api/apiServicesModel.cpp" line="68"/>
<source>Amnezia Premium is VPN for comfortable work, downloading large files and watching videos in 8K resolution. Works for any sites with no restrictions. Speed up to %1 MBit/s. Unlimited traffic.</source>
<translation>Amnezia Premium VPN для комфортной работы, скачивания больших файлов и просмотра видео в высоком разрешении. Скорость до %1 Мбит/с. Безлимитный трафик.</translation>
<translation type="vanished">Amnezia Premium VPN для комфортной работы, скачивания больших файлов и просмотра видео в высоком разрешении. Скорость до %1 Мбит/с. Безлимитный трафик.</translation>
</message>
<message>
<location filename="../ui/models/api/apiServicesModel.cpp" line="72"/>
@@ -101,9 +104,18 @@
<translation>AmneziaFree предоставляет бесплатный неограниченный доступ к базовому набору сайтов и приложений, таким как Facebook, Instagram, Twitter (X), Discord, Telegram и другим. YouTube не включен в бесплатный тариф.</translation>
</message>
<message>
<location filename="../ui/models/api/apiServicesModel.cpp" line="82"/>
<source>Amnezia Premium is VPN for comfortable work, downloading large files and watching videos in 8K resolution. Works for any sites with no restrictions.</source>
<translation>Amnezia Premium VPN для комфортной работы, скачивания больших файлов и просмотра видео в высоком разрешении. Работает для любых сайтов без ограничений.</translation>
<translation type="vanished">Amnezia Premium VPN для комфортной работы, скачивания больших файлов и просмотра видео в высоком разрешении. Работает для любых сайтов без ограничений.</translation>
</message>
<message>
<location filename="../ui/models/api/apiServicesModel.cpp" line="68"/>
<source>Amnezia Premium is classic VPN for seamless work, downloading large files, and watching videos. Access all websites and online resources. Speeds up to %1 Mbps.</source>
<translation>Amnezia Premium - это классический VPN для комфортной работы, загрузки больших файлов и просмотра видео. Доступ ко всем сайтам и онлайн ресурсам. Скорость - до %1 Мбит/с.</translation>
</message>
<message>
<location filename="../ui/models/api/apiServicesModel.cpp" line="82"/>
<source>Amnezia Premium is classic VPN for for seamless work, downloading large files, and watching videos. Access all websites and online resources.</source>
<translation>Amnezia Premium - это классический VPN для комфортной работы, загрузки больших файлов и просмотра видео. Доступ ко всем сайтам и онлайн ресурсам.</translation>
</message>
<message>
<location filename="../ui/models/api/apiServicesModel.cpp" line="97"/>
@@ -340,14 +352,23 @@ Can&apos;t be disabled for current server</source>
<translation type="vanished">Неверный файл конфигурации</translation>
</message>
<message>
<location filename="../ui/controllers/importController.cpp" line="651"/>
<location filename="../ui/controllers/importController.cpp" line="650"/>
<source>Scanned %1 of %2.</source>
<translation>Отсканировано %1 из %2.</translation>
</message>
<message>
<location filename="../ui/controllers/importController.cpp" line="686"/>
<location filename="../ui/controllers/importController.cpp" line="685"/>
<source>This configuration contains an OpenVPN setup. OpenVPN configurations can include malicious scripts, so only add it if you fully trust the provider of this config. </source>
<translation>Эта конфигурация содержит настройки OpenVPN. Конфигурации OpenVPN могут содержать вредоносные скрипты, поэтому добавляйте их только в том случае, если полностью доверяете источнику этого файла. </translation>
</message>
<message>
<location filename="../ui/controllers/importController.cpp" line="689"/>
<source>&lt;br&gt;In the imported configuration, potentially dangerous lines were found:</source>
<translation>&lt;br&gt;В импортированной конфигурации обнаружены потенциально опасные строки:</translation>
</message>
<message>
<source>In the imported configuration, potentially dangerous lines were found:</source>
<translation>В импортированной конфигурации были обнаружены потенциально опасные строки:</translation>
<translation type="vanished">В импортированной конфигурации были обнаружены потенциально опасные строки:</translation>
</message>
</context>
<context>
@@ -517,12 +538,12 @@ Already installed containers were found on the server. All installed containers
<message>
<location filename="../ui/qml/Pages2/PageDevMenu.qml" line="68"/>
<source>Gateway endpoint</source>
<translation type="unfinished"></translation>
<translation>Gateway endpoint</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageDevMenu.qml" line="97"/>
<source>Dev gateway environment</source>
<translation type="unfinished"></translation>
<translation>Dev gateway environment</translation>
</message>
</context>
<context>
@@ -652,47 +673,47 @@ Already installed containers were found on the server. All installed containers
<message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="146"/>
<source>Jc - Junk packet count</source>
<translation type="unfinished"></translation>
<translation>Jc - Junk packet count</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="168"/>
<source>Jmin - Junk packet minimum size</source>
<translation type="unfinished"></translation>
<translation>Jmin - Junk packet minimum size</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="186"/>
<source>Jmax - Junk packet maximum size</source>
<translation type="unfinished"></translation>
<translation>Jmax - Junk packet maximum size</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="204"/>
<source>S1 - Init packet junk size</source>
<translation type="unfinished"></translation>
<translation>S1 - Init packet junk size</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="228"/>
<source>S2 - Response packet junk size</source>
<translation type="unfinished"></translation>
<translation>S2 - Response packet junk size</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="252"/>
<source>H1 - Init packet magic header</source>
<translation type="unfinished"></translation>
<translation>H1 - Init packet magic header</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="270"/>
<source>H2 - Response packet magic header</source>
<translation type="unfinished"></translation>
<translation>H2 - Response packet magic header</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="288"/>
<source>H4 - Transport packet magic header</source>
<translation type="unfinished"></translation>
<translation>H4 - Transport packet magic header</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="306"/>
<source>H3 - Underload packet magic header</source>
<translation type="unfinished"></translation>
<translation>H3 - Underload packet magic header</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="354"/>
@@ -1461,7 +1482,7 @@ Already installed containers were found on the server. All installed containers
<message>
<location filename="../ui/qml/Pages2/PageSettings.qml" line="123"/>
<source>Dev console</source>
<translation type="unfinished"></translation>
<translation>Dev console</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettings.qml" line="142"/>
@@ -1589,8 +1610,12 @@ Already installed containers were found on the server. All installed containers
<context>
<name>PageSettingsApiDevices</name>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiDevices.qml" line="45"/>
<source>Active devices</source>
<translation type="vanished">Активные устройства</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiDevices.qml" line="45"/>
<source>Active Devices</source>
<translation>Активные устройства</translation>
</message>
<message>
@@ -1742,9 +1767,13 @@ Already installed containers were found on the server. All installed containers
<translation>Сохранить конфигурацию AmneziaVPN</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiNativeConfigs.qml" line="48"/>
<source>Configuration files</source>
<translation>Файл конфигурации</translation>
<translation type="vanished">Файл конфигурации</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiNativeConfigs.qml" line="48"/>
<source>Configuration Files</source>
<translation>Файлы конфигурации</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiNativeConfigs.qml" line="49"/>
@@ -1832,9 +1861,8 @@ Already installed containers were found on the server. All installed containers
<translation type="vanished">Период работы</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="37"/>
<source>Valid until</source>
<translation>Действует до</translation>
<translation type="vanished">Действует до</translation>
</message>
<message>
<source>Speed</source>
@@ -1845,14 +1873,12 @@ Already installed containers were found on the server. All installed containers
<translation type="vanished">Скопировано</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="29"/>
<source>Subscription status</source>
<translation>Статус подписки</translation>
<translation type="vanished">Статус подписки</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="45"/>
<source>Active connections</source>
<translation>Активные соединения</translation>
<translation type="vanished">Активные соединения</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="171"/>
@@ -1860,9 +1886,8 @@ Already installed containers were found on the server. All installed containers
<translation>Сетевые адреса одного или нескольких серверов были обновлены. Пожалуйста, удалите старые конфигурацию и загрузите новые файлы</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="186"/>
<source>Subscription key</source>
<translation>Ключ для подключения</translation>
<translation type="vanished">Ключ для подключения</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="190"/>
@@ -1870,9 +1895,8 @@ Already installed containers were found on the server. All installed containers
<translation>Ключ подписки Amnezia Premium</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="194"/>
<source>Save VPN key to file</source>
<translation>Сохранить VPN-ключ в файле</translation>
<translation type="vanished">Сохранить VPN-ключ в файле</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="195"/>
@@ -1880,9 +1904,8 @@ Already installed containers were found on the server. All installed containers
<translation>Скопировать VPN ключ</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="216"/>
<source>Configuration files</source>
<translation>Файл конфигурации</translation>
<translation type="vanished">Файл конфигурации</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="218"/>
@@ -1890,8 +1913,42 @@ Already installed containers were found on the server. All installed containers
<translation>Управление файлами конфигурации</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="236"/>
<source>Active devices</source>
<translation type="vanished">Активные устройства</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="29"/>
<source>Subscription Status</source>
<translation>Статус подписки</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="37"/>
<source>Valid Until</source>
<translation>Действительна до</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="45"/>
<source>Active Connections</source>
<translation>Активные соединения</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="186"/>
<source>Subscription Key</source>
<translation>Ключ для подключения</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="194"/>
<source>Save VPN key as a file</source>
<translation>Сохранить VPN-ключ в файл</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="216"/>
<source>Configuration Files</source>
<translation>Файлы конфигурации</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiServerInfo.qml" line="236"/>
<source>Active Devices</source>
<translation>Активные устройства</translation>
</message>
<message>
@@ -1982,8 +2039,12 @@ Already installed containers were found on the server. All installed containers
<translation></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiSupport.qml" line="30"/>
<source>Email Support</source>
<translation type="vanished">Email</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiSupport.qml" line="30"/>
<source>Email</source>
<translation>Email</translation>
</message>
<message>
@@ -2332,8 +2393,12 @@ Already installed containers were found on the server. All installed containers
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsConnection.qml" line="143"/>
<source>Cannot change KillSwitch settings during active connection</source>
<translation>Невозможно изменить настройки KillSwitch во время активного подключения</translation>
</message>
<message>
<source>Cannot change killSwitch settings during active connection</source>
<translation>Невозможно изменить настройки аварийного выключателя во время активного соединения</translation>
<translation type="vanished">Невозможно изменить настройки аварийного выключателя во время активного соединения</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsConnection.qml" line="86"/>
@@ -2496,12 +2561,12 @@ Already installed containers were found on the server. All installed containers
<message>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="175"/>
<source>Client logs</source>
<translation type="unfinished"></translation>
<translation>Логи приложения</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="176"/>
<source>AmneziaVPN logs</source>
<translation type="unfinished"></translation>
<translation>AmneziaVPN logs</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="142"/>
@@ -2516,12 +2581,12 @@ Already installed containers were found on the server. All installed containers
<message>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="204"/>
<source>Service logs</source>
<translation type="unfinished"></translation>
<translation>Логи службы</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="205"/>
<source>AmneziaVPN-service logs</source>
<translation type="unfinished"></translation>
<translation>AmneziaVPN-service logs</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="78"/>
@@ -3006,7 +3071,7 @@ It&apos;s okay as long as it&apos;s from someone you trust.</source>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="109"/>
<source>Support tag</source>
<translation type="unfinished"></translation>
<translation>Support tag</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="120"/>
@@ -3594,7 +3659,7 @@ and will not be shared or disclosed to the Amnezia or any third parties</source>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="726"/>
<source>Allowed IPs: %1</source>
<translation type="unfinished"></translation>
<translation>Разрешенные подсети: %1</translation>
</message>
<message>
<source>Creation date: </source>
@@ -4046,38 +4111,58 @@ and will not be shared or disclosed to the Amnezia or any third parties</source>
<source>Server error: Package manager error</source>
<translation>Ошибка сервера: Ошибка менеджера пакетов</translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="25"/>
<source>The sudo package is not pre-installed on the server</source>
<translation>Пакет sudo не установлен на сервере по умолчанию</translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="26"/>
<source>The server user&apos;s home directory is not accessible</source>
<translation>Домашний каталог пользователя сервера недоступен</translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="27"/>
<source>Action not allowed in sudoers</source>
<translation>Действие не разрешено в sudoers</translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="28"/>
<source>The user&apos;s password is required</source>
<translation>Требуется пароль пользователя</translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="31"/>
<source>SSH request was denied</source>
<translation>SSH-запрос был отклонён</translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="28"/>
<location filename="../core/errorstrings.cpp" line="32"/>
<source>SSH request was interrupted</source>
<translation>SSH-запрос был прерван</translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="29"/>
<location filename="../core/errorstrings.cpp" line="33"/>
<source>SSH internal error</source>
<translation>Внутренняя ошибка SSH</translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="30"/>
<location filename="../core/errorstrings.cpp" line="34"/>
<source>Invalid private key or invalid passphrase entered</source>
<translation>Введен неверный закрытый ключ или неверная парольная фраза</translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="31"/>
<location filename="../core/errorstrings.cpp" line="35"/>
<source>The selected private key format is not supported, use openssh ED25519 key types or PEM key types</source>
<translation>Выбранный формат закрытого ключа не поддерживается, используйте типы ключей openssh ED25519 или PEM</translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="32"/>
<location filename="../core/errorstrings.cpp" line="36"/>
<source>Timeout connecting to server</source>
<translation>Тайм-аут подключения к серверу</translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="35"/>
<location filename="../core/errorstrings.cpp" line="39"/>
<source>SCP error: Generic failure</source>
<translation>Ошибка SCP: общий сбой</translation>
</message>
@@ -4134,23 +4219,23 @@ and will not be shared or disclosed to the Amnezia or any third parties</source>
<translation type="vanished">Sftp error: No media was in remote drive</translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="53"/>
<location filename="../core/errorstrings.cpp" line="57"/>
<source>The config does not contain any containers and credentials for connecting to the server</source>
<translation>Конфигурация не содержит каких-либо контейнеров и учетных данных для подключения к серверу</translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="61"/>
<location filename="../core/errorstrings.cpp" line="70"/>
<location filename="../core/errorstrings.cpp" line="65"/>
<location filename="../core/errorstrings.cpp" line="74"/>
<source>Error when retrieving configuration from API</source>
<translation>Ошибка при получении конфигурации из API</translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="62"/>
<location filename="../core/errorstrings.cpp" line="66"/>
<source>This config has already been added to the application</source>
<translation>Данная конфигурация уже была добавлена в приложение</translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="85"/>
<location filename="../core/errorstrings.cpp" line="89"/>
<source>ErrorCode: %1. </source>
<translation>Код ошибки: %1. </translation>
</message>
@@ -4159,139 +4244,139 @@ and will not be shared or disclosed to the Amnezia or any third parties</source>
<translation type="vanished">Failed to save config to disk</translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="38"/>
<location filename="../core/errorstrings.cpp" line="42"/>
<source>OpenVPN config missing</source>
<translation>Отсутствует конфигурация OpenVPN</translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="39"/>
<location filename="../core/errorstrings.cpp" line="43"/>
<source>OpenVPN management server error</source>
<translation>Серверная ошибка управлением OpenVPN</translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="42"/>
<location filename="../core/errorstrings.cpp" line="46"/>
<source>OpenVPN executable missing</source>
<translation>Отсутствует исполняемый файл OpenVPN</translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="43"/>
<location filename="../core/errorstrings.cpp" line="47"/>
<source>Shadowsocks (ss-local) executable missing</source>
<translation>Отсутствует исполняемый файл Shadowsocks (ss-local)</translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="44"/>
<location filename="../core/errorstrings.cpp" line="48"/>
<source>Cloak (ck-client) executable missing</source>
<translation>Отсутствует исполняемый файл Cloak (ck-client)</translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="45"/>
<location filename="../core/errorstrings.cpp" line="49"/>
<source>Amnezia helper service error</source>
<translation>Ошибка вспомогательной службы Amnezia</translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="46"/>
<location filename="../core/errorstrings.cpp" line="50"/>
<source>OpenSSL failed</source>
<translation>Ошибка OpenSSL</translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="49"/>
<location filename="../core/errorstrings.cpp" line="53"/>
<source>Can&apos;t connect: another VPN connection is active</source>
<translation>Невозможно подключиться: активно другое VPN-соединение</translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="50"/>
<location filename="../core/errorstrings.cpp" line="54"/>
<source>Can&apos;t setup OpenVPN TAP network adapter</source>
<translation>Невозможно настроить сетевой адаптер OpenVPN TAP</translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="51"/>
<location filename="../core/errorstrings.cpp" line="55"/>
<source>VPN pool error: no available addresses</source>
<translation>Ошибка пула VPN: нет доступных адресов</translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="54"/>
<location filename="../core/errorstrings.cpp" line="58"/>
<source>Unable to open config file</source>
<translation>Не удалось открыть файл конфигурации</translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="55"/>
<location filename="../core/errorstrings.cpp" line="59"/>
<source>VPN Protocols is not installed.
Please install VPN container at first</source>
<translation>VPN-протоколы не установлены.
Пожалуйста, установите протокол</translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="58"/>
<location filename="../core/errorstrings.cpp" line="62"/>
<source>VPN connection error</source>
<translation>Ошибка VPN-соединения</translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="63"/>
<location filename="../core/errorstrings.cpp" line="67"/>
<source>In the response from the server, an empty config was received</source>
<translation>В ответе от сервера была получена пустая конфигурация</translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="64"/>
<location filename="../core/errorstrings.cpp" line="68"/>
<source>SSL error occurred</source>
<translation>Произошла ошибка SSL</translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="65"/>
<location filename="../core/errorstrings.cpp" line="69"/>
<source>Server response timeout on api request</source>
<translation>Тайм-аут ответа сервера на запрос API</translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="66"/>
<location filename="../core/errorstrings.cpp" line="70"/>
<source>Missing AGW public key</source>
<translation type="unfinished"></translation>
<translation>Отсутствует публичный ключ AGW</translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="67"/>
<location filename="../core/errorstrings.cpp" line="71"/>
<source>Failed to decrypt response payload</source>
<translation></translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="68"/>
<location filename="../core/errorstrings.cpp" line="72"/>
<source>Missing list of available services</source>
<translation type="unfinished"></translation>
<translation>Отсутствует список доступных сервисов</translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="69"/>
<location filename="../core/errorstrings.cpp" line="73"/>
<source>The limit of allowed configurations per subscription has been exceeded</source>
<translation>Превышен лимит разрешенных конфигураций для одной подписки</translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="73"/>
<location filename="../core/errorstrings.cpp" line="77"/>
<source>QFile error: The file could not be opened</source>
<translation>Ошибка QFile: не удалось открыть файл</translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="74"/>
<location filename="../core/errorstrings.cpp" line="78"/>
<source>QFile error: An error occurred when reading from the file</source>
<translation>Ошибка QFile: произошла ошибка при чтении из файла</translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="75"/>
<location filename="../core/errorstrings.cpp" line="79"/>
<source>QFile error: The file could not be accessed</source>
<translation>Ошибка QFile: не удалось получить доступ к файлу</translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="76"/>
<location filename="../core/errorstrings.cpp" line="80"/>
<source>QFile error: An unspecified error occurred</source>
<translation>Ошибка QFile: произошла неизвестная ошибка</translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="77"/>
<location filename="../core/errorstrings.cpp" line="81"/>
<source>QFile error: A fatal error occurred</source>
<translation>Ошибка QFile: произошла фатальная ошибка</translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="78"/>
<location filename="../core/errorstrings.cpp" line="82"/>
<source>QFile error: The operation was aborted</source>
<translation>Ошибка QFile: операция была прервана</translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="82"/>
<location filename="../core/errorstrings.cpp" line="86"/>
<source>Internal error</source>
<translation>Внутренняя ошибка</translation>
</message>
@@ -5053,37 +5138,37 @@ This means that AmneziaWG keeps the fast performance of the original while addin
<translation>Имя хоста не похоже на IP-адрес или доменное имя</translation>
</message>
<message>
<location filename="../ui/controllers/sitesController.cpp" line="67"/>
<location filename="../ui/controllers/sitesController.cpp" line="66"/>
<source>New site added: %1</source>
<translation>Добавлен новый сайт: %1</translation>
</message>
<message>
<location filename="../ui/controllers/sitesController.cpp" line="80"/>
<location filename="../ui/controllers/sitesController.cpp" line="78"/>
<source>Site removed: %1</source>
<translation>Сайт удален: %1</translation>
</message>
<message>
<location filename="../ui/controllers/sitesController.cpp" line="87"/>
<location filename="../ui/controllers/sitesController.cpp" line="85"/>
<source>Can&apos;t open file: %1</source>
<translation>Невозможно открыть файл: %1</translation>
</message>
<message>
<location filename="../ui/controllers/sitesController.cpp" line="93"/>
<location filename="../ui/controllers/sitesController.cpp" line="91"/>
<source>Failed to parse JSON data from file: %1</source>
<translation>Не удалось разобрать JSON-данные из файла: %1</translation>
</message>
<message>
<location filename="../ui/controllers/sitesController.cpp" line="98"/>
<location filename="../ui/controllers/sitesController.cpp" line="96"/>
<source>The JSON data is not an array in file: %1</source>
<translation>JSON-данные не являются массивом в файле: %1</translation>
</message>
<message>
<location filename="../ui/controllers/sitesController.cpp" line="129"/>
<location filename="../ui/controllers/sitesController.cpp" line="126"/>
<source>Import completed</source>
<translation>Импорт завершен</translation>
</message>
<message>
<location filename="../ui/controllers/sitesController.cpp" line="148"/>
<location filename="../ui/controllers/sitesController.cpp" line="145"/>
<source>Export completed</source>
<translation>Экспорт завершен</translation>
</message>
@@ -5132,7 +5217,7 @@ This means that AmneziaWG keeps the fast performance of the original while addin
<context>
<name>VpnConnection</name>
<message>
<location filename="../vpnconnection.cpp" line="415"/>
<location filename="../vpnconnection.cpp" line="421"/>
<source>Mbps</source>
<translation>Мбит/с</translation>
</message>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,101 @@
#include "allowedDnsController.h"
#include <QFile>
#include <QStandardPaths>
#include <QJsonDocument>
#include <QJsonArray>
#include <QJsonObject>
#include "systemController.h"
#include "core/networkUtilities.h"
#include "core/defs.h"
AllowedDnsController::AllowedDnsController(const std::shared_ptr<Settings> &settings,
const QSharedPointer<AllowedDnsModel> &allowedDnsModel,
QObject *parent)
: QObject(parent), m_settings(settings), m_allowedDnsModel(allowedDnsModel)
{
}
void AllowedDnsController::addDns(QString ip)
{
if (ip.isEmpty()) {
return;
}
if (!NetworkUtilities::ipAddressRegExp().match(ip).hasMatch()) {
emit errorOccurred(tr("The address does not look like a valid IP address"));
return;
}
if (m_allowedDnsModel->addDns(ip)) {
emit finished(tr("New DNS server added: %1").arg(ip));
} else {
emit errorOccurred(tr("DNS server already exists: %1").arg(ip));
}
}
void AllowedDnsController::removeDns(int index)
{
auto modelIndex = m_allowedDnsModel->index(index);
auto ip = m_allowedDnsModel->data(modelIndex, AllowedDnsModel::Roles::IpRole).toString();
m_allowedDnsModel->removeDns(modelIndex);
emit finished(tr("DNS server removed: %1").arg(ip));
}
void AllowedDnsController::importDns(const QString &fileName, bool replaceExisting)
{
QByteArray jsonData;
if (!SystemController::readFile(fileName, jsonData)) {
emit errorOccurred(tr("Can't open file: %1").arg(fileName));
return;
}
QJsonDocument jsonDocument = QJsonDocument::fromJson(jsonData);
if (jsonDocument.isNull()) {
emit errorOccurred(tr("Failed to parse JSON data from file: %1").arg(fileName));
return;
}
if (!jsonDocument.isArray()) {
emit errorOccurred(tr("The JSON data is not an array in file: %1").arg(fileName));
return;
}
auto jsonArray = jsonDocument.array();
QStringList dnsServers;
for (auto jsonValue : jsonArray) {
auto ip = jsonValue.toString();
if (!NetworkUtilities::ipAddressRegExp().match(ip).hasMatch()) {
qDebug() << ip << " is not a valid IP address";
continue;
}
dnsServers.append(ip);
}
m_allowedDnsModel->addDnsList(dnsServers, replaceExisting);
emit finished(tr("Import completed"));
}
void AllowedDnsController::exportDns(const QString &fileName)
{
auto dnsServers = m_allowedDnsModel->getCurrentDnsServers();
QJsonArray jsonArray;
for (const auto &ip : dnsServers) {
jsonArray.append(ip);
}
QJsonDocument jsonDocument(jsonArray);
QByteArray jsonData = jsonDocument.toJson();
SystemController::saveFile(fileName, jsonData);
emit finished(tr("Export completed"));
}

View File

@@ -0,0 +1,35 @@
#ifndef ALLOWEDDNSCONTROLLER_H
#define ALLOWEDDNSCONTROLLER_H
#include <QObject>
#include "settings.h"
#include "ui/models/allowed_dns_model.h"
class AllowedDnsController : public QObject
{
Q_OBJECT
public:
explicit AllowedDnsController(const std::shared_ptr<Settings> &settings,
const QSharedPointer<AllowedDnsModel> &allowedDnsModel,
QObject *parent = nullptr);
public slots:
void addDns(QString ip);
void removeDns(int index);
void importDns(const QString &fileName, bool replaceExisting);
void exportDns(const QString &fileName);
signals:
void errorOccurred(const QString &errorMessage);
void finished(const QString &message);
void saveFile(const QString &fileName, const QString &data);
private:
std::shared_ptr<Settings> m_settings;
QSharedPointer<AllowedDnsModel> m_allowedDnsModel;
};
#endif // ALLOWEDDNSCONTROLLER_H

View File

@@ -310,7 +310,7 @@ bool ApiConfigsController::deactivateDevice()
auto serverConfigObject = m_serversModel->getServerConfig(serverIndex);
auto apiConfigObject = serverConfigObject.value(configKey::apiConfig).toObject();
if (apiUtils::getConfigType(serverConfigObject) != apiDefs::ConfigType::AmneziaPremiumV2) {
if (!apiUtils::isPremiumServer(serverConfigObject)) {
return true;
}
@@ -345,7 +345,7 @@ bool ApiConfigsController::deactivateExternalDevice(const QString &uuid, const Q
auto serverConfigObject = m_serversModel->getServerConfig(serverIndex);
auto apiConfigObject = serverConfigObject.value(configKey::apiConfig).toObject();
if (apiUtils::getConfigType(serverConfigObject) != apiDefs::ConfigType::AmneziaPremiumV2) {
if (!apiUtils::isPremiumServer(serverConfigObject)) {
return true;
}

View File

@@ -62,7 +62,7 @@ bool ApiSettingsController::getAccountInfo(bool reload)
QByteArray responseBody;
if (apiUtils::getConfigType(serverConfig) == apiDefs::ConfigType::AmneziaPremiumV2) {
if (apiUtils::isPremiumServer(serverConfig)) {
ErrorCode errorCode = gatewayController.post(QString("%1v1/account_info"), apiPayload, responseBody);
if (errorCode != ErrorCode::NoError) {
emit errorOccurred(errorCode);

View File

@@ -56,7 +56,7 @@ namespace
} else if ((config.contains(xrayConfigPatternInbound)) && (config.contains(xrayConfigPatternOutbound))) {
return ConfigTypes::Xray;
} else if (config.contains(openVpnConfigPatternCli)
&& (config.contains(openVpnConfigPatternDriver1) || config.contains(openVpnConfigPatternDriver2))) {
&& (config.contains(openVpnConfigPatternDriver1) || config.contains(openVpnConfigPatternDriver2))) {
return ConfigTypes::OpenVpn;
}
return ConfigTypes::Invalid;
@@ -94,6 +94,8 @@ bool ImportController::extractConfigFromFile(const QString &fileName)
bool ImportController::extractConfigFromData(QString data)
{
m_maliciousWarningText.clear();
QString config = data;
QString prefix;
QString errormsg;
@@ -654,35 +656,47 @@ void ImportController::checkForMaliciousStrings(const QJsonObject &serverConfig)
const QJsonArray &containers = serverConfig[config_key::containers].toArray();
for (const QJsonValue &container : containers) {
auto containerConfig = container.toObject();
auto containerName = containerConfig[config_key::container].toString();
if ((containerName == ContainerProps::containerToString(DockerContainer::OpenVpn))
|| (containerName == ContainerProps::containerToString(DockerContainer::Cloak))
|| (containerName == ContainerProps::containerToString(DockerContainer::ShadowSocks))) {
QString protocolConfig =
containerConfig[ProtocolProps::protoToString(Proto::OpenVpn)].toObject()[config_key::last_config].toString();
QString protocolConfigJson = QJsonDocument::fromJson(protocolConfig.toUtf8()).object()[config_key::config].toString();
QString containerName = containerConfig[config_key::container].toString();
const QRegularExpression regExp { "(\\w+-\\w+|\\w+)" };
const size_t dangerousTagsMaxCount = 3;
if (containerName == ContainerProps::containerToString(DockerContainer::OpenVpn)
|| containerName == ContainerProps::containerToString(DockerContainer::Cloak)
|| containerName == ContainerProps::containerToString(DockerContainer::ShadowSocks))
{
QString protoCfgB64 = containerConfig[ProtocolProps::protoToString(Proto::OpenVpn)]
.toObject()[config_key::last_config].toString();
QString cfgJson = QJsonDocument::fromJson(protoCfgB64.toUtf8())
.object()[config_key::config].toString();
QStringList lines = cfgJson.replace("\r", "").split('\n');
const size_t dangerousTagsMaxCount = 1;
// https://github.com/OpenVPN/openvpn/blob/master/doc/man-sections/script-options.rst
QStringList dangerousTags {
"up", "tls-verify", "ipchange", "client-connect", "route-up", "route-pre-down", "client-disconnect", "down", "learn-address", "auth-user-pass-verify"
const QStringList dangerousTags {
"up", "tls-verify", "ipchange", "client-connect",
"route-up", "route-pre-down", "client-disconnect",
"down", "learn-address", "auth-user-pass-verify"
};
const QStringList allowedTags { "up", "down" };
QStringList maliciousStrings;
QStringList lines = protocolConfigJson.replace("\r", "").split("\n");
for (const QString &l : lines) {
QRegularExpressionMatch match = regExp.match(l);
if (dangerousTags.contains(match.captured(0))) {
maliciousStrings << l;
QStringList found;
for (QString line : lines) {
line = line.trimmed();
if (line.isEmpty() || line.startsWith('#') || line.startsWith(';'))
continue;
QString tag = line.section(QRegularExpression("\\s+"), 0, 0);
if (dangerousTags.contains(tag) && !allowedTags.contains(tag)) {
found << line;
}
}
if (maliciousStrings.size() >= dangerousTagsMaxCount) {
m_maliciousWarningText = tr("In the imported configuration, potentially dangerous lines were found:");
for (const auto &string : maliciousStrings) {
m_maliciousWarningText.push_back(QString("<br><i>%1</i>").arg(string));
m_maliciousWarningText = tr("This configuration contains an OpenVPN setup. OpenVPN configurations can include malicious "
"scripts, so only add it if you fully trust the provider of this config. ");
if (found.size() >= dangerousTagsMaxCount) {
m_maliciousWarningText += tr("<br>Potentially dangerous directives found:");
for (auto &l : found) {
m_maliciousWarningText += QString("<br><i>%1</i>").arg(l);
}
}
}

View File

@@ -31,13 +31,15 @@ namespace PageLoader
PageSettingsLogging,
PageSettingsSplitTunneling,
PageSettingsAppSplitTunneling,
PageSettingsKillSwitch,
PageSettingsApiServerInfo,
PageSettingsApiAvailableCountries,
PageSettingsApiSupport,
PageSettingsApiInstructions,
PageSettingsApiNativeConfigs,
PageSettingsApiDevices,
PageSettingsKillSwitchExceptions,
PageServiceSftpSettings,
PageServiceTorWebsiteSettings,
PageServiceDnsSettings,

View File

@@ -245,6 +245,23 @@ bool SettingsController::isKillSwitchEnabled()
void SettingsController::toggleKillSwitch(bool enable)
{
m_settings->setKillSwitchEnabled(enable);
emit killSwitchEnabledChanged();
if (enable == false) {
emit strictKillSwitchEnabledChanged(false);
} else {
emit strictKillSwitchEnabledChanged(isStrictKillSwitchEnabled());
}
}
bool SettingsController::isStrictKillSwitchEnabled()
{
return m_settings->isStrictKillSwitchEnabled();
}
void SettingsController::toggleStrictKillSwitch(bool enable)
{
m_settings->setStrictKillSwitchEnabled(enable);
emit strictKillSwitchEnabledChanged(enable);
}
bool SettingsController::isNotificationPermissionGranted()

View File

@@ -24,6 +24,8 @@ public:
Q_PROPERTY(QString secondaryDns READ getSecondaryDns WRITE setSecondaryDns NOTIFY secondaryDnsChanged)
Q_PROPERTY(bool isLoggingEnabled READ isLoggingEnabled WRITE toggleLogging NOTIFY loggingStateChanged)
Q_PROPERTY(bool isNotificationPermissionGranted READ isNotificationPermissionGranted NOTIFY onNotificationStateChanged)
Q_PROPERTY(bool isKillSwitchEnabled READ isKillSwitchEnabled WRITE toggleKillSwitch NOTIFY killSwitchEnabledChanged)
Q_PROPERTY(bool strictKillSwitchEnabled READ isStrictKillSwitchEnabled WRITE toggleStrictKillSwitch NOTIFY strictKillSwitchEnabledChanged)
Q_PROPERTY(bool isDevModeEnabled READ isDevModeEnabled NOTIFY devModeEnabled)
Q_PROPERTY(QString gatewayEndpoint READ getGatewayEndpoint WRITE setGatewayEndpoint NOTIFY gatewayEndpointChanged)
@@ -75,6 +77,9 @@ public slots:
bool isKillSwitchEnabled();
void toggleKillSwitch(bool enable);
bool isStrictKillSwitchEnabled();
void toggleStrictKillSwitch(bool enable);
bool isNotificationPermissionGranted();
void requestNotificationPermission();
@@ -98,6 +103,8 @@ signals:
void primaryDnsChanged();
void secondaryDnsChanged();
void loggingStateChanged();
void killSwitchEnabledChanged();
void strictKillSwitchEnabledChanged(bool enabled);
void restoreBackupFinished();
void changeSettingsFinished(const QString &finishedMessage);

View File

@@ -0,0 +1,86 @@
#include "allowed_dns_model.h"
AllowedDnsModel::AllowedDnsModel(std::shared_ptr<Settings> settings, QObject *parent)
: QAbstractListModel(parent), m_settings(settings)
{
fillDnsServers();
}
int AllowedDnsModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return m_dnsServers.size();
}
QVariant AllowedDnsModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || index.row() < 0 || index.row() >= static_cast<int>(rowCount()))
return QVariant();
switch (role) {
case IpRole:
return m_dnsServers.at(index.row());
default:
return QVariant();
}
}
bool AllowedDnsModel::addDns(const QString &ip)
{
if (m_dnsServers.contains(ip)) {
return false;
}
beginInsertRows(QModelIndex(), rowCount(), rowCount());
m_dnsServers.append(ip);
m_settings->setAllowedDnsServers(m_dnsServers);
endInsertRows();
return true;
}
void AllowedDnsModel::addDnsList(const QStringList &dnsServers, bool replaceExisting)
{
beginResetModel();
if (replaceExisting) {
m_dnsServers.clear();
}
for (const QString &ip : dnsServers) {
if (!m_dnsServers.contains(ip)) {
m_dnsServers.append(ip);
}
}
m_settings->setAllowedDnsServers(m_dnsServers);
endResetModel();
}
void AllowedDnsModel::removeDns(QModelIndex index)
{
if (!index.isValid() || index.row() >= m_dnsServers.size()) {
return;
}
beginRemoveRows(QModelIndex(), index.row(), index.row());
m_dnsServers.removeAt(index.row());
m_settings->setAllowedDnsServers(m_dnsServers);
endRemoveRows();
}
QStringList AllowedDnsModel::getCurrentDnsServers()
{
return m_dnsServers;
}
QHash<int, QByteArray> AllowedDnsModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[IpRole] = "ip";
return roles;
}
void AllowedDnsModel::fillDnsServers()
{
m_dnsServers = m_settings->allowedDnsServers();
}

View File

@@ -0,0 +1,37 @@
#ifndef ALLOWEDDNSMODEL_H
#define ALLOWEDDNSMODEL_H
#include <QAbstractListModel>
#include "settings.h"
class AllowedDnsModel : public QAbstractListModel
{
Q_OBJECT
public:
enum Roles {
IpRole = Qt::UserRole + 1
};
explicit AllowedDnsModel(std::shared_ptr<Settings> settings, QObject *parent = nullptr);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
public slots:
bool addDns(const QString &ip);
void addDnsList(const QStringList &dnsServers, bool replaceExisting);
void removeDns(QModelIndex index);
QStringList getCurrentDnsServers();
protected:
QHash<int, QByteArray> roleNames() const override;
private:
void fillDnsServers();
std::shared_ptr<Settings> m_settings;
QStringList m_dnsServers;
};
#endif // ALLOWEDDNSMODEL_H

View File

@@ -48,15 +48,19 @@ QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const
}
case ServiceDescriptionRole: {
if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaPremiumV2) {
return tr("Classic VPN for seamless work, downloading large files, and watching videos. Access all websites and online resources. "
return tr("Classic VPN for seamless work, downloading large files, and watching videos. Access all websites and online "
"resources. "
"Speeds up to 200 Mbps");
} else if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaFreeV3) {
return tr("Free unlimited access to a basic set of websites such as Facebook, Instagram, Twitter (X), Discord, Telegram and "
"more. YouTube is not included in the free plan.");
} else {
return "";
}
}
case IsComponentVisibleRole: {
return m_accountInfoData.configType == apiDefs::ConfigType::AmneziaPremiumV2;
return m_accountInfoData.configType == apiDefs::ConfigType::AmneziaPremiumV2
|| m_accountInfoData.configType == apiDefs::ConfigType::ExternalPremium;
}
case HasExpiredWorkerRole: {
for (int i = 0; i < m_issuedConfigsInfo.size(); i++) {
@@ -93,6 +97,8 @@ void ApiAccountInfoModel::updateModel(const QJsonObject &accountInfoObject, cons
m_accountInfoData = accountInfoData;
m_supportInfo = accountInfoObject.value(apiDefs::key::supportInfo).toObject();
endResetModel();
}
@@ -121,12 +127,27 @@ QJsonArray ApiAccountInfoModel::getIssuedConfigsInfo()
QString ApiAccountInfoModel::getTelegramBotLink()
{
if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaFreeV3) {
return tr("amnezia_free_support_bot");
} else if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaPremiumV2) {
return tr("amnezia_premium_support_bot");
}
return "";
return m_supportInfo.value(apiDefs::key::telegram).toString();
}
QString ApiAccountInfoModel::getEmailLink()
{
return m_supportInfo.value(apiDefs::key::email).toString();
}
QString ApiAccountInfoModel::getBillingEmailLink()
{
return m_supportInfo.value(apiDefs::key::billingEmail).toString();
}
QString ApiAccountInfoModel::getSiteLink()
{
return m_supportInfo.value(apiDefs::key::websiteName).toString();
}
QString ApiAccountInfoModel::getFullSiteLink()
{
return m_supportInfo.value(apiDefs::key::website).toString();
}
QHash<int, QByteArray> ApiAccountInfoModel::roleNames() const

View File

@@ -33,7 +33,12 @@ public slots:
QJsonArray getAvailableCountries();
QJsonArray getIssuedConfigsInfo();
QString getTelegramBotLink();
QString getEmailLink();
QString getBillingEmailLink();
QString getSiteLink();
QString getFullSiteLink();
protected:
QHash<int, QByteArray> roleNames() const override;
@@ -51,6 +56,7 @@ private:
AccountInfoData m_accountInfoData;
QJsonArray m_availableCountries;
QJsonArray m_issuedConfigsInfo;
QJsonObject m_supportInfo;
};
#endif // APIACCOUNTINFOMODEL_H

View File

@@ -1,13 +1,11 @@
#include "languageModel.h"
LanguageModel::LanguageModel(std::shared_ptr<Settings> settings, QObject *parent)
: m_settings(settings), QAbstractListModel(parent)
LanguageModel::LanguageModel(std::shared_ptr<Settings> settings, QObject *parent) : m_settings(settings), QAbstractListModel(parent)
{
QMetaEnum metaEnum = QMetaEnum::fromType<LanguageSettings::AvailableLanguageEnum>();
for (int i = 0; i < metaEnum.keyCount(); i++) {
m_availableLanguages.push_back(
LanguageModelData {getLocalLanguageName(static_cast<LanguageSettings::AvailableLanguageEnum>(i)),
static_cast<LanguageSettings::AvailableLanguageEnum>(i) });
m_availableLanguages.push_back(LanguageModelData { getLocalLanguageName(static_cast<LanguageSettings::AvailableLanguageEnum>(i)),
static_cast<LanguageSettings::AvailableLanguageEnum>(i) });
}
}
@@ -50,8 +48,7 @@ QString LanguageModel::getLocalLanguageName(const LanguageSettings::AvailableLan
case LanguageSettings::AvailableLanguageEnum::Burmese: strLanguage = "မြန်မာဘာသာ"; break;
case LanguageSettings::AvailableLanguageEnum::Urdu: strLanguage = "اُرْدُوْ"; break;
case LanguageSettings::AvailableLanguageEnum::Hindi: strLanguage = "हिन्दी"; break;
default:
break;
default: break;
}
return strLanguage;
@@ -104,11 +101,12 @@ QString LanguageModel::getCurrentLanguageName()
return m_availableLanguages[getCurrentLanguageIndex()].name;
}
QString LanguageModel::getCurrentSiteUrl()
QString LanguageModel::getCurrentSiteUrl(const QString &path)
{
auto language = static_cast<LanguageSettings::AvailableLanguageEnum>(getCurrentLanguageIndex());
switch (language) {
case LanguageSettings::AvailableLanguageEnum::Russian: return "https://storage.googleapis.com/amnezia/amnezia.org";
default: return "https://amnezia.org";
case LanguageSettings::AvailableLanguageEnum::Russian:
return "https://storage.googleapis.com/amnezia/amnezia.org" + (path.isEmpty() ? "" : (QString("?m-path=/%1").arg(path)));
default: return QString("https://amnezia.org") + (path.isEmpty() ? "" : (QString("/%1").arg(path)));
}
}

View File

@@ -59,7 +59,7 @@ public slots:
int getCurrentLanguageIndex();
int getLineHeightAppend();
QString getCurrentLanguageName();
QString getCurrentSiteUrl();
QString getCurrentSiteUrl(const QString &path = "");
signals:
void updateTranslations(const QLocale &locale);

View File

@@ -29,7 +29,7 @@ Rectangle {
cursorShape: Qt.PointingHandCursor
onClicked: function() {
Qt.openUrlExternally(LanguageModel.getCurrentSiteUrl() + "/premium")
Qt.openUrlExternally(LanguageModel.getCurrentSiteUrl("premium"))
}
}

View File

@@ -0,0 +1,73 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Style 1.0
import "../Controls2"
import "../Controls2/TextTypes"
Item {
id: root
property bool enabled: true
property string placeholderText: ""
property alias textField: searchField.textField
signal addClicked(string text)
signal moreClicked()
implicitWidth: 360
implicitHeight: 96
Rectangle {
id: background
anchors.fill: parent
color: "#0E0F12"
opacity: 0.85
z: -1
}
RowLayout {
id: addSiteButton
enabled: root.enabled
spacing: 2
anchors {
fill: parent
topMargin: 16
leftMargin: 16
rightMargin: 16
bottomMargin: 24
}
TextFieldWithHeaderType {
id: searchField
Layout.fillWidth: true
rightButtonClickedOnEnter: true
textField.placeholderText: root.placeholderText
buttonImageSource: "qrc:/images/controls/plus.svg"
clickedFunc: function() {
root.addClicked(textField.text)
textField.text = ""
}
}
ImageButtonType {
id: addSiteButtonImage
implicitWidth: 56
implicitHeight: 56
image: "qrc:/images/controls/more-vertical.svg"
imageColor: AmneziaStyle.color.paleGray
onClicked: root.moreClicked()
Keys.onReturnPressed: addSiteButtonImage.clicked()
Keys.onEnterPressed: addSiteButtonImage.clicked()
}
}
}

View File

@@ -18,7 +18,8 @@ ListView {
property var selectedText
width: rootWidth
height: contentItem.height
anchors.top: parent.top
anchors.bottom: parent.bottom
clip: true
snapMode: ListView.SnapToItem

View File

@@ -0,0 +1,45 @@
import QtQuick
import QtQuick.Layouts
import Style 1.0
import "TextTypes"
Item {
id: root
property string headerText
property int headerTextMaximumLineCount: 2
property int headerTextElide: Qt.ElideRight
property string descriptionText
property alias headerRow: headerRow
implicitWidth: content.implicitWidth
implicitHeight: content.implicitHeight
ColumnLayout {
id: content
anchors.fill: parent
RowLayout {
id: headerRow
Header1TextType {
id: header
Layout.fillWidth: true
text: root.headerText
maximumLineCount: root.headerTextMaximumLineCount
elide: root.headerTextElide
}
}
ParagraphTextType {
id: description
Layout.topMargin: 16
Layout.fillWidth: true
text: root.descriptionText
color: AmneziaStyle.color.mutedGray
visible: root.descriptionText !== ""
}
}
}

View File

@@ -239,6 +239,7 @@ Item {
sourceComponent: root.listView
Layout.fillHeight: true
Layout.fillWidth: true
}
}
}

View File

@@ -1,86 +0,0 @@
import QtQuick
import QtQuick.Layouts
import Style 1.0
import "TextTypes"
Item {
id: root
property string actionButtonImage
property var actionButtonFunction
property alias actionButton: headerActionButton
property string headerText
property int headerTextMaximumLineCount: 2
property int headerTextElide: Qt.ElideRight
property string descriptionText
implicitWidth: content.implicitWidth
implicitHeight: content.implicitHeight
ColumnLayout {
id: content
anchors.fill: parent
RowLayout {
Header1TextType {
id: header
Layout.fillWidth: true
text: root.headerText
maximumLineCount: root.headerTextMaximumLineCount
elide: root.headerTextElide
}
ImageButtonType {
id: headerActionButton
implicitWidth: 40
implicitHeight: 40
Layout.alignment: Qt.AlignRight
image: root.actionButtonImage
imageColor: AmneziaStyle.color.paleGray
visible: image ? true : false
onClicked: {
if (actionButtonFunction && typeof actionButtonFunction === "function") {
actionButtonFunction()
}
}
}
}
ParagraphTextType {
id: description
Layout.topMargin: 16
Layout.fillWidth: true
text: root.descriptionText
color: AmneziaStyle.color.mutedGray
visible: root.descriptionText !== ""
}
}
Keys.onEnterPressed: {
if (actionButtonFunction && typeof actionButtonFunction === "function") {
actionButtonFunction()
}
}
Keys.onReturnPressed: {
if (actionButtonFunction && typeof actionButtonFunction === "function") {
actionButtonFunction()
}
}
}

View File

@@ -0,0 +1,44 @@
import QtQuick
import QtQuick.Layouts
import Style 1.0
BaseHeaderType {
id: root
property string actionButtonImage
property var actionButtonFunction
property alias actionButton: headerActionButton
Component.onCompleted: {
headerRow.children.push(headerActionButton)
}
ImageButtonType {
id: headerActionButton
implicitWidth: 40
implicitHeight: 40
Layout.alignment: Qt.AlignRight
image: root.actionButtonImage
imageColor: AmneziaStyle.color.paleGray
visible: image ? true : false
onClicked: {
if (actionButtonFunction && typeof actionButtonFunction === "function") {
actionButtonFunction()
}
}
}
Keys.onEnterPressed: {
if (actionButtonFunction && typeof actionButtonFunction === "function") {
actionButtonFunction()
}
}
Keys.onReturnPressed: {
if (actionButtonFunction && typeof actionButtonFunction === "function") {
actionButtonFunction()
}
}
}

View File

@@ -0,0 +1,28 @@
import QtQuick
import QtQuick.Layouts
import Style 1.0
BaseHeaderType {
id: root
property var switcherFunction
property bool showSwitcher: false
property alias switcher: headerSwitcher
Component.onCompleted: {
headerRow.children.push(headerSwitcher)
}
SwitcherType {
id: headerSwitcher
Layout.alignment: Qt.AlignRight
visible: root.showSwitcher
onToggled: {
if (switcherFunction && typeof switcherFunction === "function") {
switcherFunction(checked)
}
}
}
}

View File

@@ -20,7 +20,11 @@ RadioButton {
property string selectedColor: AmneziaStyle.color.transparent
property string textColor: AmneziaStyle.color.paleGray
property string textDisabledColor: AmneziaStyle.color.mutedGray
property string selectedTextColor: AmneziaStyle.color.goldenApricot
property string selectedTextDisabledColor: AmneziaStyle.color.burntOrange
property string descriptionColor: AmneziaStyle.color.mutedGray
property string descriptionDisabledColor: AmneziaStyle.color.charcoalGray
property string borderFocusedColor: AmneziaStyle.color.paleGray
property int borderFocusedWidth: 1
@@ -30,6 +34,12 @@ RadioButton {
property bool isFocusable: true
property string radioButtonInnerCirclePressedSource: "qrc:/images/controls/radio-button-inner-circle-pressed.png"
property string radioButtonInnerCircleSource: "qrc:/images/controls/radio-button-inner-circle.png"
property string radioButtonPressedSource: "qrc:/images/controls/radio-button-pressed.svg"
property string radioButtonDefaultSource: "qrc:/images/controls/radio-button.svg"
Keys.onTabPressed: {
FocusController.nextKeyTabItem()
}
@@ -94,14 +104,15 @@ RadioButton {
if (showImage) {
return imageSource
} else if (root.pressed) {
return "qrc:/images/controls/radio-button-inner-circle-pressed.png"
return root.radioButtonInnerCirclePressedSource
} else if (root.checked) {
return "qrc:/images/controls/radio-button-inner-circle.png"
return root.radioButtonInnerCircleSource
}
return ""
}
opacity: root.enabled ? 1.0 : 0.3
anchors.centerIn: parent
width: 24
@@ -113,12 +124,13 @@ RadioButton {
if (showImage) {
return ""
} else if (root.pressed || root.checked) {
return "qrc:/images/controls/radio-button-pressed.svg"
return root.radioButtonPressedSource
} else {
return "qrc:/images/controls/radio-button.svg"
return root.radioButtonDefaultSource
}
}
opacity: root.enabled ? 1.0 : 0.3
anchors.centerIn: parent
width: 24
@@ -148,10 +160,11 @@ RadioButton {
elide: root.textElide
color: {
if (root.checked) {
return selectedTextColor
if (root.enabled) {
return root.checked ? selectedTextColor : textColor
} else {
return root.checked ? selectedTextDisabledColor : textDisabledColor
}
return textColor
}
Layout.fillWidth: true
@@ -164,7 +177,7 @@ RadioButton {
CaptionTextType {
id: description
color: AmneziaStyle.color.mutedGray
color: root.enabled ? root.descriptionColor : root.descriptionDisabledColor
text: root.descriptionText
visible: root.descriptionText !== ""
@@ -177,6 +190,7 @@ RadioButton {
MouseArea {
anchors.fill: root
cursorShape: Qt.PointingHandCursor
preventStealing: false
enabled: false
}
}

View File

@@ -56,7 +56,7 @@ PageType {
anchors.rightMargin: 16
anchors.leftMargin: 16
HeaderType {
BaseHeaderType {
Layout.fillWidth: true
Layout.topMargin: 20

View File

@@ -39,7 +39,7 @@ PageType {
header: ColumnLayout {
width: listView.width
HeaderType {
BaseHeaderType {
id: header
Layout.fillWidth: true

View File

@@ -91,7 +91,7 @@ PageType {
spacing: 0
HeaderType {
BaseHeaderType {
Layout.fillWidth: true
headerText: qsTr("AmneziaWG settings")

View File

@@ -91,7 +91,7 @@ PageType {
spacing: 0
HeaderType {
BaseHeaderType {
Layout.fillWidth: true
headerText: qsTr("AmneziaWG settings")

View File

@@ -76,7 +76,7 @@ PageType {
spacing: 0
HeaderType {
BaseHeaderType {
Layout.fillWidth: true
headerText: qsTr("Cloak settings")

View File

@@ -75,7 +75,7 @@ PageType {
spacing: 0
HeaderType {
BaseHeaderType {
Layout.fillWidth: true
headerText: qsTr("OpenVPN settings")

View File

@@ -32,7 +32,7 @@ PageType {
id: backButton
}
HeaderType {
BaseHeaderType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16

View File

@@ -78,7 +78,7 @@ PageType {
spacing: 0
HeaderType {
BaseHeaderType {
Layout.fillWidth: true
headerText: qsTr("Shadowsocks settings")

View File

@@ -85,7 +85,7 @@ PageType {
spacing: 0
HeaderType {
BaseHeaderType {
Layout.fillWidth: true
headerText: qsTr("WG settings")

View File

@@ -77,7 +77,7 @@ PageType {
spacing: 0
HeaderType {
BaseHeaderType {
Layout.fillWidth: true
headerText: qsTr("WG settings")
}

View File

@@ -75,7 +75,7 @@ PageType {
spacing: 0
HeaderType {
BaseHeaderType {
Layout.fillWidth: true
headerText: qsTr("XRay settings")
}

View File

@@ -43,7 +43,7 @@ PageType {
anchors.left: parent.left
anchors.right: parent.right
HeaderType {
BaseHeaderType {
id: header
Layout.fillWidth: true

View File

@@ -85,7 +85,7 @@ PageType {
spacing: 0
HeaderType {
BaseHeaderType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16

View File

@@ -77,7 +77,7 @@ PageType {
spacing: 0
HeaderType {
BaseHeaderType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
@@ -217,7 +217,7 @@ PageType {
}
}
HeaderType {
BaseHeaderType {
Layout.fillWidth: true
headerText: qsTr("SOCKS5 settings")

View File

@@ -54,7 +54,7 @@ PageType {
spacing: 0
HeaderType {
BaseHeaderType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16

View File

@@ -29,7 +29,7 @@ PageType {
spacing: 0
HeaderType {
BaseHeaderType {
id: header
Layout.fillWidth: true
Layout.topMargin: 24

View File

@@ -252,7 +252,7 @@ PageType {
text: qsTr("Privacy Policy")
clickedFunc: function() {
Qt.openUrlExternally(LanguageModel.getCurrentSiteUrl() + "/policy")
Qt.openUrlExternally(LanguageModel.getCurrentSiteUrl("policy"))
}
}
}

View File

@@ -69,7 +69,7 @@ PageType {
Layout.topMargin: 20
}
HeaderType {
HeaderTypeWithButton {
id: headerContent
objectName: "headerContent"
@@ -135,12 +135,6 @@ PageType {
}
}
MouseArea {
anchors.fill: containerRadioButton
cursorShape: Qt.PointingHandCursor
enabled: false
}
Keys.onEnterPressed: {
if (checkable) {
checked = true

View File

@@ -35,7 +35,7 @@ PageType {
id: backButton
}
HeaderType {
BaseHeaderType {
id: header
Layout.fillWidth: true

View File

@@ -91,7 +91,7 @@ PageType {
id: backButton
}
HeaderType {
BaseHeaderType {
id: header
Layout.fillWidth: true

View File

@@ -38,7 +38,7 @@ PageType {
id: backButton
}
HeaderType {
BaseHeaderType {
id: header
Layout.fillWidth: true

View File

@@ -93,7 +93,7 @@ PageType {
Layout.topMargin: 20
}
HeaderType {
HeaderTypeWithButton {
id: headerContent
objectName: "headerContent"

View File

@@ -28,24 +28,24 @@ PageType {
id: techSupport
readonly property string title: qsTr("Email")
readonly property string description: qsTr("support@amnezia.org")
readonly property string link: "mailto:support@amnezia.org"
readonly property string description: ApiAccountInfoModel.getEmailLink()
readonly property string link: "mailto:" + ApiAccountInfoModel.getEmailLink()
}
QtObject {
id: paymentSupport
readonly property string title: qsTr("Email Billing & Orders")
readonly property string description: qsTr("help@vpnpay.io")
readonly property string link: "mailto:help@vpnpay.io"
readonly property string description: ApiAccountInfoModel.getBillingEmailLink()
readonly property string link: "mailto:" + ApiAccountInfoModel.getBillingEmailLink()
}
QtObject {
id: site
readonly property string title: qsTr("Website")
readonly property string description: qsTr("amnezia.org")
readonly property string link: LanguageModel.getCurrentSiteUrl()
readonly property string description: ApiAccountInfoModel.getSiteLink()
readonly property string link: ApiAccountInfoModel.getFullSiteLink()
}
property list<QtObject> supportModel: [
@@ -71,7 +71,7 @@ PageType {
id: backButton
}
HeaderType {
BaseHeaderType {
id: header
Layout.fillWidth: true

View File

@@ -79,29 +79,22 @@ PageType {
id: backButton
}
RowLayout {
HeaderType {
Layout.fillWidth: true
Layout.leftMargin: 16
HeaderTypeWithSwitcher {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
headerText: qsTr("App split tunneling")
headerText: qsTr("App split tunneling")
enabled: root.pageEnabled
showSwitcher: true
switcher {
checked: AppSplitTunnelingModel.isTunnelingEnabled
enabled: root.pageEnabled
}
SwitcherType {
id: switcher
Layout.fillWidth: true
Layout.rightMargin: 16
enabled: root.pageEnabled
checked: AppSplitTunnelingModel.isTunnelingEnabled
onToggled: {
AppSplitTunnelingModel.toggleSplitTunneling(checked)
selector.text = root.routeModesModel[getRouteModesModelIndex()].name
}
switcherFunction: function(checked) {
AppSplitTunnelingModel.toggleSplitTunneling(checked)
selector.text = root.routeModesModel[getRouteModesModelIndex()].name
}
}

View File

@@ -38,7 +38,7 @@ PageType {
spacing: 0
HeaderType {
BaseHeaderType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16

View File

@@ -60,7 +60,7 @@ PageType {
spacing: 16
HeaderType {
BaseHeaderType {
Layout.fillWidth: true
headerText: qsTr("Back up your configuration")

View File

@@ -36,7 +36,7 @@ PageType {
anchors.left: parent.left
anchors.right: parent.right
HeaderType {
BaseHeaderType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
@@ -94,9 +94,7 @@ PageType {
}
}
DividerType {
visible: root.isAppSplitTinnelingEnabled
}
DividerType {}
LabelWithButtonType {
id: splitTunnelingButton2
@@ -119,29 +117,20 @@ PageType {
visible: root.isAppSplitTinnelingEnabled
}
SwitcherType {
id: killSwitchSwitcher
LabelWithButtonType {
id: killSwitchButton
visible: !GC.isMobile()
Layout.fillWidth: true
Layout.margins: 16
text: qsTr("KillSwitch")
descriptionText: qsTr("Disables your internet if your encrypted VPN connection drops out for any reason.")
descriptionText: qsTr("Blocks network connections without VPN")
rightImageSource: "qrc:/images/controls/chevron-right.svg"
parentFlickable: fl
checked: SettingsController.isKillSwitchEnabled()
checkable: !ConnectionController.isConnected
onCheckedChanged: {
if (checked !== SettingsController.isKillSwitchEnabled()) {
SettingsController.toggleKillSwitch(checked)
}
}
onClicked: {
if (!checkable) {
PageController.showNotificationMessage(qsTr("Cannot change KillSwitch settings during active connection"))
}
clickedFunction: function() {
PageController.goToPage(PageEnum.PageSettingsKillSwitch)
}
}

View File

@@ -50,7 +50,7 @@ PageType {
spacing: 16
HeaderType {
BaseHeaderType {
Layout.fillWidth: true
headerText: qsTr("DNS servers")

View File

@@ -0,0 +1,123 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import PageEnum 1.0
import Style 1.0
import "./"
import "../Controls2"
import "../Config"
PageType {
id: root
BackButtonType {
id: backButton
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 20
}
FlickableType {
id: fl
anchors.top: backButton.bottom
anchors.bottom: parent.bottom
contentHeight: content.height
ColumnLayout {
id: content
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
HeaderTypeWithSwitcher {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
headerText: qsTr("KillSwitch")
descriptionText: qsTr("Enable to ensure network traffic goes through a secure VPN tunnel, preventing accidental exposure of your IP and DNS queries if the connection drops")
showSwitcher: true
switcher {
checked: SettingsController.isKillSwitchEnabled
enabled: !ConnectionController.isConnected
}
switcherFunction: function(checked) {
if (!ConnectionController.isConnected) {
SettingsController.isKillSwitchEnabled = checked
} else {
PageController.showNotificationMessage(qsTr("Cannot change killSwitch settings during active connection"))
switcher.checked = SettingsController.isKillSwitchEnabled
}
}
}
VerticalRadioButton {
id: softKillSwitch
Layout.fillWidth: true
Layout.topMargin: 32
Layout.leftMargin: 16
Layout.rightMargin: 16
enabled: SettingsController.isKillSwitchEnabled && !ConnectionController.isConnected
checked: !SettingsController.strictKillSwitchEnabled
text: qsTr("Soft KillSwitch")
descriptionText: qsTr("Internet connection is blocked if VPN connection drops accidentally")
onClicked: function() {
SettingsController.strictKillSwitchEnabled = false
}
}
DividerType {}
VerticalRadioButton {
id: strictKillSwitch
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
enabled: SettingsController.isKillSwitchEnabled && !ConnectionController.isConnected
checked: SettingsController.strictKillSwitchEnabled
text: qsTr("Strict KillSwitch")
descriptionText: qsTr("Internet connection is blocked even if VPN was turned off manually or not started")
onClicked: function() {
var headerText = qsTr("Just a little heads-up")
var descriptionText = qsTr("If you disconnect from VPN or the VPN connection drops while the Strict Kill Switch is turned on, your internet access will be disabled. To restore it, connect to VPN, change the Kill Switch mode or turn the Kill Switch off.")
var yesButtonText = qsTr("Continue")
var noButtonText = qsTr("Cancel")
var yesButtonFunction = function() {
SettingsController.strictKillSwitchEnabled = true
}
var noButtonFunction = function() {
}
showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
}
}
DividerType {}
LabelWithButtonType {
Layout.topMargin: 32
Layout.fillWidth: true
enabled: true
text: qsTr("DNS Exceptions")
descriptionText: qsTr("DNS servers from the list will remain accessible when Kill Switch is triggered")
rightImageSource: "qrc:/images/controls/chevron-right.svg"
clickedFunction: function() {
PageController.goToPage(PageEnum.PageSettingsKillSwitchExceptions)
}
}
}
}
}

View File

@@ -0,0 +1,302 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Dialogs
import QtCore
import SortFilterProxyModel 0.2
import PageEnum 1.0
import ProtocolEnum 1.0
import ContainerProps 1.0
import Style 1.0
import "./"
import "../Controls2"
import "../Controls2/TextTypes"
import "../Config"
import "../Components"
PageType {
id: root
property bool pageEnabled: true
ColumnLayout {
id: header
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 20
BackButtonType {
id: backButton
}
BaseHeaderType {
enabled: root.pageEnabled
Layout.fillWidth: true
Layout.leftMargin: 16
headerText: qsTr("DNS Exceptions")
descriptionText: qsTr("DNS servers from the list will remain accessible when Kill Switch is triggered")
}
}
ListView {
id: listView
anchors.top: header.bottom
anchors.topMargin: 16
anchors.bottom: parent.bottom
width: parent.width
enabled: root.pageEnabled
property bool isFocusable: true
cacheBuffer: 200
displayMarginBeginning: 40
displayMarginEnd: 40
ScrollBar.vertical: ScrollBarType { }
footer: Item {
width: listView.width
height: addSitePanel.height
}
footerPositioning: ListView.InlineFooter
model: SortFilterProxyModel {
id: dnsFilterModel
sourceModel: AllowedDnsModel
filters: [
RegExpFilter {
roleName: "ip"
pattern: ".*" + addSitePanel.textField.text + ".*"
caseSensitivity: Qt.CaseInsensitive
}
]
}
clip: true
reuseItems: true
delegate: ColumnLayout {
id: delegateContent
width: listView.width
LabelWithButtonType {
id: site
Layout.fillWidth: true
text: ip
rightImageSource: "qrc:/images/controls/trash.svg"
rightImageColor: AmneziaStyle.color.paleGray
clickedFunction: function() {
var headerText = qsTr("Delete ") + ip + "?"
var yesButtonText = qsTr("Continue")
var noButtonText = qsTr("Cancel")
var yesButtonFunction = function() {
AllowedDnsController.removeDns(dnsFilterModel.mapToSource(index))
if (!GC.isMobile()) {
site.rightButton.forceActiveFocus()
}
}
var noButtonFunction = function() {
if (!GC.isMobile()) {
site.rightButton.forceActiveFocus()
}
}
showQuestionDrawer(headerText, "", yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
}
}
DividerType {}
}
}
AddSitePanel {
id: addSitePanel
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
enabled: root.pageEnabled
placeholderText: qsTr("IPv4 address")
onAddClicked: function(text) {
PageController.showBusyIndicator(true)
AllowedDnsController.addDns(text)
PageController.showBusyIndicator(false)
}
onMoreClicked: {
moreActionsDrawer.openTriggered()
}
}
DrawerType2 {
id: moreActionsDrawer
anchors.fill: parent
expandedHeight: parent.height * 0.4375
expandedStateContent: ColumnLayout {
id: moreActionsDrawerContent
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
Header2Type {
Layout.fillWidth: true
Layout.margins: 16
headerText: qsTr("Import / Export addresses")
}
LabelWithButtonType {
id: importSitesButton
Layout.fillWidth: true
text: qsTr("Import")
rightImageSource: "qrc:/images/controls/chevron-right.svg"
clickedFunction: function() {
importSitesDrawer.openTriggered()
}
}
DividerType {}
LabelWithButtonType {
id: exportSitesButton
Layout.fillWidth: true
text: qsTr("Save address list")
clickedFunction: function() {
var fileName = ""
if (GC.isMobile()) {
fileName = "amnezia_killswitch_exceptions.json"
} else {
fileName = SystemController.getFileName(qsTr("Save addresses"),
qsTr("Address files (*.json)"),
StandardPaths.standardLocations(StandardPaths.DocumentsLocation) + "/amnezia_killswitch_exceptions",
true,
".json")
}
if (fileName !== "") {
PageController.showBusyIndicator(true)
AllowedDnsController.exportDns(fileName)
moreActionsDrawer.closeTriggered()
PageController.showBusyIndicator(false)
}
}
}
DividerType {}
}
}
DrawerType2 {
id: importSitesDrawer
anchors.fill: parent
expandedHeight: parent.height * 0.4375
expandedStateContent: Item {
implicitHeight: importSitesDrawer.expandedHeight
BackButtonType {
id: importSitesDrawerBackButton
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 16
backButtonFunction: function() {
importSitesDrawer.closeTriggered()
}
}
FlickableType {
anchors.top: importSitesDrawerBackButton.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
contentHeight: importSitesDrawerContent.height
ColumnLayout {
id: importSitesDrawerContent
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
Header2Type {
Layout.fillWidth: true
Layout.margins: 16
headerText: qsTr("Import address list")
}
LabelWithButtonType {
id: importSitesButton2
Layout.fillWidth: true
text: qsTr("Replace address list")
clickedFunction: function() {
var fileName = SystemController.getFileName(qsTr("Open address file"),
qsTr("Address files (*.json)"))
if (fileName !== "") {
importSitesDrawerContent.importSites(fileName, true)
}
}
}
DividerType {}
LabelWithButtonType {
id: importSitesButton3
Layout.fillWidth: true
text: qsTr("Add imported addresses to existing ones")
clickedFunction: function() {
var fileName = SystemController.getFileName(qsTr("Open address file"),
qsTr("Address files (*.json)"))
if (fileName !== "") {
importSitesDrawerContent.importSites(fileName, false)
}
}
}
function importSites(fileName, replaceExistingSites) {
PageController.showBusyIndicator(true)
AllowedDnsController.importDns(fileName, replaceExistingSites)
PageController.showBusyIndicator(false)
importSitesDrawer.closeTriggered()
moreActionsDrawer.closeTriggered()
}
DividerType {}
}
}
}
}
}

View File

@@ -40,7 +40,7 @@ PageType {
header: ColumnLayout {
width: listView.width
HeaderType {
BaseHeaderType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16

View File

@@ -71,7 +71,7 @@ PageType {
objectName: "backButton"
}
HeaderType {
HeaderTypeWithButton {
id: headerContent
objectName: "headerContent"

View File

@@ -34,7 +34,7 @@ PageType {
id: backButton
}
HeaderType {
BaseHeaderType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16

View File

@@ -31,7 +31,7 @@ PageType {
id: backButton
}
HeaderType {
BaseHeaderType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16

View File

@@ -94,33 +94,22 @@ PageType {
id: backButton
}
RowLayout {
HeaderType {
enabled: root.pageEnabled
HeaderTypeWithSwitcher {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.fillWidth: true
Layout.leftMargin: 16
headerText: qsTr("Split tunneling")
}
SwitcherType {
id: switcher
enabled: root.pageEnabled
Layout.fillWidth: true
Layout.rightMargin: 16
function onToggledFunc() {
SitesModel.toggleSplitTunneling(this.checked)
selector.text = root.routeModesModel[getRouteModesModelIndex()].name
}
headerText: qsTr("Split tunneling")
enabled: root.pageEnabled
showSwitcher: true
switcher {
checked: SitesModel.isTunnelingEnabled
onToggled: { onToggledFunc() }
Keys.onEnterPressed: { onToggledFunc() }
Keys.onReturnPressed: { onToggledFunc() }
enabled: root.pageEnabled
}
switcherFunction: function(checked) {
SitesModel.toggleSplitTunneling(checked)
selector.text = root.routeModesModel[getRouteModesModelIndex()].name
}
}

View File

@@ -35,7 +35,7 @@ PageType {
Layout.topMargin: 20
}
HeaderType {
BaseHeaderType {
Layout.fillWidth: true
Layout.topMargin: 8
Layout.rightMargin: 16

Some files were not shown because too many files have changed in this diff Show More