diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index a218a1ad9..9f5566273 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -48,14 +48,18 @@ jobs: export QIF_BIN_DIR=${{ runner.temp }}/Qt/Tools/QtInstallerFramework/${{ env.QIF_VERSION }}/bin bash deploy/build_linux.sh + - name: 'Pack installer' + run: cd deploy && tar -cf AmneziaVPN_Linux_Installer.tar AmneziaVPN_Linux_Installer.bin + - name: 'Upload installer artifact' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: AmneziaVPN_Linux_installer - path: deploy/AmneziaVPN_Linux_Installer + name: AmneziaVPN_Linux_installer.tar + path: deploy/AmneziaVPN_Linux_Installer.tar retention-days: 7 + - name: 'Upload unpacked artifact' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: AmneziaVPN_Linux_unpacked path: deploy/AppDir @@ -110,13 +114,14 @@ jobs: call deploy\\build_windows.bat - name: 'Upload installer artifact' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: AmneziaVPN_Windows_installer path: AmneziaVPN_x${{ env.BUILD_ARCH }}.exe retention-days: 7 + - name: 'Upload unpacked artifact' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: AmneziaVPN_Windows_unpacked path: deploy\\build_${{ env.BUILD_ARCH }}\\client\\Release @@ -200,7 +205,7 @@ jobs: IOS_NE_PROVISIONING_PROFILE: ${{ secrets.IOS_NE_PROVISIONING_PROFILE }} # - name: 'Upload appstore .ipa and dSYMs to artifacts' -# uses: actions/upload-artifact@v3 +# uses: actions/upload-artifact@v4 # with: # name: app-store ipa & dsyms # path: | @@ -255,13 +260,14 @@ jobs: bash deploy/build_macos.sh - name: 'Upload installer artifact' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: AmneziaVPN_MacOS_installer path: AmneziaVPN.dmg retention-days: 7 + - name: 'Upload unpacked artifact' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: AmneziaVPN_MacOS_unpacked path: deploy/build/client/AmneziaVPN.app @@ -375,32 +381,44 @@ jobs: ANDROID_KEYSTORE_KEY_ALIAS: ${{ secrets.ANDROID_RELEASE_KEYSTORE_KEY_ALIAS }} ANDROID_KEYSTORE_KEY_PASS: ${{ secrets.ANDROID_RELEASE_KEYSTORE_KEY_PASS }} shell: bash - run: ./deploy/build_android.sh --apk all --build-platform ${{ env.ANDROID_BUILD_PLATFORM }} + run: ./deploy/build_android.sh --aab --apk all --build-platform ${{ env.ANDROID_BUILD_PLATFORM }} - name: 'Upload x86_64 apk' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: AmneziaVPN-android-x86_64 path: deploy/build/AmneziaVPN-x86_64-release.apk + compression-level: 0 retention-days: 7 - name: 'Upload x86 apk' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: AmneziaVPN-android-x86 path: deploy/build/AmneziaVPN-x86-release.apk + compression-level: 0 retention-days: 7 - name: 'Upload arm64-v8a apk' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: AmneziaVPN-android-arm64-v8a path: deploy/build/AmneziaVPN-arm64-v8a-release.apk + compression-level: 0 retention-days: 7 - name: 'Upload armeabi-v7a apk' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: AmneziaVPN-android-armeabi-v7a path: deploy/build/AmneziaVPN-armeabi-v7a-release.apk + compression-level: 0 + retention-days: 7 + + - name: 'Upload aab' + uses: actions/upload-artifact@v4 + with: + name: AmneziaVPN-android + path: deploy/build/AmneziaVPN-release.aab + compression-level: 0 retention-days: 7 diff --git a/.gitmodules b/.gitmodules index c96dd6bc1..ff50f897e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -22,6 +22,6 @@ [submodule "client/3rd-prebuilt"] path = client/3rd-prebuilt url = https://github.com/amnezia-vpn/3rd-prebuilt -[submodule "client/3rd/awg-apple"] - path = client/3rd/awg-apple - url = https://github.com/amnezia-vpn/awg-apple +[submodule "client/3rd/amneziawg-apple"] + path = client/3rd/amneziawg-apple + url = https://github.com/amnezia-vpn/amneziawg-apple diff --git a/CMakeLists.txt b/CMakeLists.txt index 084ff3d0d..06e92993c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR) set(PROJECT AmneziaVPN) -project(${PROJECT} VERSION 4.1.0.1 +project(${PROJECT} VERSION 4.2.1.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 39) +set(APP_ANDROID_VERSION_CODE 42) if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") set(MZ_PLATFORM_NAME "linux") diff --git a/README.md b/README.md index 94dffe09d..88ec574c4 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,6 @@ Amnezia is an open-source VPN client, with a key feature that enables you to dep [https://www.reddit.com/r/AmneziaVPN](https://www.reddit.com/r/AmneziaVPN) - Reddit [https://t.me/amnezia_vpn_en](https://t.me/amnezia_vpn_en) - Telegram support channel (English) [https://t.me/amnezia_vpn](https://t.me/amnezia_vpn) - Telegram support channel (Russian) -[https://signal.group/...](https://signal.group/#CjQKIB2gUf8QH_IXnOJMGQWMDjYz9cNfmRQipGWLFiIgc4MwEhAKBONrSiWHvoUFbbD0xwdh) - Signal channel ## Tech @@ -36,7 +35,7 @@ AmneziaVPN uses a number of open source projects to work: Make sure to pull all submodules after checking out the repo. ```bash -git submodule update --init +git submodule update --init --recursive ``` ## Development @@ -50,7 +49,15 @@ Look deploy folder for build scripts. 1. First, make sure you have [XCode](https://developer.apple.com/xcode/) installed, at least version 14 or higher. -2. We use QT to generate the XCode project. we need QT version 6.4. Install QT for macos in [here](https://doc.qt.io/qt-6/macos.html) +2. We use QT to generate the XCode project. we need QT version 6.6.1. Install QT for macos in [here](https://doc.qt.io/qt-6/macos.html) or [QT Online Installer](https://www.qt.io/download-open-source). Required modules: + - macOS + - iOS + - Qt 5 Compatibility Module + - Qt Shader Tools + - Additional Libraries: + - Qt Image Formats + - Qt Multimedia + - Qt Remote Objects 3. Install cmake is require. We recommend cmake version 3.25. You can install cmake in [here](https://cmake.org/download/) @@ -66,10 +73,11 @@ gomobile init 5. Build project ```bash export QT_BIN_DIR="/Qt//ios/bin" +export QT_MACOS_ROOT_DIR="/Qt//macos" export QT_IOS_BIN=$QT_BIN_DIR export PATH=$PATH:~/go/bin mkdir build-ios -$QT_IOS_BIN/qt-cmake . -B build-ios -GXcode -DQT_HOST_PATH=$QT_BIN_DIR +$QT_IOS_BIN/qt-cmake . -B build-ios -GXcode -DQT_HOST_PATH=$QT_MACOS_ROOT_DIR ``` Replace PATH-TO-QT-FOLDER and QT-VERSION to your environment diff --git a/client/3rd-prebuilt b/client/3rd-prebuilt index fcf3022a2..e568e7d0e 160000 --- a/client/3rd-prebuilt +++ b/client/3rd-prebuilt @@ -1 +1 @@ -Subproject commit fcf3022a2724402f68cc11bcbed9b43ea9ffcc07 +Subproject commit e568e7d0e8defe8fe009c0127323f2c55fd9be76 diff --git a/client/3rd/amneziawg-apple b/client/3rd/amneziawg-apple new file mode 160000 index 000000000..f23eee470 --- /dev/null +++ b/client/3rd/amneziawg-apple @@ -0,0 +1 @@ +Subproject commit f23eee4700ed4a2ef44a800d2c20466c9ab0222b diff --git a/client/3rd/awg-apple b/client/3rd/awg-apple deleted file mode 160000 index 233eda676..000000000 --- a/client/3rd/awg-apple +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 233eda6760962efddc860f177a0ce2bcdf533d85 diff --git a/client/3rd/qtkeychain b/client/3rd/qtkeychain index 8bbaa6d83..74776e2a3 160000 --- a/client/3rd/qtkeychain +++ b/client/3rd/qtkeychain @@ -1 +1 @@ -Subproject commit 8bbaa6d8302cf0747d9786ace4dd13c7fb746502 +Subproject commit 74776e2a3e2d98d19943e0968901c5b5e04cc1bd diff --git a/client/amnezia_application.cpp b/client/amnezia_application.cpp index 4d3722262..f0a7769a8 100644 --- a/client/amnezia_application.cpp +++ b/client/amnezia_application.cpp @@ -1,377 +1,389 @@ -#include "amnezia_application.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include "logger.h" -#include "version.h" - -#include "platforms/ios/QRCodeReaderBase.h" -#if defined(Q_OS_ANDROID) - #include "platforms/android/android_controller.h" -#endif - -#include "protocols/qml_register_protocols.h" - -#if defined(Q_OS_IOS) - #include "platforms/ios/ios_controller.h" -#endif - -#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) -AmneziaApplication::AmneziaApplication(int &argc, char *argv[]) : AMNEZIA_BASE_CLASS(argc, argv) -#else -AmneziaApplication::AmneziaApplication(int &argc, char *argv[], bool allowSecondary, SingleApplication::Options options, - int timeout, const QString &userData) - : SingleApplication(argc, argv, allowSecondary, options, timeout, userData) -#endif -{ - setQuitOnLastWindowClosed(false); - - // Fix config file permissions -#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) - { - QSettings s(ORGANIZATION_NAME, APPLICATION_NAME); - s.setValue("permFixed", true); - } - - QString configLoc1 = QStandardPaths::standardLocations(QStandardPaths::ConfigLocation).first() + "/" - + ORGANIZATION_NAME + "/" + APPLICATION_NAME + ".conf"; - QFile::setPermissions(configLoc1, QFileDevice::ReadOwner | QFileDevice::WriteOwner); - - QString configLoc2 = QStandardPaths::standardLocations(QStandardPaths::ConfigLocation).first() + "/" - + ORGANIZATION_NAME + "/" + APPLICATION_NAME + "/" + APPLICATION_NAME + ".conf"; - QFile::setPermissions(configLoc2, QFileDevice::ReadOwner | QFileDevice::WriteOwner); -#endif - - m_settings = std::shared_ptr(new Settings); -} - -AmneziaApplication::~AmneziaApplication() -{ - m_vpnConnectionThread.quit(); - m_vpnConnectionThread.wait(3000); - - if (m_engine) { - QObject::disconnect(m_engine, 0, 0, 0); - delete m_engine; - } -} - -void AmneziaApplication::init() -{ - m_engine = new QQmlApplicationEngine; - - const QUrl url(QStringLiteral("qrc:/ui/qml/main2.qml")); - QObject::connect( - m_engine, &QQmlApplicationEngine::objectCreated, this, - [url](QObject *obj, const QUrl &objUrl) { - if (!obj && url == objUrl) - QCoreApplication::exit(-1); - }, - Qt::QueuedConnection); - - m_engine->rootContext()->setContextProperty("Debug", &Logger::Instance()); - - m_configurator = std::shared_ptr(new VpnConfigurator(m_settings, this)); - m_vpnConnection.reset(new VpnConnection(m_settings, m_configurator)); - m_vpnConnection->moveToThread(&m_vpnConnectionThread); - m_vpnConnectionThread.start(); - - initModels(); - loadTranslator(); - initControllers(); - -#ifdef Q_OS_ANDROID - connect(AndroidController::instance(), &AndroidController::initConnectionState, this, - [this](Vpn::ConnectionState state) { - m_connectionController->onConnectionStateChanged(state); - if (m_vpnConnection) - m_vpnConnection->restoreConnection(); - }); - if (!AndroidController::instance()->initialize()) { - qCritical() << QString("Init failed"); - if (m_vpnConnection) - emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Error); - return; - } - - connect(AndroidController::instance(), &AndroidController::importConfigFromOutside, [this](QString data) { - m_pageController->replaceStartPage(); - m_importController->extractConfigFromData(data); - m_pageController->goToPageViewConfig(); - }); -#endif - -#ifdef Q_OS_IOS - IosController::Instance()->initialize(); - connect(IosController::Instance(), &IosController::importConfigFromOutside, [this](QString data) { - m_pageController->replaceStartPage(); - m_importController->extractConfigFromData(data); - m_pageController->goToPageViewConfig(); - }); - - connect(IosController::Instance(), &IosController::importBackupFromOutside, [this](QString filePath) { - m_pageController->replaceStartPage(); - m_pageController->goToPageSettingsBackup(); - m_settingsController->importBackupFromOutside(filePath); - }); -#endif - - m_notificationHandler.reset(NotificationHandler::create(nullptr)); - - connect(m_vpnConnection.get(), &VpnConnection::connectionStateChanged, m_notificationHandler.get(), - &NotificationHandler::setConnectionState); - - connect(m_notificationHandler.get(), &NotificationHandler::raiseRequested, m_pageController.get(), - &PageController::raiseMainWindow); - connect(m_notificationHandler.get(), &NotificationHandler::connectRequested, m_connectionController.get(), - &ConnectionController::openConnection); - connect(m_notificationHandler.get(), &NotificationHandler::disconnectRequested, m_connectionController.get(), - &ConnectionController::closeConnection); - connect(this, &AmneziaApplication::translationsUpdated, m_notificationHandler.get(), - &NotificationHandler::onTranslationsUpdated); - - m_engine->load(url); - m_systemController->setQmlRoot(m_engine->rootObjects().value(0)); - - if (m_settings->isSaveLogs()) { - if (!Logger::init()) { - qWarning() << "Initialization of debug subsystem failed"; - } - } - -#ifdef Q_OS_WIN - if (m_parser.isSet("a")) - m_pageController->showOnStartup(); - else - emit m_pageController->raiseMainWindow(); -#else - m_pageController->showOnStartup(); -#endif - - // TODO - fix -#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) - if (isPrimary()) { - QObject::connect(this, &SingleApplication::instanceStarted, m_pageController.get(), [this]() { - qDebug() << "Secondary instance started, showing this window instead"; - emit m_pageController->raiseMainWindow(); - }); - } -#endif - -// Android TextField clipboard workaround -// https://bugreports.qt.io/browse/QTBUG-113461 -#ifdef Q_OS_ANDROID - QObject::connect(qApp, &QGuiApplication::applicationStateChanged, [](Qt::ApplicationState state) { - if (state == Qt::ApplicationActive) { - if (qApp->clipboard()->mimeData()->formats().contains("text/html")) { - QTextDocument doc; - doc.setHtml(qApp->clipboard()->mimeData()->html()); - qApp->clipboard()->setText(doc.toPlainText()); - } - } - }); -#endif -} - -void AmneziaApplication::registerTypes() -{ - qRegisterMetaType("ServerCredentials"); - - qRegisterMetaType("DockerContainer"); - qRegisterMetaType("TransportProto"); - qRegisterMetaType("Proto"); - qRegisterMetaType("ServiceType"); - - declareQmlProtocolEnum(); - declareQmlContainerEnum(); - - qmlRegisterType("QRCodeReader", 1, 0, "QRCodeReader"); - - m_containerProps.reset(new ContainerProps()); - qmlRegisterSingletonInstance("ContainerProps", 1, 0, "ContainerProps", m_containerProps.get()); - - m_protocolProps.reset(new ProtocolProps()); - qmlRegisterSingletonInstance("ProtocolProps", 1, 0, "ProtocolProps", m_protocolProps.get()); - - qmlRegisterSingletonType(QUrl("qrc:/ui/qml/Filters/ContainersModelFilters.qml"), "ContainersModelFilters", 1, 0, - "ContainersModelFilters"); - - // - Vpn::declareQmlVpnConnectionStateEnum(); - PageLoader::declareQmlPageEnum(); -} - -void AmneziaApplication::loadFonts() -{ - QQuickStyle::setStyle("Basic"); - - QFontDatabase::addApplicationFont(":/fonts/pt-root-ui_vf.ttf"); -} - -void AmneziaApplication::loadTranslator() -{ - auto locale = m_settings->getAppLanguage(); - m_translator.reset(new QTranslator()); - updateTranslator(locale); -} - -void AmneziaApplication::updateTranslator(const QLocale &locale) -{ - if (!m_translator->isEmpty()) { - QCoreApplication::removeTranslator(m_translator.get()); - } - - QString strFileName = QString(":/translations/amneziavpn") + QLatin1String("_") + locale.name() + ".qm"; - if (m_translator->load(strFileName)) { - if (QCoreApplication::installTranslator(m_translator.get())) { - m_settings->setAppLanguage(locale); - } - } else { - m_settings->setAppLanguage(QLocale::English); - } - - m_engine->retranslate(); - - emit translationsUpdated(); -} - -bool AmneziaApplication::parseCommands() -{ - m_parser.setApplicationDescription(APPLICATION_NAME); - m_parser.addHelpOption(); - m_parser.addVersionOption(); - - QCommandLineOption c_autostart { { "a", "autostart" }, "System autostart" }; - m_parser.addOption(c_autostart); - - QCommandLineOption c_cleanup { { "c", "cleanup" }, "Cleanup logs" }; - m_parser.addOption(c_cleanup); - - m_parser.process(*this); - - if (m_parser.isSet(c_cleanup)) { - Logger::cleanUp(); - QTimer::singleShot(100, this, [this] { quit(); }); - exec(); - return false; - } - return true; -} - -QQmlApplicationEngine *AmneziaApplication::qmlEngine() const -{ - return m_engine; -} - -void AmneziaApplication::initModels() -{ - m_containersModel.reset(new ContainersModel(this)); - m_engine->rootContext()->setContextProperty("ContainersModel", m_containersModel.get()); - - m_serversModel.reset(new ServersModel(m_settings, this)); - m_engine->rootContext()->setContextProperty("ServersModel", m_serversModel.get()); - connect(m_serversModel.get(), &ServersModel::containersUpdated, m_containersModel.get(), - &ContainersModel::updateModel); - connect(m_serversModel.get(), &ServersModel::defaultContainerChanged, m_containersModel.get(), - &ContainersModel::setDefaultContainer); - m_containersModel->setDefaultContainer(m_serversModel->getDefaultContainer()); // make better? - - m_languageModel.reset(new LanguageModel(m_settings, this)); - m_engine->rootContext()->setContextProperty("LanguageModel", m_languageModel.get()); - connect(m_languageModel.get(), &LanguageModel::updateTranslations, this, &AmneziaApplication::updateTranslator); - connect(this, &AmneziaApplication::translationsUpdated, m_languageModel.get(), &LanguageModel::translationsUpdated); - - m_sitesModel.reset(new SitesModel(m_settings, this)); - m_engine->rootContext()->setContextProperty("SitesModel", m_sitesModel.get()); - - m_protocolsModel.reset(new ProtocolsModel(m_settings, this)); - m_engine->rootContext()->setContextProperty("ProtocolsModel", m_protocolsModel.get()); - - m_openVpnConfigModel.reset(new OpenVpnConfigModel(this)); - m_engine->rootContext()->setContextProperty("OpenVpnConfigModel", m_openVpnConfigModel.get()); - - m_shadowSocksConfigModel.reset(new ShadowSocksConfigModel(this)); - m_engine->rootContext()->setContextProperty("ShadowSocksConfigModel", m_shadowSocksConfigModel.get()); - - m_cloakConfigModel.reset(new CloakConfigModel(this)); - m_engine->rootContext()->setContextProperty("CloakConfigModel", m_cloakConfigModel.get()); - - m_wireGuardConfigModel.reset(new WireGuardConfigModel(this)); - m_engine->rootContext()->setContextProperty("WireGuardConfigModel", m_wireGuardConfigModel.get()); - - m_awgConfigModel.reset(new AwgConfigModel(this)); - m_engine->rootContext()->setContextProperty("AwgConfigModel", m_awgConfigModel.get()); - -#ifdef Q_OS_WINDOWS - m_ikev2ConfigModel.reset(new Ikev2ConfigModel(this)); - m_engine->rootContext()->setContextProperty("Ikev2ConfigModel", m_ikev2ConfigModel.get()); -#endif - - m_sftpConfigModel.reset(new SftpConfigModel(this)); - m_engine->rootContext()->setContextProperty("SftpConfigModel", m_sftpConfigModel.get()); - - m_clientManagementModel.reset(new ClientManagementModel(m_settings, this)); - m_engine->rootContext()->setContextProperty("ClientManagementModel", m_clientManagementModel.get()); - - connect(m_configurator.get(), &VpnConfigurator::newVpnConfigCreated, this, - [this](const QString &clientId, const QString &clientName, const DockerContainer container, - ServerCredentials credentials) { - m_serversModel->reloadContainerConfig(); - m_clientManagementModel->appendClient(clientId, clientName, container, credentials); - }); -} - -void AmneziaApplication::initControllers() -{ - m_connectionController.reset(new ConnectionController(m_serversModel, m_containersModel, m_vpnConnection)); - m_engine->rootContext()->setContextProperty("ConnectionController", m_connectionController.get()); - - connect(this, &AmneziaApplication::translationsUpdated, m_connectionController.get(), - &ConnectionController::onTranslationsUpdated); - - m_pageController.reset(new PageController(m_serversModel, m_settings)); - m_engine->rootContext()->setContextProperty("PageController", m_pageController.get()); - - m_installController.reset(new InstallController(m_serversModel, m_containersModel, m_protocolsModel, m_settings)); - m_engine->rootContext()->setContextProperty("InstallController", m_installController.get()); - connect(m_installController.get(), &InstallController::passphraseRequestStarted, m_pageController.get(), - &PageController::showPassphraseRequestDrawer); - connect(m_pageController.get(), &PageController::passphraseRequestDrawerClosed, m_installController.get(), - &InstallController::setEncryptedPassphrase); - connect(m_installController.get(), &InstallController::currentContainerUpdated, m_connectionController.get(), - &ConnectionController::onCurrentContainerUpdated); - - m_importController.reset(new ImportController(m_serversModel, m_containersModel, m_settings)); - m_engine->rootContext()->setContextProperty("ImportController", m_importController.get()); - - m_exportController.reset(new ExportController(m_serversModel, m_containersModel, m_clientManagementModel, - m_settings, m_configurator)); - m_engine->rootContext()->setContextProperty("ExportController", m_exportController.get()); - - m_settingsController.reset(new SettingsController(m_serversModel, m_containersModel, m_languageModel, m_settings)); - m_engine->rootContext()->setContextProperty("SettingsController", m_settingsController.get()); - if (m_settingsController->isAutoConnectEnabled() && m_serversModel->getDefaultServerIndex() >= 0) { - QTimer::singleShot(1000, this, [this]() { m_connectionController->openConnection(); }); - } - connect(m_settingsController.get(), &SettingsController::amneziaDnsToggled , m_serversModel.get(), - &ServersModel::toggleAmneziaDns); - - m_sitesController.reset(new SitesController(m_settings, m_vpnConnection, m_sitesModel)); - m_engine->rootContext()->setContextProperty("SitesController", m_sitesController.get()); - - m_systemController.reset(new SystemController(m_settings)); - m_engine->rootContext()->setContextProperty("SystemController", m_systemController.get()); - - m_cloudController.reset(new ApiController(m_serversModel, m_containersModel)); - m_engine->rootContext()->setContextProperty("ApiController", m_cloudController.get()); -} +#include "amnezia_application.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "logger.h" +#include "version.h" + +#include "platforms/ios/QRCodeReaderBase.h" +#if defined(Q_OS_ANDROID) + #include "platforms/android/android_controller.h" +#endif + +#include "protocols/qml_register_protocols.h" + +#if defined(Q_OS_IOS) + #include "platforms/ios/ios_controller.h" +#endif + +#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) +AmneziaApplication::AmneziaApplication(int &argc, char *argv[]) : AMNEZIA_BASE_CLASS(argc, argv) +#else +AmneziaApplication::AmneziaApplication(int &argc, char *argv[], bool allowSecondary, SingleApplication::Options options, + int timeout, const QString &userData) + : SingleApplication(argc, argv, allowSecondary, options, timeout, userData) +#endif +{ + setQuitOnLastWindowClosed(false); + + // Fix config file permissions +#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) + { + QSettings s(ORGANIZATION_NAME, APPLICATION_NAME); + s.setValue("permFixed", true); + } + + QString configLoc1 = QStandardPaths::standardLocations(QStandardPaths::ConfigLocation).first() + "/" + + ORGANIZATION_NAME + "/" + APPLICATION_NAME + ".conf"; + QFile::setPermissions(configLoc1, QFileDevice::ReadOwner | QFileDevice::WriteOwner); + + QString configLoc2 = QStandardPaths::standardLocations(QStandardPaths::ConfigLocation).first() + "/" + + ORGANIZATION_NAME + "/" + APPLICATION_NAME + "/" + APPLICATION_NAME + ".conf"; + QFile::setPermissions(configLoc2, QFileDevice::ReadOwner | QFileDevice::WriteOwner); +#endif + + m_settings = std::shared_ptr(new Settings); +} + +AmneziaApplication::~AmneziaApplication() +{ + m_vpnConnectionThread.quit(); + m_vpnConnectionThread.wait(3000); + + if (m_engine) { + QObject::disconnect(m_engine, 0, 0, 0); + delete m_engine; + } +} + +void AmneziaApplication::init() +{ + m_engine = new QQmlApplicationEngine; + + const QUrl url(QStringLiteral("qrc:/ui/qml/main2.qml")); + QObject::connect( + m_engine, &QQmlApplicationEngine::objectCreated, this, + [url](QObject *obj, const QUrl &objUrl) { + if (!obj && url == objUrl) + QCoreApplication::exit(-1); + }, + Qt::QueuedConnection); + + m_engine->rootContext()->setContextProperty("Debug", &Logger::Instance()); + + m_configurator = std::shared_ptr(new VpnConfigurator(m_settings, this)); + m_vpnConnection.reset(new VpnConnection(m_settings, m_configurator)); + m_vpnConnection->moveToThread(&m_vpnConnectionThread); + m_vpnConnectionThread.start(); + + initModels(); + loadTranslator(); + initControllers(); + +#ifdef Q_OS_ANDROID + if(!AndroidController::initLogging()) { + qFatal("Android logging initialization failed"); + } + AndroidController::instance()->setSaveLogs(m_settings->isSaveLogs()); + connect(m_settings.get(), &Settings::saveLogsChanged, + AndroidController::instance(), &AndroidController::setSaveLogs); + + connect(AndroidController::instance(), &AndroidController::initConnectionState, this, + [this](Vpn::ConnectionState state) { + m_connectionController->onConnectionStateChanged(state); + if (m_vpnConnection) + m_vpnConnection->restoreConnection(); + }); + if (!AndroidController::instance()->initialize()) { + qFatal("Android controller initialization failed"); + } + + connect(AndroidController::instance(), &AndroidController::importConfigFromOutside, [this](QString data) { + m_pageController->replaceStartPage(); + m_importController->extractConfigFromData(data); + m_pageController->goToPageViewConfig(); + }); +#endif + +#ifdef Q_OS_IOS + IosController::Instance()->initialize(); + connect(IosController::Instance(), &IosController::importConfigFromOutside, [this](QString data) { + m_pageController->replaceStartPage(); + m_importController->extractConfigFromData(data); + m_pageController->goToPageViewConfig(); + }); + + connect(IosController::Instance(), &IosController::importBackupFromOutside, [this](QString filePath) { + m_pageController->replaceStartPage(); + m_pageController->goToPageSettingsBackup(); + m_settingsController->importBackupFromOutside(filePath); + }); +#endif + + m_notificationHandler.reset(NotificationHandler::create(nullptr)); + + connect(m_vpnConnection.get(), &VpnConnection::connectionStateChanged, m_notificationHandler.get(), + &NotificationHandler::setConnectionState); + + connect(m_notificationHandler.get(), &NotificationHandler::raiseRequested, m_pageController.get(), + &PageController::raiseMainWindow); + connect(m_notificationHandler.get(), &NotificationHandler::connectRequested, m_connectionController.get(), + &ConnectionController::openConnection); + connect(m_notificationHandler.get(), &NotificationHandler::disconnectRequested, m_connectionController.get(), + &ConnectionController::closeConnection); + connect(this, &AmneziaApplication::translationsUpdated, m_notificationHandler.get(), + &NotificationHandler::onTranslationsUpdated); + + m_engine->load(url); + m_systemController->setQmlRoot(m_engine->rootObjects().value(0)); + +#ifndef Q_OS_ANDROID + if (m_settings->isSaveLogs()) { + if (!Logger::init()) { + qWarning() << "Initialization of debug subsystem failed"; + } + } +#endif + +#ifdef Q_OS_WIN + if (m_parser.isSet("a")) + m_pageController->showOnStartup(); + else + emit m_pageController->raiseMainWindow(); +#else + m_pageController->showOnStartup(); +#endif + + // TODO - fix +#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) + if (isPrimary()) { + QObject::connect(this, &SingleApplication::instanceStarted, m_pageController.get(), [this]() { + qDebug() << "Secondary instance started, showing this window instead"; + emit m_pageController->raiseMainWindow(); + }); + } +#endif + +// Android TextArea clipboard workaround +// Text from TextArea always has "text/html" mime-type: +// /qt/6.6.1/Src/qtdeclarative/src/quick/items/qquicktextcontrol.cpp:1865 +// Next, html is created for this mime-type: +// /qt/6.6.1/Src/qtdeclarative/src/quick/items/qquicktextcontrol.cpp:1885 +// And this html goes to the Androids clipboard, i.e. text from TextArea is always copied as richText: +// /qt/6.6.1/Src/qtbase/src/plugins/platforms/android/androidjniclipboard.cpp:46 +// So we catch all the copies to the clipboard and clear them from "text/html" +#ifdef Q_OS_ANDROID + connect(QGuiApplication::clipboard(), &QClipboard::dataChanged, []() { + auto clipboard = QGuiApplication::clipboard(); + if (clipboard->mimeData()->hasHtml()) { + clipboard->setText(clipboard->text()); + } + }); +#endif +} + +void AmneziaApplication::registerTypes() +{ + qRegisterMetaType("ServerCredentials"); + + qRegisterMetaType("DockerContainer"); + qRegisterMetaType("TransportProto"); + qRegisterMetaType("Proto"); + qRegisterMetaType("ServiceType"); + + declareQmlProtocolEnum(); + declareQmlContainerEnum(); + + qmlRegisterType("QRCodeReader", 1, 0, "QRCodeReader"); + + m_containerProps.reset(new ContainerProps()); + qmlRegisterSingletonInstance("ContainerProps", 1, 0, "ContainerProps", m_containerProps.get()); + + m_protocolProps.reset(new ProtocolProps()); + qmlRegisterSingletonInstance("ProtocolProps", 1, 0, "ProtocolProps", m_protocolProps.get()); + + qmlRegisterSingletonType(QUrl("qrc:/ui/qml/Filters/ContainersModelFilters.qml"), "ContainersModelFilters", 1, 0, + "ContainersModelFilters"); + + // + Vpn::declareQmlVpnConnectionStateEnum(); + PageLoader::declareQmlPageEnum(); +} + +void AmneziaApplication::loadFonts() +{ + QQuickStyle::setStyle("Basic"); + + QFontDatabase::addApplicationFont(":/fonts/pt-root-ui_vf.ttf"); +} + +void AmneziaApplication::loadTranslator() +{ + auto locale = m_settings->getAppLanguage(); + m_translator.reset(new QTranslator()); + updateTranslator(locale); +} + +void AmneziaApplication::updateTranslator(const QLocale &locale) +{ + if (!m_translator->isEmpty()) { + QCoreApplication::removeTranslator(m_translator.get()); + } + + QString strFileName = QString(":/translations/amneziavpn") + QLatin1String("_") + locale.name() + ".qm"; + if (m_translator->load(strFileName)) { + if (QCoreApplication::installTranslator(m_translator.get())) { + m_settings->setAppLanguage(locale); + } + } else { + m_settings->setAppLanguage(QLocale::English); + } + + m_engine->retranslate(); + + emit translationsUpdated(); +} + +bool AmneziaApplication::parseCommands() +{ + m_parser.setApplicationDescription(APPLICATION_NAME); + m_parser.addHelpOption(); + m_parser.addVersionOption(); + + QCommandLineOption c_autostart { { "a", "autostart" }, "System autostart" }; + m_parser.addOption(c_autostart); + + QCommandLineOption c_cleanup { { "c", "cleanup" }, "Cleanup logs" }; + m_parser.addOption(c_cleanup); + + m_parser.process(*this); + + if (m_parser.isSet(c_cleanup)) { + Logger::cleanUp(); + QTimer::singleShot(100, this, [this] { quit(); }); + exec(); + return false; + } + return true; +} + +QQmlApplicationEngine *AmneziaApplication::qmlEngine() const +{ + return m_engine; +} + +void AmneziaApplication::initModels() +{ + m_containersModel.reset(new ContainersModel(this)); + m_engine->rootContext()->setContextProperty("ContainersModel", m_containersModel.get()); + + m_serversModel.reset(new ServersModel(m_settings, this)); + m_engine->rootContext()->setContextProperty("ServersModel", m_serversModel.get()); + connect(m_serversModel.get(), &ServersModel::containersUpdated, m_containersModel.get(), + &ContainersModel::updateModel); + connect(m_serversModel.get(), &ServersModel::defaultContainerChanged, m_containersModel.get(), + &ContainersModel::setDefaultContainer); + m_containersModel->setDefaultContainer(m_serversModel->getDefaultContainer()); // make better? + + m_languageModel.reset(new LanguageModel(m_settings, this)); + m_engine->rootContext()->setContextProperty("LanguageModel", m_languageModel.get()); + connect(m_languageModel.get(), &LanguageModel::updateTranslations, this, &AmneziaApplication::updateTranslator); + connect(this, &AmneziaApplication::translationsUpdated, m_languageModel.get(), &LanguageModel::translationsUpdated); + + m_sitesModel.reset(new SitesModel(m_settings, this)); + m_engine->rootContext()->setContextProperty("SitesModel", m_sitesModel.get()); + + m_protocolsModel.reset(new ProtocolsModel(m_settings, this)); + m_engine->rootContext()->setContextProperty("ProtocolsModel", m_protocolsModel.get()); + + m_openVpnConfigModel.reset(new OpenVpnConfigModel(this)); + m_engine->rootContext()->setContextProperty("OpenVpnConfigModel", m_openVpnConfigModel.get()); + + m_shadowSocksConfigModel.reset(new ShadowSocksConfigModel(this)); + m_engine->rootContext()->setContextProperty("ShadowSocksConfigModel", m_shadowSocksConfigModel.get()); + + m_cloakConfigModel.reset(new CloakConfigModel(this)); + m_engine->rootContext()->setContextProperty("CloakConfigModel", m_cloakConfigModel.get()); + + m_wireGuardConfigModel.reset(new WireGuardConfigModel(this)); + m_engine->rootContext()->setContextProperty("WireGuardConfigModel", m_wireGuardConfigModel.get()); + + m_awgConfigModel.reset(new AwgConfigModel(this)); + m_engine->rootContext()->setContextProperty("AwgConfigModel", m_awgConfigModel.get()); + +#ifdef Q_OS_WINDOWS + m_ikev2ConfigModel.reset(new Ikev2ConfigModel(this)); + m_engine->rootContext()->setContextProperty("Ikev2ConfigModel", m_ikev2ConfigModel.get()); +#endif + + m_sftpConfigModel.reset(new SftpConfigModel(this)); + m_engine->rootContext()->setContextProperty("SftpConfigModel", m_sftpConfigModel.get()); + + m_clientManagementModel.reset(new ClientManagementModel(m_settings, this)); + m_engine->rootContext()->setContextProperty("ClientManagementModel", m_clientManagementModel.get()); + connect(m_clientManagementModel.get(), &ClientManagementModel::adminConfigRevoked, + m_serversModel.get(), &ServersModel::clearCachedProfile); + + connect(m_configurator.get(), &VpnConfigurator::newVpnConfigCreated, this, + [this](const QString &clientId, const QString &clientName, const DockerContainer container, + ServerCredentials credentials) { + m_serversModel->reloadContainerConfig(); + m_clientManagementModel->appendClient(clientId, clientName, container, credentials); + emit m_configurator->clientModelUpdated(); + }); +} + +void AmneziaApplication::initControllers() +{ + m_connectionController.reset(new ConnectionController(m_serversModel, m_containersModel, m_vpnConnection)); + m_engine->rootContext()->setContextProperty("ConnectionController", m_connectionController.get()); + + connect(this, &AmneziaApplication::translationsUpdated, m_connectionController.get(), + &ConnectionController::onTranslationsUpdated); + + m_pageController.reset(new PageController(m_serversModel, m_settings)); + m_engine->rootContext()->setContextProperty("PageController", m_pageController.get()); + + m_installController.reset(new InstallController(m_serversModel, m_containersModel, m_protocolsModel, m_settings)); + m_engine->rootContext()->setContextProperty("InstallController", m_installController.get()); + connect(m_installController.get(), &InstallController::passphraseRequestStarted, m_pageController.get(), + &PageController::showPassphraseRequestDrawer); + connect(m_pageController.get(), &PageController::passphraseRequestDrawerClosed, m_installController.get(), + &InstallController::setEncryptedPassphrase); + connect(m_installController.get(), &InstallController::currentContainerUpdated, m_connectionController.get(), + &ConnectionController::onCurrentContainerUpdated); + + m_importController.reset(new ImportController(m_serversModel, m_containersModel, m_settings)); + m_engine->rootContext()->setContextProperty("ImportController", m_importController.get()); + + m_exportController.reset(new ExportController(m_serversModel, m_containersModel, m_clientManagementModel, + m_settings, m_configurator)); + m_engine->rootContext()->setContextProperty("ExportController", m_exportController.get()); + + m_settingsController.reset(new SettingsController(m_serversModel, m_containersModel, m_languageModel, m_sitesModel, m_settings)); + m_engine->rootContext()->setContextProperty("SettingsController", m_settingsController.get()); + if (m_settingsController->isAutoConnectEnabled() && m_serversModel->getDefaultServerIndex() >= 0) { + QTimer::singleShot(1000, this, [this]() { m_connectionController->openConnection(); }); + } + connect(m_settingsController.get(), &SettingsController::amneziaDnsToggled , m_serversModel.get(), + &ServersModel::toggleAmneziaDns); + + m_sitesController.reset(new SitesController(m_settings, m_vpnConnection, m_sitesModel)); + m_engine->rootContext()->setContextProperty("SitesController", m_sitesController.get()); + + m_systemController.reset(new SystemController(m_settings)); + m_engine->rootContext()->setContextProperty("SystemController", m_systemController.get()); + + m_cloudController.reset(new ApiController(m_serversModel, m_containersModel)); + m_engine->rootContext()->setContextProperty("ApiController", m_cloudController.get()); +} diff --git a/client/amnezia_application.h b/client/amnezia_application.h index aff853a6d..52427281f 100644 --- a/client/amnezia_application.h +++ b/client/amnezia_application.h @@ -1,127 +1,127 @@ -#ifndef AMNEZIA_APPLICATION_H -#define AMNEZIA_APPLICATION_H - -#include -#include -#include -#include -#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) - #include -#else - #include -#endif - -#include "settings.h" -#include "vpnconnection.h" - -#include "configurators/vpn_configurator.h" - -#include "ui/controllers/connectionController.h" -#include "ui/controllers/exportController.h" -#include "ui/controllers/importController.h" -#include "ui/controllers/installController.h" -#include "ui/controllers/pageController.h" -#include "ui/controllers/settingsController.h" -#include "ui/controllers/sitesController.h" -#include "ui/controllers/systemController.h" -#include "ui/controllers/apiController.h" -#include "ui/models/containers_model.h" -#include "ui/models/languageModel.h" -#include "ui/models/protocols/cloakConfigModel.h" -#include "ui/notificationhandler.h" -#ifdef Q_OS_WINDOWS - #include "ui/models/protocols/ikev2ConfigModel.h" -#endif -#include "ui/models/protocols/awgConfigModel.h" -#include "ui/models/protocols/openvpnConfigModel.h" -#include "ui/models/protocols/shadowsocksConfigModel.h" -#include "ui/models/protocols/wireguardConfigModel.h" -#include "ui/models/protocols_model.h" -#include "ui/models/servers_model.h" -#include "ui/models/services/sftpConfigModel.h" -#include "ui/models/sites_model.h" -#include "ui/models/clientManagementModel.h" - -#define amnApp (static_cast(QCoreApplication::instance())) - -#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) - #define AMNEZIA_BASE_CLASS QGuiApplication -#else - #define AMNEZIA_BASE_CLASS SingleApplication - #define QAPPLICATION_CLASS QApplication - #include "singleapplication.h" -#endif - -class AmneziaApplication : public AMNEZIA_BASE_CLASS -{ - Q_OBJECT -public: -#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) - AmneziaApplication(int &argc, char *argv[]); -#else - AmneziaApplication(int &argc, char *argv[], bool allowSecondary = false, - SingleApplication::Options options = SingleApplication::User, int timeout = 1000, - const QString &userData = {}); -#endif - virtual ~AmneziaApplication(); - - void init(); - void registerTypes(); - void loadFonts(); - void loadTranslator(); - void updateTranslator(const QLocale &locale); - bool parseCommands(); - - QQmlApplicationEngine *qmlEngine() const; - -signals: - void translationsUpdated(); - -private: - void initModels(); - void initControllers(); - - QQmlApplicationEngine *m_engine {}; - std::shared_ptr m_settings; - std::shared_ptr m_configurator; - - QSharedPointer m_containerProps; - QSharedPointer m_protocolProps; - - QSharedPointer m_translator; - QCommandLineParser m_parser; - - QSharedPointer m_containersModel; - QSharedPointer m_serversModel; - QSharedPointer m_languageModel; - QSharedPointer m_protocolsModel; - QSharedPointer m_sitesModel; - QSharedPointer m_clientManagementModel; - - QScopedPointer m_openVpnConfigModel; - QScopedPointer m_shadowSocksConfigModel; - QScopedPointer m_cloakConfigModel; - QScopedPointer m_wireGuardConfigModel; - QScopedPointer m_awgConfigModel; -#ifdef Q_OS_WINDOWS - QScopedPointer m_ikev2ConfigModel; -#endif - - QScopedPointer m_sftpConfigModel; - - QSharedPointer m_vpnConnection; - QThread m_vpnConnectionThread; - QScopedPointer m_notificationHandler; - - QScopedPointer m_connectionController; - QScopedPointer m_pageController; - QScopedPointer m_installController; - QScopedPointer m_importController; - QScopedPointer m_exportController; - QScopedPointer m_settingsController; - QScopedPointer m_sitesController; - QScopedPointer m_systemController; - QScopedPointer m_cloudController; -}; - -#endif // AMNEZIA_APPLICATION_H +#ifndef AMNEZIA_APPLICATION_H +#define AMNEZIA_APPLICATION_H + +#include +#include +#include +#include +#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) + #include +#else + #include +#endif + +#include "settings.h" +#include "vpnconnection.h" + +#include "configurators/vpn_configurator.h" + +#include "ui/controllers/connectionController.h" +#include "ui/controllers/exportController.h" +#include "ui/controllers/importController.h" +#include "ui/controllers/installController.h" +#include "ui/controllers/pageController.h" +#include "ui/controllers/settingsController.h" +#include "ui/controllers/sitesController.h" +#include "ui/controllers/systemController.h" +#include "ui/controllers/apiController.h" +#include "ui/models/containers_model.h" +#include "ui/models/languageModel.h" +#include "ui/models/protocols/cloakConfigModel.h" +#include "ui/notificationhandler.h" +#ifdef Q_OS_WINDOWS + #include "ui/models/protocols/ikev2ConfigModel.h" +#endif +#include "ui/models/protocols/awgConfigModel.h" +#include "ui/models/protocols/openvpnConfigModel.h" +#include "ui/models/protocols/shadowsocksConfigModel.h" +#include "ui/models/protocols/wireguardConfigModel.h" +#include "ui/models/protocols_model.h" +#include "ui/models/servers_model.h" +#include "ui/models/services/sftpConfigModel.h" +#include "ui/models/sites_model.h" +#include "ui/models/clientManagementModel.h" + +#define amnApp (static_cast(QCoreApplication::instance())) + +#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) + #define AMNEZIA_BASE_CLASS QGuiApplication +#else + #define AMNEZIA_BASE_CLASS SingleApplication + #define QAPPLICATION_CLASS QApplication + #include "singleapplication.h" +#endif + +class AmneziaApplication : public AMNEZIA_BASE_CLASS +{ + Q_OBJECT +public: +#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) + AmneziaApplication(int &argc, char *argv[]); +#else + AmneziaApplication(int &argc, char *argv[], bool allowSecondary = false, + SingleApplication::Options options = SingleApplication::User, int timeout = 1000, + const QString &userData = {}); +#endif + virtual ~AmneziaApplication(); + + void init(); + void registerTypes(); + void loadFonts(); + void loadTranslator(); + void updateTranslator(const QLocale &locale); + bool parseCommands(); + + QQmlApplicationEngine *qmlEngine() const; + +signals: + void translationsUpdated(); + +private: + void initModels(); + void initControllers(); + + QQmlApplicationEngine *m_engine {}; + std::shared_ptr m_settings; + std::shared_ptr m_configurator; + + QSharedPointer m_containerProps; + QSharedPointer m_protocolProps; + + QSharedPointer m_translator; + QCommandLineParser m_parser; + + QSharedPointer m_containersModel; + QSharedPointer m_serversModel; + QSharedPointer m_languageModel; + QSharedPointer m_protocolsModel; + QSharedPointer m_sitesModel; + QSharedPointer m_clientManagementModel; + + QScopedPointer m_openVpnConfigModel; + QScopedPointer m_shadowSocksConfigModel; + QScopedPointer m_cloakConfigModel; + QScopedPointer m_wireGuardConfigModel; + QScopedPointer m_awgConfigModel; +#ifdef Q_OS_WINDOWS + QScopedPointer m_ikev2ConfigModel; +#endif + + QScopedPointer m_sftpConfigModel; + + QSharedPointer m_vpnConnection; + QThread m_vpnConnectionThread; + QScopedPointer m_notificationHandler; + + QScopedPointer m_connectionController; + QScopedPointer m_pageController; + QScopedPointer m_installController; + QScopedPointer m_importController; + QScopedPointer m_exportController; + QScopedPointer m_settingsController; + QScopedPointer m_sitesController; + QScopedPointer m_systemController; + QScopedPointer m_cloudController; +}; + +#endif // AMNEZIA_APPLICATION_H diff --git a/client/android/AndroidManifest.xml b/client/android/AndroidManifest.xml index 30b77f091..fb417f05f 100644 --- a/client/android/AndroidManifest.xml +++ b/client/android/AndroidManifest.xml @@ -1,6 +1,8 @@ - - @@ -32,13 +33,17 @@ android:label="-- %%INSERT_APP_NAME%% --" android:icon="@mipmap/icon" android:roundIcon="@mipmap/icon_round" - android:theme="@style/NoActionBar"> + android:theme="@style/NoActionBar" + android:fullBackupContent="@xml/backup_content" + android:dataExtractionRules="@xml/data_extraction_rules" + tools:targetApi="s"> @@ -147,7 +152,7 @@ android:authorities="org.amnezia.vpn.qtprovider" android:exported="false" android:grantUriPermissions="true"> - + diff --git a/client/android/awg/src/main/kotlin/AwgConfig.kt b/client/android/awg/src/main/kotlin/AwgConfig.kt index 372747f2c..014c6e0a4 100644 --- a/client/android/awg/src/main/kotlin/AwgConfig.kt +++ b/client/android/awg/src/main/kotlin/AwgConfig.kt @@ -99,7 +99,7 @@ class AwgConfig private constructor( fun setH3(h3: Long) = apply { this.h3 = h3 } fun setH4(h4: Long) = apply { this.h4 = h4 } - override fun build(): AwgConfig = AwgConfig(this) + override fun build(): AwgConfig = configBuild().run { AwgConfig(this@Builder) } } companion object { diff --git a/client/android/build.gradle.kts b/client/android/build.gradle.kts index 757989aa9..0a1fe83fe 100644 --- a/client/android/build.gradle.kts +++ b/client/android/build.gradle.kts @@ -108,7 +108,6 @@ dependencies { implementation(project(":cloak")) implementation(libs.androidx.core) implementation(libs.androidx.activity) - implementation(libs.androidx.security.crypto) implementation(libs.kotlinx.coroutines) implementation(libs.bundles.androidx.camera) implementation(libs.google.mlkit) diff --git a/client/android/cloak/src/main/kotlin/Cloak.kt b/client/android/cloak/src/main/kotlin/Cloak.kt index 5a5491308..651e353b4 100644 --- a/client/android/cloak/src/main/kotlin/Cloak.kt +++ b/client/android/cloak/src/main/kotlin/Cloak.kt @@ -3,6 +3,9 @@ package org.amnezia.vpn.protocol.cloak import android.util.Base64 import net.openvpn.ovpn3.ClientAPI_Config import org.amnezia.vpn.protocol.openvpn.OpenVpn +import org.amnezia.vpn.protocol.openvpn.OpenVpnConfig +import org.amnezia.vpn.util.net.InetNetwork +import org.amnezia.vpn.util.net.parseInetAddress import org.json.JSONObject /** @@ -51,6 +54,13 @@ class Cloak : OpenVpn() { return openVpnConfig } + override fun configPluggableTransport(configBuilder: OpenVpnConfig.Builder, config: JSONObject) { + // exclude remote server ip from vpn routes + val remoteServer = config.getString("hostName") + val remoteServerAddress = InetNetwork(parseInetAddress(remoteServer)) + configBuilder.excludeRoute(remoteServerAddress) + } + private fun checkCloakJson(cloakConfigJson: JSONObject): JSONObject { cloakConfigJson.put("NumConn", 1) cloakConfigJson.put("ProxyMethod", "openvpn") diff --git a/client/android/openvpn/src/main/kotlin/org/amnezia/vpn/protocol/openvpn/OpenVpn.kt b/client/android/openvpn/src/main/kotlin/org/amnezia/vpn/protocol/openvpn/OpenVpn.kt index 34069a0d4..9e1c62cc4 100644 --- a/client/android/openvpn/src/main/kotlin/org/amnezia/vpn/protocol/openvpn/OpenVpn.kt +++ b/client/android/openvpn/src/main/kotlin/org/amnezia/vpn/protocol/openvpn/OpenVpn.kt @@ -2,7 +2,6 @@ package org.amnezia.vpn.protocol.openvpn import android.content.Context import android.net.VpnService.Builder -import android.os.Build import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -14,7 +13,6 @@ import org.amnezia.vpn.protocol.ProtocolState import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED import org.amnezia.vpn.protocol.Statistics import org.amnezia.vpn.protocol.VpnStartException -import org.amnezia.vpn.util.net.InetNetwork import org.amnezia.vpn.util.net.getLocalNetworks import org.json.JSONObject @@ -79,16 +77,8 @@ open class OpenVpn : Protocol() { if (evalConfig.error) { throw BadConfigException("OpenVPN config parse error: ${evalConfig.message}") } - configBuilder.apply { - // fix for split tunneling - // The exclude split tunneling OpenVpn configuration does not contain a default route. - // It is required for split tunneling in newer versions of Android. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - addRoute(InetNetwork("0.0.0.0", 0)) - addRoute(InetNetwork("::", 0)) - } - configSplitTunneling(config) - } + configPluggableTransport(configBuilder, config) + configBuilder.configSplitTunneling(config) scope.launch { val status = client.connect() @@ -122,6 +112,8 @@ open class OpenVpn : Protocol() { return openVpnConfig } + protected open fun configPluggableTransport(configBuilder: OpenVpnConfig.Builder, config: JSONObject) {} + private fun makeEstablish(vpnBuilder: Builder): (OpenVpnConfig.Builder) -> Int = { configBuilder -> val openVpnConfig = configBuilder.build() buildVpnInterface(openVpnConfig, vpnBuilder) diff --git a/client/android/openvpn/src/main/kotlin/org/amnezia/vpn/protocol/openvpn/OpenVpnClient.kt b/client/android/openvpn/src/main/kotlin/org/amnezia/vpn/protocol/openvpn/OpenVpnClient.kt index c716a9708..4f0f17967 100644 --- a/client/android/openvpn/src/main/kotlin/org/amnezia/vpn/protocol/openvpn/OpenVpnClient.kt +++ b/client/android/openvpn/src/main/kotlin/org/amnezia/vpn/protocol/openvpn/OpenVpnClient.kt @@ -52,7 +52,7 @@ class OpenVpnClient( // Callback to construct a new tun builder // Should be called first. override fun tun_builder_new(): Boolean { - Log.v(TAG, "tun_builder_new") + Log.d(TAG, "tun_builder_new") configBuilder.clearAddresses() return true } @@ -60,7 +60,7 @@ class OpenVpnClient( // Callback to set MTU of the VPN interface // Never called more than once per tun_builder session. override fun tun_builder_set_mtu(mtu: Int): Boolean { - Log.v(TAG, "tun_builder_set_mtu: $mtu") + Log.d(TAG, "tun_builder_set_mtu: $mtu") configBuilder.setMtu(mtu) return true } @@ -71,7 +71,7 @@ class OpenVpnClient( address: String, prefix_length: Int, gateway: String, ipv6: Boolean, net30: Boolean ): Boolean { - Log.v(TAG, "tun_builder_add_address: $address, $prefix_length, $gateway, $ipv6, $net30") + Log.d(TAG, "tun_builder_add_address: $address, $prefix_length, $gateway, $ipv6, $net30") configBuilder.addAddress(InetNetwork(address, prefix_length)) return true } @@ -80,7 +80,7 @@ class OpenVpnClient( // May be called more than once per tun_builder session // metric is optional and should be ignored if < 0 override fun tun_builder_add_route(address: String, prefix_length: Int, metric: Int, ipv6: Boolean): Boolean { - Log.v(TAG, "tun_builder_add_route: $address, $prefix_length, $metric, $ipv6") + Log.d(TAG, "tun_builder_add_route: $address, $prefix_length, $metric, $ipv6") if (address == "remote_host") return false configBuilder.addRoute(InetNetwork(address, prefix_length)) return true @@ -90,10 +90,8 @@ class OpenVpnClient( // May be called more than once per tun_builder session // metric is optional and should be ignored if < 0 override fun tun_builder_exclude_route(address: String, prefix_length: Int, metric: Int, ipv6: Boolean): Boolean { - Log.v(TAG, "tun_builder_exclude_route: $address, $prefix_length, $metric, $ipv6") - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - configBuilder.excludeRoute(InetNetwork(address, prefix_length)) - } + Log.d(TAG, "tun_builder_exclude_route: $address, $prefix_length, $metric, $ipv6") + configBuilder.excludeRoute(InetNetwork(address, prefix_length)) return true } @@ -104,7 +102,7 @@ class OpenVpnClient( // domain should be routed. // Guaranteed to be called after tun_builder_reroute_gw. override fun tun_builder_add_dns_server(address: String, ipv6: Boolean): Boolean { - Log.v(TAG, "tun_builder_add_dns_server: $address, $ipv6") + Log.d(TAG, "tun_builder_add_dns_server: $address, $ipv6") configBuilder.addDnsServer(parseInetAddress(address)) return true } @@ -119,28 +117,28 @@ class OpenVpnClient( // ignored for that family // See also Android's VPNService.Builder.allowFamily method /* override fun tun_builder_set_allow_family(af: Int, allow: Boolean): Boolean { - Log.v(TAG, "tun_builder_set_allow_family: $af, $allow") + Log.d(TAG, "tun_builder_set_allow_family: $af, $allow") return true } */ // Callback to set address of remote server // Never called more than once per tun_builder session. override fun tun_builder_set_remote_address(address: String, ipv6: Boolean): Boolean { - Log.v(TAG, "tun_builder_set_remote_address: $address, $ipv6") + Log.d(TAG, "tun_builder_set_remote_address: $address, $ipv6") return true } // Optional callback that indicates OSI layer, should be 2 or 3. // Defaults to 3. override fun tun_builder_set_layer(layer: Int): Boolean { - Log.v(TAG, "tun_builder_set_layer: $layer") + Log.d(TAG, "tun_builder_set_layer: $layer") return layer == 3 } // Callback to set the session name // Never called more than once per tun_builder session. override fun tun_builder_set_session_name(name: String): Boolean { - Log.v(TAG, "tun_builder_set_session_name: $name") + Log.d(TAG, "tun_builder_set_session_name: $name") return true } @@ -149,7 +147,7 @@ class OpenVpnClient( // if the tunnel could not be established. // Always called last after tun_builder session has been configured. override fun tun_builder_establish(): Int { - Log.v(TAG, "tun_builder_establish") + Log.d(TAG, "tun_builder_establish") return establish(configBuilder) } @@ -159,7 +157,7 @@ class OpenVpnClient( // flags are defined in RGWFlags (rgwflags.hpp). // Never called more than once per tun_builder session. override fun tun_builder_reroute_gw(ipv4: Boolean, ipv6: Boolean, flags: Long): Boolean { - Log.v(TAG, "tun_builder_reroute_gw: $ipv4, $ipv6, $flags") + Log.d(TAG, "tun_builder_reroute_gw: $ipv4, $ipv6, $flags") if ((flags and EMULATED_EXCLUDE_ROUTES.toLong()) != 0L) return true if (ipv4) { configBuilder.addRoute(InetNetwork("0.0.0.0", 0)) @@ -176,7 +174,7 @@ class OpenVpnClient( // reroute_dns parameter. // Guaranteed to be called after tun_builder_reroute_gw. override fun tun_builder_add_search_domain(domain: String): Boolean { - Log.v(TAG, "tun_builder_add_search_domain: $domain") + Log.d(TAG, "tun_builder_add_search_domain: $domain") configBuilder.setSearchDomain(domain) return true } @@ -184,7 +182,7 @@ class OpenVpnClient( // Callback to set the HTTP proxy // Never called more than once per tun_builder session. override fun tun_builder_set_proxy_http(host: String, port: Int): Boolean { - Log.v(TAG, "tun_builder_set_proxy_http: $host, $port") + Log.d(TAG, "tun_builder_set_proxy_http: $host, $port") if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { try { configBuilder.setHttpProxy(ProxyInfo.buildDirectProxy(host, port)) @@ -199,7 +197,7 @@ class OpenVpnClient( // Callback to set the HTTPS proxy // Never called more than once per tun_builder session. override fun tun_builder_set_proxy_https(host: String, port: Int): Boolean { - Log.v(TAG, "tun_builder_set_proxy_https: $host, $port") + Log.d(TAG, "tun_builder_set_proxy_https: $host, $port") return false } @@ -208,7 +206,7 @@ class OpenVpnClient( // to exclude them from the VPN network are generated // This should be a list of CIDR networks (e.g. 192.168.0.0/24) override fun tun_builder_get_local_networks(ipv6: Boolean): ClientAPI_StringVec { - Log.v(TAG, "tun_builder_get_local_networks: $ipv6") + Log.d(TAG, "tun_builder_get_local_networks: $ipv6") val networks = ClientAPI_StringVec() for (address in getLocalNetworks(ipv6)) { networks.add(address.toString()) @@ -222,21 +220,21 @@ class OpenVpnClient( // tun_builder_reroute_gw. Route metric is ignored // if < 0. /* override fun tun_builder_set_route_metric_default(metric: Int): Boolean { - Log.v(TAG, "tun_builder_set_route_metric_default: $metric") + Log.d(TAG, "tun_builder_set_route_metric_default: $metric") return super.tun_builder_set_route_metric_default(metric) } */ // Callback to add a host which should bypass the proxy // May be called more than once per tun_builder session /* override fun tun_builder_add_proxy_bypass(bypass_host: String): Boolean { - Log.v(TAG, "tun_builder_add_proxy_bypass: $bypass_host") + Log.d(TAG, "tun_builder_add_proxy_bypass: $bypass_host") return super.tun_builder_add_proxy_bypass(bypass_host) } */ // Callback to set the proxy "Auto Config URL" // Never called more than once per tun_builder session. /* override fun tun_builder_set_proxy_auto_config_url(url: String): Boolean { - Log.v(TAG, "tun_builder_set_proxy_auto_config_url: $url") + Log.d(TAG, "tun_builder_set_proxy_auto_config_url: $url") return super.tun_builder_set_proxy_auto_config_url(url) } */ @@ -245,7 +243,7 @@ class OpenVpnClient( // May be called more than once per tun_builder session. // Guaranteed to be called after tun_builder_reroute_gw. /* override fun tun_builder_add_wins_server(address: String): Boolean { - Log.v(TAG, "tun_builder_add_wins_server: $address") + Log.d(TAG, "tun_builder_add_wins_server: $address") return super.tun_builder_add_wins_server(address) } */ @@ -254,7 +252,7 @@ class OpenVpnClient( // set the "Connection-specific DNS Suffix" property on // the TAP driver. /* override fun tun_builder_set_adapter_domain_suffix(name: String): Boolean { - Log.v(TAG, "tun_builder_set_adapter_domain_suffix: $name") + Log.d(TAG, "tun_builder_set_adapter_domain_suffix: $name") return super.tun_builder_set_adapter_domain_suffix(name) } */ @@ -266,13 +264,13 @@ class OpenVpnClient( // tun_builder_establish_lite() will be called. Otherwise, // tun_builder_establish() will be called. /* override fun tun_builder_persist(): Boolean { - Log.v(TAG, "tun_builder_persist") + Log.d(TAG, "tun_builder_persist") return super.tun_builder_persist() } */ // Indicates a reconnection with persisted tun state. /* override fun tun_builder_establish_lite() { - Log.v(TAG, "tun_builder_establish_lite") + Log.d(TAG, "tun_builder_establish_lite") super.tun_builder_establish_lite() } */ @@ -280,7 +278,7 @@ class OpenVpnClient( // If disconnect == true, then the teardown is occurring // prior to final disconnect. /* override fun tun_builder_teardown(disconnect: Boolean) { - Log.v(TAG, "tun_builder_teardown: $disconnect") + Log.d(TAG, "tun_builder_teardown: $disconnect") super.tun_builder_teardown(disconnect) } */ @@ -290,7 +288,7 @@ class OpenVpnClient( // Parse OpenVPN configuration file. override fun eval_config(arg0: ClientAPI_Config): ClientAPI_EvalConfig { - Log.v(TAG, "eval_config") + Log.d(TAG, "eval_config") return super.eval_config(arg0) } @@ -299,7 +297,7 @@ class OpenVpnClient( // to event() and log() functions. Make sure to call eval_config() // and possibly provide_creds() as well before this function. override fun connect(): ClientAPI_Status { - Log.v(TAG, "connect") + Log.d(TAG, "connect") return super.connect() } @@ -307,7 +305,7 @@ class OpenVpnClient( // Will be called from the thread executing connect(). // The remote and ipv6 are the remote host this socket will connect to override fun socket_protect(socket: Int, remote: String, ipv6: Boolean): Boolean { - Log.v(TAG, "socket_protect: $socket, $remote, $ipv6") + Log.d(TAG, "socket_protect: $socket, $remote, $ipv6") return protect(socket) } @@ -315,7 +313,7 @@ class OpenVpnClient( // May be called asynchronously from a different thread // when connect() is running. override fun stop() { - Log.v(TAG, "stop") + Log.d(TAG, "stop") super.stop() } @@ -323,21 +321,21 @@ class OpenVpnClient( // when network is down. May be called from a different thread // when connect() is running. override fun pause(reason: String) { - Log.v(TAG, "pause: $reason") + Log.d(TAG, "pause: $reason") super.pause(reason) } // Resume the client after it has been paused. May be called from a // different thread when connect() is running. override fun resume() { - Log.v(TAG, "resume") + Log.d(TAG, "resume") super.resume() } // Do a disconnect/reconnect cycle n seconds from now. May be called // from a different thread when connect() is running. override fun reconnect(seconds: Int) { - Log.v(TAG, "reconnect") + Log.d(TAG, "reconnect: $seconds") super.reconnect(seconds) } @@ -346,14 +344,14 @@ class OpenVpnClient( // CONNECTION_TIMEOUT event. If true, the core will enter a PAUSE // state. override fun pause_on_connection_timeout(): Boolean { - Log.v(TAG, "pause_on_connection_timeout") + Log.d(TAG, "pause_on_connection_timeout") return false } // Return information about the most recent connection. Should be called // after an event of type "CONNECTED". /* override fun connection_info(): ClientAPI_ConnectionInfo { - Log.v(TAG, "connection_info") + Log.d(TAG, "connection_info") return super.connection_info() } */ @@ -366,7 +364,7 @@ class OpenVpnClient( override fun event(event: ClientAPI_Event) { val name = event.name val info = event.info - Log.v(TAG, "OpenVpn event: $name: $info") + Log.d(TAG, "OpenVpn event: $name: $info") when (name) { "COMPRESSION_ENABLED", "WARN" -> Log.w(TAG, "$name: $info") "CONNECTED" -> state.value = CONNECTED @@ -398,31 +396,31 @@ class OpenVpnClient( // return transport stats only override fun transport_stats(): ClientAPI_TransportStats { - Log.v(TAG, "transport_stats") + Log.d(TAG, "transport_stats") return super.transport_stats() } // return a stats value, index should be >= 0 and < stats_n() /* override fun stats_value(index: Int): Long { - Log.v(TAG, "stats_value: $index") + Log.d(TAG, "stats_value: $index") return super.stats_value(index) } */ // return all stats in a bundle /* override fun stats_bundle(): ClientAPI_LLVector { - Log.v(TAG, "stats_bundle") + Log.d(TAG, "stats_bundle") return super.stats_bundle() } */ // return tun stats only /* override fun tun_stats(): ClientAPI_InterfaceStats { - Log.v(TAG, "tun_stats") + Log.d(TAG, "tun_stats") return super.tun_stats() } */ // post control channel message /* override fun post_cc_msg(msg: String) { - Log.v(TAG, "post_cc_msg: $msg") + Log.d(TAG, "post_cc_msg: $msg") super.post_cc_msg(msg) } */ } diff --git a/client/android/openvpn/src/main/kotlin/org/amnezia/vpn/protocol/openvpn/OpenVpnConfig.kt b/client/android/openvpn/src/main/kotlin/org/amnezia/vpn/protocol/openvpn/OpenVpnConfig.kt index 36d8d93b1..9554f9782 100644 --- a/client/android/openvpn/src/main/kotlin/org/amnezia/vpn/protocol/openvpn/OpenVpnConfig.kt +++ b/client/android/openvpn/src/main/kotlin/org/amnezia/vpn/protocol/openvpn/OpenVpnConfig.kt @@ -11,7 +11,7 @@ class OpenVpnConfig private constructor( class Builder : ProtocolConfig.Builder(false) { override var mtu: Int = OPENVPN_DEFAULT_MTU - override fun build(): OpenVpnConfig = OpenVpnConfig(this) + override fun build(): OpenVpnConfig = configBuild().run { OpenVpnConfig(this@Builder) } } companion object { diff --git a/client/android/protocolApi/src/main/kotlin/Protocol.kt b/client/android/protocolApi/src/main/kotlin/Protocol.kt index b2c52e9ad..b729f9f7b 100644 --- a/client/android/protocolApi/src/main/kotlin/Protocol.kt +++ b/client/android/protocolApi/src/main/kotlin/Protocol.kt @@ -14,8 +14,6 @@ import java.util.zip.ZipFile import kotlinx.coroutines.flow.MutableStateFlow import org.amnezia.vpn.util.Log import org.amnezia.vpn.util.net.InetNetwork -import org.amnezia.vpn.util.net.IpRange -import org.amnezia.vpn.util.net.IpRangeSet import org.json.JSONObject private const val TAG = "Protocol" @@ -53,40 +51,16 @@ abstract class Protocol { val splitTunnelType = config.optInt("splitTunnelType") if (splitTunnelType == SPLIT_TUNNEL_DISABLE) return val splitTunnelSites = config.getJSONArray("splitTunnelSites") - when (splitTunnelType) { - SPLIT_TUNNEL_INCLUDE -> { - // remove default routes, if any - removeRoute(InetNetwork("0.0.0.0", 0)) - removeRoute(InetNetwork("::", 0)) - // add routes from config - for (i in 0 until splitTunnelSites.length()) { - val address = InetNetwork.parse(splitTunnelSites.getString(i)) - addRoute(address) - } - } + val addressHandlerFunc = when (splitTunnelType) { + SPLIT_TUNNEL_INCLUDE -> ::includeAddress + SPLIT_TUNNEL_EXCLUDE -> ::excludeAddress - SPLIT_TUNNEL_EXCLUDE -> { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - // exclude routes from config - for (i in 0 until splitTunnelSites.length()) { - val address = InetNetwork.parse(splitTunnelSites.getString(i)) - excludeRoute(address) - } - } else { - // For older versions of Android, build a list of subnets without excluded addresses - val ipRangeSet = IpRangeSet() - ipRangeSet.remove(IpRange("127.0.0.0", 8)) - for (i in 0 until splitTunnelSites.length()) { - val address = InetNetwork.parse(splitTunnelSites.getString(i)) - ipRangeSet.remove(IpRange(address)) - } - // remove default routes, if any - removeRoute(InetNetwork("0.0.0.0", 0)) - removeRoute(InetNetwork("::", 0)) - ipRangeSet.subnets().forEach(::addRoute) - addRoute(InetNetwork("2000::", 3)) - } - } + else -> throw BadConfigException("Unexpected value of the 'splitTunnelType' parameter: $splitTunnelType") + } + + for (i in 0 until splitTunnelSites.length()) { + val address = InetNetwork.parse(splitTunnelSites.getString(i)) + addressHandlerFunc(address) } } diff --git a/client/android/protocolApi/src/main/kotlin/ProtocolConfig.kt b/client/android/protocolApi/src/main/kotlin/ProtocolConfig.kt index a4d7683e1..75ba1abfe 100644 --- a/client/android/protocolApi/src/main/kotlin/ProtocolConfig.kt +++ b/client/android/protocolApi/src/main/kotlin/ProtocolConfig.kt @@ -5,6 +5,8 @@ import android.os.Build import androidx.annotation.RequiresApi import java.net.InetAddress import org.amnezia.vpn.util.net.InetNetwork +import org.amnezia.vpn.util.net.IpRange +import org.amnezia.vpn.util.net.IpRangeSet open class ProtocolConfig protected constructor( val addresses: Set, @@ -12,6 +14,8 @@ open class ProtocolConfig protected constructor( val searchDomain: String?, val routes: Set, val excludedRoutes: Set, + val includedAddresses: Set, + val excludedAddresses: Set, val excludedApplications: Set, val httpProxy: ProxyInfo?, val allowAllAF: Boolean, @@ -25,6 +29,8 @@ open class ProtocolConfig protected constructor( builder.searchDomain, builder.routes, builder.excludedRoutes, + builder.includedAddresses, + builder.excludedAddresses, builder.excludedApplications, builder.httpProxy, builder.allowAllAF, @@ -37,6 +43,8 @@ open class ProtocolConfig protected constructor( internal val dnsServers: MutableSet = hashSetOf() internal val routes: MutableSet = hashSetOf() internal val excludedRoutes: MutableSet = hashSetOf() + internal val includedAddresses: MutableSet = hashSetOf() + internal val excludedAddresses: MutableSet = hashSetOf() internal val excludedApplications: MutableSet = hashSetOf() internal var searchDomain: String? = null @@ -71,12 +79,15 @@ open class ProtocolConfig protected constructor( fun removeRoute(route: InetNetwork) = apply { this.routes.remove(route) } fun clearRoutes() = apply { this.routes.clear() } - @RequiresApi(Build.VERSION_CODES.TIRAMISU) fun excludeRoute(route: InetNetwork) = apply { this.excludedRoutes += route } - - @RequiresApi(Build.VERSION_CODES.TIRAMISU) fun excludeRoutes(routes: Collection) = apply { this.excludedRoutes += routes } + fun includeAddress(addr: InetNetwork) = apply { this.includedAddresses += addr } + fun includeAddresses(addresses: Collection) = apply { this.includedAddresses += addresses } + + fun excludeAddress(addr: InetNetwork) = apply { this.excludedAddresses += addr } + fun excludeAddresses(addresses: Collection) = apply { this.excludedAddresses += addresses } + fun excludeApplication(application: String) = apply { this.excludedApplications += application } fun excludeApplications(applications: Collection) = apply { this.excludedApplications += applications } @@ -91,6 +102,48 @@ open class ProtocolConfig protected constructor( fun setMtu(mtu: Int) = apply { this.mtu = mtu } + private fun processSplitTunneling() { + if (includedAddresses.isNotEmpty() && excludedAddresses.isNotEmpty()) { + throw BadConfigException("Config contains addresses for inclusive and exclusive split tunneling at the same time") + } + + if (includedAddresses.isNotEmpty()) { + // remove default routes, if any + removeRoute(InetNetwork("0.0.0.0", 0)) + removeRoute(InetNetwork("::", 0)) + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + // for older versions of Android, add the default route to the excluded routes + // to correctly build the excluded subnets list later + excludeRoute(InetNetwork("0.0.0.0", 0)) + } + addRoutes(includedAddresses) + } else if (excludedAddresses.isNotEmpty()) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + // default routes are required for split tunneling in newer versions of Android + addRoute(InetNetwork("0.0.0.0", 0)) + addRoute(InetNetwork("::", 0)) + } + excludeRoutes(excludedAddresses) + } + } + + private fun processExcludedRoutes() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + // for older versions of Android, build a list of subnets without excluded routes + // and add them to routes + val ipRangeSet = IpRangeSet() + ipRangeSet.remove(IpRange("127.0.0.0", 8)) + excludedRoutes.forEach { + ipRangeSet.remove(IpRange(it)) + } + // remove default routes, if any + removeRoute(InetNetwork("0.0.0.0", 0)) + removeRoute(InetNetwork("::", 0)) + ipRangeSet.subnets().forEach(::addRoute) + addRoute(InetNetwork("2000::", 3)) + } + } + private fun validate() { val errorMessage = StringBuilder() @@ -103,7 +156,13 @@ open class ProtocolConfig protected constructor( if (errorMessage.isNotEmpty()) throw BadConfigException(errorMessage.toString()) } - open fun build(): ProtocolConfig = validate().run { ProtocolConfig(this@Builder) } + protected fun configBuild() { + processSplitTunneling() + processExcludedRoutes() + validate() + } + + open fun build(): ProtocolConfig = configBuild().run { ProtocolConfig(this@Builder) } } companion object { diff --git a/client/android/res/xml/backup_content.xml b/client/android/res/xml/backup_content.xml new file mode 100644 index 000000000..33260809c --- /dev/null +++ b/client/android/res/xml/backup_content.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/client/android/res/xml/data_extraction_rules.xml b/client/android/res/xml/data_extraction_rules.xml new file mode 100644 index 000000000..f5f079cc4 --- /dev/null +++ b/client/android/res/xml/data_extraction_rules.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/client/android/src/org/amnezia/vpn/AmneziaActivity.kt b/client/android/src/org/amnezia/vpn/AmneziaActivity.kt index 11497274e..9a813626d 100644 --- a/client/android/src/org/amnezia/vpn/AmneziaActivity.kt +++ b/client/android/src/org/amnezia/vpn/AmneziaActivity.kt @@ -2,8 +2,10 @@ package org.amnezia.vpn import android.content.ComponentName import android.content.Intent +import android.content.Intent.EXTRA_MIME_TYPES import android.content.Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY import android.content.ServiceConnection +import android.content.pm.PackageManager import android.net.Uri import android.net.VpnService import android.os.Bundle @@ -12,11 +14,13 @@ import android.os.IBinder import android.os.Looper import android.os.Message import android.os.Messenger +import android.webkit.MimeTypeMap import android.widget.Toast import androidx.annotation.MainThread import androidx.core.content.ContextCompat import java.io.IOException import kotlin.LazyThreadSafetyMode.NONE +import kotlin.text.RegexOption.IGNORE_CASE import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -35,6 +39,7 @@ private const val TAG = "AmneziaActivity" private const val CHECK_VPN_PERMISSION_ACTION_CODE = 1 private const val CREATE_FILE_ACTION_CODE = 2 +private const val OPEN_FILE_ACTION_CODE = 3 private const val BIND_SERVICE_TIMEOUT = 1000L class AmneziaActivity : QtActivity() { @@ -59,6 +64,7 @@ class AmneziaActivity : QtActivity() { ServiceEvent.DISCONNECTED -> { QtAndroidController.onVpnDisconnected() + doUnbindService() } ServiceEvent.RECONNECTING -> { @@ -139,7 +145,7 @@ class AmneziaActivity : QtActivity() { */ override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - Log.v(TAG, "Create Amnezia activity: $intent") + Log.d(TAG, "Create Amnezia activity: $intent") mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) vpnServiceMessenger = IpcMessenger( onDeadObjectException = ::doUnbindService, @@ -150,7 +156,7 @@ class AmneziaActivity : QtActivity() { override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) - Log.v(TAG, "onNewIntent: $intent") + Log.d(TAG, "onNewIntent: $intent") intent?.let(::processIntent) } @@ -170,7 +176,7 @@ class AmneziaActivity : QtActivity() { override fun onStart() { super.onStart() - Log.v(TAG, "Start Amnezia activity") + Log.d(TAG, "Start Amnezia activity") mainScope.launch { qtInitialized.await() doBindService() @@ -178,13 +184,13 @@ class AmneziaActivity : QtActivity() { } override fun onStop() { - Log.v(TAG, "Stop Amnezia activity") + Log.d(TAG, "Stop Amnezia activity") doUnbindService() super.onStop() } override fun onDestroy() { - Log.v(TAG, "Destroy Amnezia activity") + Log.d(TAG, "Destroy Amnezia activity") mainScope.cancel() super.onDestroy() } @@ -201,10 +207,19 @@ class AmneziaActivity : QtActivity() { } } + OPEN_FILE_ACTION_CODE -> { + when (resultCode) { + RESULT_OK -> data?.data?.toString() ?: "" + else -> "" + }.let { uri -> + QtAndroidController.onFileOpened(uri) + } + } + CHECK_VPN_PERMISSION_ACTION_CODE -> { when (resultCode) { RESULT_OK -> { - Log.v(TAG, "Vpn permission granted") + Log.d(TAG, "Vpn permission granted") Toast.makeText(this, "Vpn permission granted", Toast.LENGTH_LONG).show() checkVpnPermissionCallbacks?.run { onSuccess() } } @@ -227,7 +242,7 @@ class AmneziaActivity : QtActivity() { */ @MainThread private fun doBindService() { - Log.v(TAG, "Bind service") + Log.d(TAG, "Bind service") Intent(this, AmneziaVpnService::class.java).also { bindService(it, serviceConnection, BIND_ABOVE_CLIENT) } @@ -238,7 +253,7 @@ class AmneziaActivity : QtActivity() { @MainThread private fun doUnbindService() { if (isInBoundState) { - Log.v(TAG, "Unbind service") + Log.d(TAG, "Unbind service") isWaitingStatus = true QtAndroidController.onServiceDisconnected() vpnServiceMessenger.reset() @@ -273,7 +288,7 @@ class AmneziaActivity : QtActivity() { @MainThread private fun checkVpnPermission(onSuccess: () -> Unit, onFail: () -> Unit) { - Log.v(TAG, "Check VPN permission") + Log.d(TAG, "Check VPN permission") VpnService.prepare(applicationContext)?.let { checkVpnPermissionCallbacks = CheckVpnPermissionCallbacks(onSuccess, onFail) startActivityForResult(it, CHECK_VPN_PERMISSION_ACTION_CODE) @@ -294,7 +309,7 @@ class AmneziaActivity : QtActivity() { } private fun connectToVpn(vpnConfig: String) { - Log.v(TAG, "Connect to VPN") + Log.d(TAG, "Connect to VPN") vpnServiceMessenger.send { Action.CONNECT.packToMessage { putString(VPN_CONFIG, vpnConfig) @@ -303,7 +318,7 @@ class AmneziaActivity : QtActivity() { } private fun startVpnService(vpnConfig: String) { - Log.v(TAG, "Start VPN service") + Log.d(TAG, "Start VPN service") Intent(this, AmneziaVpnService::class.java).apply { putExtra(VPN_CONFIG, vpnConfig) }.also { @@ -312,7 +327,7 @@ class AmneziaActivity : QtActivity() { } private fun disconnectFromVpn() { - Log.v(TAG, "Disconnect from VPN") + Log.d(TAG, "Disconnect from VPN") vpnServiceMessenger.send(Action.DISCONNECT) } @@ -356,7 +371,7 @@ class AmneziaActivity : QtActivity() { @Suppress("unused") fun saveFile(fileName: String, data: String) { - Log.v(TAG, "Save file $fileName") + Log.d(TAG, "Save file $fileName") mainScope.launch { tmpFileContentToSave = data @@ -371,17 +386,43 @@ class AmneziaActivity : QtActivity() { } @Suppress("unused") - fun setNotificationText(title: String, message: String, timerSec: Int) { - Log.v(TAG, "Set notification text") - Log.w(TAG, "Not yet implemented") + fun openFile(filter: String?) { + Log.v(TAG, "Open file with filter: $filter") + + val mimeTypes = if (!filter.isNullOrEmpty()) { + val extensionRegex = "\\*\\.[a-z .]+".toRegex(IGNORE_CASE) + val mime = MimeTypeMap.getSingleton() + extensionRegex.findAll(filter).map { + mime.getMimeTypeFromExtension(it.value.drop(2)) + }.filterNotNull().toSet() + } else emptySet() + + Intent(Intent.ACTION_OPEN_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + Log.v(TAG, "File mimyType filter: $mimeTypes") + when (mimeTypes.size) { + 1 -> type = mimeTypes.first() + + in 2..Int.MAX_VALUE -> { + type = "*/*" + putExtra(EXTRA_MIME_TYPES, mimeTypes.toTypedArray()) + } + + else -> type = "*/*" + } + }.also { + startActivityForResult(it, OPEN_FILE_ACTION_CODE) + } } @Suppress("unused") - fun cleanupLogs() { - Log.v(TAG, "Cleanup logs") - Log.w(TAG, "Not yet implemented") + fun setNotificationText(title: String, message: String, timerSec: Int) { + Log.v(TAG, "Set notification text") } + @Suppress("unused") + fun isCameraPresent(): Boolean = applicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA) + @Suppress("unused") fun startQrCodeReader() { Log.v(TAG, "Start camera") @@ -389,4 +430,29 @@ class AmneziaActivity : QtActivity() { startActivity(it) } } + + @Suppress("unused") + fun setSaveLogs(enabled: Boolean) { + Log.d(TAG, "Set save logs: $enabled") + mainScope.launch { + Log.saveLogs = enabled + vpnServiceMessenger.send { + Action.SET_SAVE_LOGS.packToMessage { + putBoolean(SAVE_LOGS, enabled) + } + } + } + } + + @Suppress("unused") + fun exportLogsFile(fileName: String) { + Log.v(TAG, "Export logs file") + saveFile(fileName, Log.getLogs()) + } + + @Suppress("unused") + fun clearLogs() { + Log.v(TAG, "Clear logs") + Log.clearLogs() + } } diff --git a/client/android/src/org/amnezia/vpn/AmneziaApplication.kt b/client/android/src/org/amnezia/vpn/AmneziaApplication.kt index e9d8fdfb7..331828873 100644 --- a/client/android/src/org/amnezia/vpn/AmneziaApplication.kt +++ b/client/android/src/org/amnezia/vpn/AmneziaApplication.kt @@ -5,14 +5,20 @@ import androidx.camera.core.CameraSelector import androidx.camera.core.CameraXConfig import androidx.core.app.NotificationChannelCompat.Builder import androidx.core.app.NotificationManagerCompat +import org.amnezia.vpn.util.Log +import org.amnezia.vpn.util.Prefs import org.qtproject.qt.android.bindings.QtApplication +private const val TAG = "AmneziaApplication" const val NOTIFICATION_CHANNEL_ID: String = "org.amnezia.vpn.notification" class AmneziaApplication : QtApplication(), CameraXConfig.Provider { override fun onCreate() { super.onCreate() + Prefs.init(this) + Log.init(this) + Log.d(TAG, "Create Amnezia application") createNotificationChannel() } diff --git a/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt b/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt index 094874c7c..ab7537165 100644 --- a/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt +++ b/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt @@ -50,6 +50,7 @@ import org.amnezia.vpn.protocol.putStatistics import org.amnezia.vpn.protocol.putStatus import org.amnezia.vpn.protocol.wireguard.Wireguard import org.amnezia.vpn.util.Log +import org.amnezia.vpn.util.Prefs import org.amnezia.vpn.util.net.NetworkState import org.json.JSONException import org.json.JSONObject @@ -58,6 +59,8 @@ private const val TAG = "AmneziaVpnService" const val VPN_CONFIG = "VPN_CONFIG" const val ERROR_MSG = "ERROR_MSG" +const val SAVE_LOGS = "SAVE_LOGS" + const val AFTER_PERMISSION_CHECK = "AFTER_PERMISSION_CHECK" private const val PREFS_CONFIG_KEY = "LAST_CONF" private const val NOTIFICATION_ID = 1337 @@ -118,7 +121,7 @@ class AmneziaVpnService : VpnService() { Action.CONNECT -> { val vpnConfig = msg.data.getString(VPN_CONFIG) - saveConfigToPrefs(vpnConfig) + Prefs.save(PREFS_CONFIG_KEY, vpnConfig) connect(vpnConfig) } @@ -135,6 +138,10 @@ class AmneziaVpnService : VpnService() { } } } + + Action.SET_SAVE_LOGS -> { + Log.saveLogs = msg.data.getBoolean(SAVE_LOGS) + } } } } @@ -179,7 +186,7 @@ class AmneziaVpnService : VpnService() { */ override fun onCreate() { super.onCreate() - Log.v(TAG, "Create Amnezia VPN service") + Log.d(TAG, "Create Amnezia VPN service") mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) connectionScope = CoroutineScope(SupervisorJob() + Dispatchers.IO + connectionExceptionHandler) clientMessenger = IpcMessenger(messengerName = "Client") @@ -193,15 +200,15 @@ class AmneziaVpnService : VpnService() { else intent?.component?.packageName != packageName if (isAlwaysOnCompat) { - Log.v(TAG, "Start service via Always-on") - connect(loadConfigFromPrefs()) + Log.d(TAG, "Start service via Always-on") + connect(Prefs.load(PREFS_CONFIG_KEY)) } else if (intent?.getBooleanExtra(AFTER_PERMISSION_CHECK, false) == true) { - Log.v(TAG, "Start service after permission check") - connect(loadConfigFromPrefs()) + Log.d(TAG, "Start service after permission check") + connect(Prefs.load(PREFS_CONFIG_KEY)) } else { - Log.v(TAG, "Start service") + Log.d(TAG, "Start service") val vpnConfig = intent?.getStringExtra(VPN_CONFIG) - saveConfigToPrefs(vpnConfig) + Prefs.save(PREFS_CONFIG_KEY, vpnConfig) connect(vpnConfig) } ServiceCompat.startForeground(this, NOTIFICATION_ID, notification, foregroundServiceTypeCompat) @@ -237,7 +244,7 @@ class AmneziaVpnService : VpnService() { } override fun onRevoke() { - Log.v(TAG, "onRevoke") + Log.d(TAG, "onRevoke") // Calls to onRevoke() method may not happen on the main thread of the process mainScope.launch { disconnect() @@ -245,7 +252,7 @@ class AmneziaVpnService : VpnService() { } override fun onDestroy() { - Log.v(TAG, "Destroy service") + Log.d(TAG, "Destroy service") runBlocking { disconnect() disconnectionJob?.join() @@ -256,7 +263,7 @@ class AmneziaVpnService : VpnService() { } private fun stopService() { - Log.v(TAG, "Stop service") + Log.d(TAG, "Stop service") // the coroutine below will be canceled during the onDestroy call mainScope.launch { delay(STOP_SERVICE_TIMEOUT) @@ -272,7 +279,7 @@ class AmneziaVpnService : VpnService() { private fun launchProtocolStateHandler() { mainScope.launch { protocolState.collect { protocolState -> - Log.d(TAG, "Protocol state: $protocolState") + Log.d(TAG, "Protocol state changed: $protocolState") when (protocolState) { CONNECTED -> { clientMessenger.send(ServiceEvent.CONNECTED) @@ -305,7 +312,7 @@ class AmneziaVpnService : VpnService() { @MainThread private fun launchSendingStatistics() { - if (isServiceBound && isConnected) { + /* if (isServiceBound && isConnected) { statisticsSendingJob = mainScope.launch { while (true) { clientMessenger.send { @@ -316,7 +323,7 @@ class AmneziaVpnService : VpnService() { delay(STATISTICS_SENDING_TIMEOUT) } } - } + } */ } @MainThread @@ -328,7 +335,7 @@ class AmneziaVpnService : VpnService() { private fun connect(vpnConfig: String?) { if (isConnected || protocolState.value == CONNECTING) return - Log.v(TAG, "Start VPN connection") + Log.d(TAG, "Start VPN connection") protocolState.value = CONNECTING @@ -357,7 +364,7 @@ class AmneziaVpnService : VpnService() { private fun disconnect() { if (isUnknown || isDisconnected || protocolState.value == DISCONNECTING) return - Log.v(TAG, "Stop VPN connection") + Log.d(TAG, "Stop VPN connection") protocolState.value = DISCONNECTING @@ -383,7 +390,7 @@ class AmneziaVpnService : VpnService() { private fun reconnect() { if (!isConnected) return - Log.v(TAG, "Reconnect VPN") + Log.d(TAG, "Reconnect VPN") protocolState.value = RECONNECTING @@ -439,10 +446,4 @@ class AmneziaVpnService : VpnService() { } else { true } - - private fun loadConfigFromPrefs(): String? = - Prefs.get(this).getString(PREFS_CONFIG_KEY, null) - - private fun saveConfigToPrefs(config: String?) = - Prefs.get(this).edit().putString(PREFS_CONFIG_KEY, config).apply() } diff --git a/client/android/src/org/amnezia/vpn/ImportConfigActivity.kt b/client/android/src/org/amnezia/vpn/ImportConfigActivity.kt index 309333efd..cae7ab75a 100644 --- a/client/android/src/org/amnezia/vpn/ImportConfigActivity.kt +++ b/client/android/src/org/amnezia/vpn/ImportConfigActivity.kt @@ -29,20 +29,20 @@ class ImportConfigActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - Log.v(TAG, "Create Import Config Activity: $intent") + Log.d(TAG, "Create Import Config Activity: $intent") intent?.let(::readConfig) } override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) - Log.v(TAG, "onNewIntent: $intent") + Log.d(TAG, "onNewIntent: $intent") intent?.let(::readConfig) } private fun readConfig(intent: Intent) { when (intent.action) { ACTION_SEND -> { - Log.v(TAG, "Process SEND action, type: ${intent.type}") + Log.d(TAG, "Process SEND action, type: ${intent.type}") when (intent.type) { "application/octet-stream" -> { intent.getUriCompat()?.let { uri -> @@ -60,7 +60,7 @@ class ImportConfigActivity : ComponentActivity() { } ACTION_VIEW -> { - Log.v(TAG, "Process VIEW action, scheme: ${intent.scheme}") + Log.d(TAG, "Process VIEW action, scheme: ${intent.scheme}") when (intent.scheme) { "file", "content" -> { intent.data?.let { uri -> @@ -128,7 +128,7 @@ class ImportConfigActivity : ComponentActivity() { private fun startMainActivity(config: String) { if (config.isNotBlank()) { - Log.v(TAG, "startMainActivity") + Log.d(TAG, "startMainActivity") Intent(applicationContext, AmneziaActivity::class.java).apply { action = ACTION_IMPORT_CONFIG addCategory(CATEGORY_DEFAULT) diff --git a/client/android/src/org/amnezia/vpn/IpcMessage.kt b/client/android/src/org/amnezia/vpn/IpcMessage.kt index c9d2bd3f9..26c3b9de4 100644 --- a/client/android/src/org/amnezia/vpn/IpcMessage.kt +++ b/client/android/src/org/amnezia/vpn/IpcMessage.kt @@ -32,7 +32,8 @@ enum class Action : IpcMessage { REGISTER_CLIENT, CONNECT, DISCONNECT, - REQUEST_STATUS + REQUEST_STATUS, + SET_SAVE_LOGS } fun T.packToMessage(): Message diff --git a/client/android/src/org/amnezia/vpn/Prefs.kt b/client/android/src/org/amnezia/vpn/Prefs.kt deleted file mode 100644 index 9b4cb2ba5..000000000 --- a/client/android/src/org/amnezia/vpn/Prefs.kt +++ /dev/null @@ -1,25 +0,0 @@ -package org.amnezia.vpn - -import android.content.Context -import android.content.SharedPreferences -import androidx.security.crypto.EncryptedSharedPreferences -import androidx.security.crypto.MasterKey -import org.amnezia.vpn.util.Log - -private const val TAG = "Prefs" -private const val PREFS_FILE = "org.amnezia.vpn.prefs" -private const val SECURE_PREFS_FILE = "$PREFS_FILE.secure" - -object Prefs { - fun get(context: Context, appContext: Context = context.applicationContext): SharedPreferences = - try { - EncryptedSharedPreferences( - appContext, - SECURE_PREFS_FILE, - MasterKey(appContext) - ) - } catch (e: Exception) { - Log.e(TAG, "Getting Encryption Storage failed: ${e.message}, plaintext fallback") - appContext.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE) - } -} diff --git a/client/android/src/org/amnezia/vpn/VpnRequestActivity.kt b/client/android/src/org/amnezia/vpn/VpnRequestActivity.kt index 9dce3d788..c5abbc39c 100644 --- a/client/android/src/org/amnezia/vpn/VpnRequestActivity.kt +++ b/client/android/src/org/amnezia/vpn/VpnRequestActivity.kt @@ -25,7 +25,7 @@ class VpnRequestActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - Log.v(TAG, "Start request activity") + Log.d(TAG, "Start request activity") val requestIntent = VpnService.prepare(applicationContext) if (requestIntent != null) { if (getSystemService()!!.isKeyguardLocked) { diff --git a/client/android/src/org/amnezia/vpn/qt/QtAndroidController.kt b/client/android/src/org/amnezia/vpn/qt/QtAndroidController.kt index bc8cc4254..cab810a71 100644 --- a/client/android/src/org/amnezia/vpn/qt/QtAndroidController.kt +++ b/client/android/src/org/amnezia/vpn/qt/QtAndroidController.kt @@ -15,6 +15,8 @@ object QtAndroidController { external fun onVpnReconnecting() external fun onStatisticsUpdate(rxBytes: Long, txBytes: Long) + external fun onFileOpened(uri: String) + external fun onConfigImported(data: String) external fun decodeQrCode(data: String): Boolean diff --git a/client/android/utils/build.gradle.kts b/client/android/utils/build.gradle.kts index ac5d1efb5..2ad03d61b 100644 --- a/client/android/utils/build.gradle.kts +++ b/client/android/utils/build.gradle.kts @@ -15,3 +15,7 @@ android { buildConfig = true } } + +dependencies { + implementation(libs.androidx.security.crypto) +} diff --git a/client/android/utils/src/main/kotlin/Log.kt b/client/android/utils/src/main/kotlin/Log.kt index 82180c3d9..03da4507e 100644 --- a/client/android/utils/src/main/kotlin/Log.kt +++ b/client/android/utils/src/main/kotlin/Log.kt @@ -1,33 +1,252 @@ package org.amnezia.vpn.util +import android.content.Context +import android.icu.text.DateFormat +import android.icu.text.SimpleDateFormat +import android.os.Build +import android.os.Process +import java.io.File +import java.io.IOException +import java.io.RandomAccessFile +import java.nio.channels.FileChannel +import java.nio.channels.FileLock +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import java.util.Date +import java.util.Locale +import java.util.concurrent.locks.ReentrantLock +import org.amnezia.vpn.util.Log.Priority.D +import org.amnezia.vpn.util.Log.Priority.E +import org.amnezia.vpn.util.Log.Priority.F +import org.amnezia.vpn.util.Log.Priority.I +import org.amnezia.vpn.util.Log.Priority.V +import org.amnezia.vpn.util.Log.Priority.W import android.util.Log as NativeLog -class Log { - companion object { - fun v(tag: String, msg: String) = debugLog(tag, msg, NativeLog::v) +private const val TAG = "Log" +private const val LOG_FILE_NAME = "amneziaVPN.log" +private const val ROTATE_LOG_FILE_NAME = "amneziaVPN.rotate.log" +private const val LOCK_FILE_NAME = ".lock" +private const val DATE_TIME_PATTERN = "MM-dd HH:mm:ss.SSS" +private const val PREFS_SAVE_LOGS_KEY = "SAVE_LOGS" +private const val LOG_MAX_FILE_SIZE = 1024 * 1024 - fun d(tag: String, msg: String) = debugLog(tag, msg, NativeLog::d) +/** + * | Priority | Save to file | Logcat logging | + * |-------------------|--------------|----------------------------------------------| + * | Verbose | Don't save | Only in Debug build | + * | Debug | Save | In Debug build or if log saving is enabled | + * | Info, Warn, Error | Save | Enabled | + * | Fatal (Assert) | Save | Enabled. Depending on system configuration, | + * | | | create a report and/or terminate the process | + */ +object Log { + private val dateTimeFormat: Any = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) DateTimeFormatter.ofPattern(DATE_TIME_PATTERN) + else object : ThreadLocal() { + override fun initialValue(): DateFormat = SimpleDateFormat(DATE_TIME_PATTERN, Locale.US) + } - fun i(tag: String, msg: String) = log(tag, msg, NativeLog::i) + private lateinit var logDir: File + private val logFile: File by lazy { File(logDir, LOG_FILE_NAME) } + private val rotateLogFile: File by lazy { File(logDir, ROTATE_LOG_FILE_NAME) } - fun w(tag: String, msg: String) = log(tag, msg, NativeLog::w) + private val fileLock: FileChannel by lazy { RandomAccessFile(File(logDir, LOCK_FILE_NAME).path, "rw").channel } + private val threadLock: ReentrantLock by lazy { ReentrantLock() } - fun e(tag: String, msg: String) = log(tag, msg, NativeLog::e) + @Volatile + private var _saveLogs: Boolean = false + var saveLogs: Boolean + get() = _saveLogs + set(value) { + if (_saveLogs != value) { + if (value && !logDir.exists() && !logDir.mkdir()) { + NativeLog.e(TAG, "Failed to create dir: $logDir") + return + } + _saveLogs = value + Prefs.save(PREFS_SAVE_LOGS_KEY, value) + } + } - fun v(tag: String, msg: Any?) = v(tag, msg.toString()) + @JvmStatic + fun v(tag: String, msg: String) = log(tag, msg, V) - fun d(tag: String, msg: Any?) = d(tag, msg.toString()) + @JvmStatic + fun d(tag: String, msg: String) = log(tag, msg, D) - fun i(tag: String, msg: Any?) = i(tag, msg.toString()) + @JvmStatic + fun i(tag: String, msg: String) = log(tag, msg, I) - fun w(tag: String, msg: Any?) = w(tag, msg.toString()) + @JvmStatic + fun w(tag: String, msg: String) = log(tag, msg, W) - fun e(tag: String, msg: Any?) = e(tag, msg.toString()) + @JvmStatic + fun e(tag: String, msg: String) = log(tag, msg, E) - private inline fun log(tag: String, msg: String, delegate: (String, String) -> Unit) = delegate(tag, msg) + @JvmStatic + fun f(tag: String, msg: String) = log(tag, msg, F) - private inline fun debugLog(tag: String, msg: String, delegate: (String, String) -> Unit) { - if (BuildConfig.DEBUG) delegate(tag, msg) + fun v(tag: String, msg: Any?) = v(tag, msg.toString()) + + fun d(tag: String, msg: Any?) = d(tag, msg.toString()) + + fun i(tag: String, msg: Any?) = i(tag, msg.toString()) + + fun w(tag: String, msg: Any?) = w(tag, msg.toString()) + + fun e(tag: String, msg: Any?) = e(tag, msg.toString()) + + fun f(tag: String, msg: Any?) = f(tag, msg.toString()) + + fun init(context: Context) { + v(TAG, "Init Log") + logDir = File(context.cacheDir, "logs") + saveLogs = Prefs.load(PREFS_SAVE_LOGS_KEY) + } + + fun getLogs(): String = + "${deviceInfo()}\n${readLogs()}\nLOGCAT:\n${getLogcat()}" + + fun clearLogs() { + withLock { + logFile.delete() + rotateLogFile.delete() } } + + private fun log(tag: String, msg: String, priority: Priority) { + if (saveLogs && priority != V) saveLogMsg(formatLogMsg(tag, msg, priority)) + + if (priority == F) { + NativeLog.wtf(tag, msg) + } else if ( + (priority != V && priority != D) || + (priority == V && BuildConfig.DEBUG) || + (priority == D && (BuildConfig.DEBUG || saveLogs)) + ) { + NativeLog.println(priority.level, tag, msg) + } + } + + private fun saveLogMsg(msg: String) { + withTryLock(condition = { logFile.length() > LOG_MAX_FILE_SIZE }) { + logFile.renameTo(rotateLogFile) + } + try { + logFile.appendText(msg) + } catch (e: IOException) { + NativeLog.e(TAG, "Failed to write log: $e") + } + } + + private fun formatLogMsg(tag: String, msg: String, priority: Priority): String { + val date = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + LocalDateTime.now().format(dateTimeFormat as DateTimeFormatter) + } else { + @Suppress("UNCHECKED_CAST") + (dateTimeFormat as ThreadLocal).get()?.format(Date()) + } + return "$date ${Process.myPid()} ${Process.myTid()} $priority [${Thread.currentThread().name}] " + + "$tag: $msg\n" + } + + private fun deviceInfo(): String { + val sb = StringBuilder() + sb.append("Model: ").appendLine(Build.MODEL) + sb.append("Brand: ").appendLine(Build.BRAND) + sb.append("Product: ").appendLine(Build.PRODUCT) + sb.append("Device: ").appendLine(Build.DEVICE) + sb.append("Codename: ").appendLine(Build.VERSION.CODENAME) + sb.append("Release: ").appendLine(Build.VERSION.RELEASE) + sb.append("SDK: ").appendLine(Build.VERSION.SDK_INT) + sb.append("ABI: ").appendLine(Build.SUPPORTED_ABIS.joinToString()) + return sb.toString() + } + + private fun readLogs(): String { + var logText = "" + withLock { + try { + if (rotateLogFile.exists()) logText = rotateLogFile.readText() + if (logFile.exists()) logText += logFile.readText() + } catch (e: IOException) { + val errorMsg = "Failed to read log: $e" + NativeLog.e(TAG, errorMsg) + logText += errorMsg + } + } + return logText + } + + private fun getLogcat(): String { + try { + val process = ProcessBuilder("logcat", "-d").redirectErrorStream(true).start() + return process.inputStream.reader().readText() + } catch (e: IOException) { + val errorMsg = "Failed to get logcat log: $e" + NativeLog.e(TAG, errorMsg) + return errorMsg + } + } + + private fun withLock(block: () -> Unit) { + threadLock.lock() + try { + var l: FileLock? = null + try { + l = fileLock.lock() + block() + } catch (e: IOException) { + NativeLog.e(TAG, "Failed to get file lock: $e") + } finally { + try { + l?.release() + } catch (e: IOException) { + NativeLog.e(TAG, "Failed to release file lock: $e") + } + } + } finally { + threadLock.unlock() + } + } + + private fun withTryLock(condition: () -> Boolean, block: () -> Unit) { + if (condition()) { + if (threadLock.tryLock()) { + try { + if (condition()) { + var l: FileLock? = null + try { + l = fileLock.tryLock() + if (l != null) { + if (condition()) { + block() + } + } + } catch (e: IOException) { + NativeLog.e(TAG, "Failed to get file tryLock: $e") + } finally { + try { + l?.release() + } catch (e: IOException) { + NativeLog.e(TAG, "Failed to release file tryLock: $e") + } + } + } + } finally { + threadLock.unlock() + } + } + } + } + + private enum class Priority(val level: Int) { + V(2), + D(3), + I(4), + W(5), + E(6), + F(7) + } } diff --git a/client/android/utils/src/main/kotlin/Prefs.kt b/client/android/utils/src/main/kotlin/Prefs.kt new file mode 100644 index 000000000..9afbc7beb --- /dev/null +++ b/client/android/utils/src/main/kotlin/Prefs.kt @@ -0,0 +1,58 @@ +package org.amnezia.vpn.util + +import android.app.Application +import android.content.Context +import android.content.SharedPreferences +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKey +import kotlin.reflect.typeOf + +private const val TAG = "Prefs" +private const val PREFS_FILE = "org.amnezia.vpn.prefs" +private const val SECURE_PREFS_FILE = "$PREFS_FILE.secure" + +object Prefs { + private lateinit var app: Application + val prefs: SharedPreferences + get() = try { + EncryptedSharedPreferences( + app, + SECURE_PREFS_FILE, + MasterKey(app) + ) + } catch (e: Exception) { + Log.e(TAG, "Getting Encryption Storage failed: $e, plaintext fallback") + app.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE) + } + + fun init(app: Application) { + Log.v(TAG, "Init Prefs") + this.app = app + } + + fun save(key: String, value: Boolean) = + prefs.edit().putBoolean(key, value).apply() + + fun save(key: String, value: String?) = + prefs.edit().putString(key, value).apply() + + fun save(key: String, value: Int) = + prefs.edit().putInt(key, value).apply() + + fun save(key: String, value: Long) = + prefs.edit().putLong(key, value).apply() + + fun save(key: String, value: Float) = + prefs.edit().putFloat(key, value).apply() + + inline fun load(key: String): T { + return when (typeOf()) { + typeOf() -> prefs.getBoolean(key, false) + typeOf() -> prefs.getString(key, "") + typeOf() -> prefs.getInt(key, 0) + typeOf() -> prefs.getLong(key, 0L) + typeOf() -> prefs.getFloat(key, 0f) + else -> throw IllegalArgumentException("SharedPreferences does not support type: ${typeOf()}") + } as T + } +} diff --git a/client/android/utils/src/main/kotlin/net/NetworkState.kt b/client/android/utils/src/main/kotlin/net/NetworkState.kt index 42d7baee9..957fc3cbe 100644 --- a/client/android/utils/src/main/kotlin/net/NetworkState.kt +++ b/client/android/utils/src/main/kotlin/net/NetworkState.kt @@ -82,7 +82,7 @@ class NetworkState( fun bindNetworkListener() { if (isListenerBound) return - Log.v(TAG, "Bind network listener") + Log.d(TAG, "Bind network listener") if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { connectivityManager.registerBestMatchingNetworkCallback(networkRequest, networkCallback, handler) } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { @@ -95,7 +95,7 @@ class NetworkState( fun unbindNetworkListener() { if (!isListenerBound) return - Log.v(TAG, "Unbind network listener") + Log.d(TAG, "Unbind network listener") connectivityManager.unregisterNetworkCallback(networkCallback) isListenerBound = false currentNetwork = null diff --git a/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt b/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt index e1f96fa31..87d5e2492 100644 --- a/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt +++ b/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt @@ -99,7 +99,10 @@ open class Wireguard : Protocol() { } protected fun WireguardConfig.Builder.configWireguard(configData: Map) { - configData["Address"]?.let { addAddress(InetNetwork.parse(it)) } + configData["Address"]?.split(",")?.map { address -> + InetNetwork.parse(address.trim()) + }?.forEach(::addAddress) + configData["DNS"]?.split(",")?.map { dns -> parseInetAddress(dns.trim()) }?.forEach(::addDnsServer) diff --git a/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/WireguardConfig.kt b/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/WireguardConfig.kt index 1e74e6ff6..0e303f0e2 100644 --- a/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/WireguardConfig.kt +++ b/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/WireguardConfig.kt @@ -11,7 +11,7 @@ open class WireguardConfig protected constructor( val endpoint: InetEndpoint, val persistentKeepalive: Int, val publicKeyHex: String, - val preSharedKeyHex: String, + val preSharedKeyHex: String?, val privateKeyHex: String ) : ProtocolConfig(protocolConfigBuilder) { @@ -43,7 +43,8 @@ open class WireguardConfig protected constructor( appendLine("endpoint=$endpoint") if (persistentKeepalive != 0) appendLine("persistent_keepalive_interval=$persistentKeepalive") - appendLine("preshared_key=$preSharedKeyHex") + if (preSharedKeyHex != null) + appendLine("preshared_key=$preSharedKeyHex") } open class Builder : ProtocolConfig.Builder(true) { @@ -56,7 +57,7 @@ open class WireguardConfig protected constructor( internal lateinit var publicKeyHex: String private set - internal lateinit var preSharedKeyHex: String + internal var preSharedKeyHex: String? = null private set internal lateinit var privateKeyHex: String @@ -74,7 +75,7 @@ open class WireguardConfig protected constructor( fun setPrivateKeyHex(privateKeyHex: String) = apply { this.privateKeyHex = privateKeyHex } - override fun build(): WireguardConfig = WireguardConfig(this) + override fun build(): WireguardConfig = configBuild().run { WireguardConfig(this@Builder) } } companion object { diff --git a/client/cmake/3rdparty.cmake b/client/cmake/3rdparty.cmake index ca7d659e5..ec544764e 100644 --- a/client/cmake/3rdparty.cmake +++ b/client/cmake/3rdparty.cmake @@ -90,7 +90,7 @@ include_directories( ${LIBSSH_ROOT_DIR}/include ${CLIENT_ROOT_DIR}/3rd/libssh/include ${CLIENT_ROOT_DIR}/3rd/QSimpleCrypto/include - ${CLIENT_ROOT_DIR}/3rd/qtkeychain + ${CLIENT_ROOT_DIR}/3rd/qtkeychain/qtkeychain ${CMAKE_CURRENT_BINARY_DIR}/3rd/qtkeychain ${CMAKE_CURRENT_BINARY_DIR}/3rd/libssh/include ) diff --git a/client/cmake/android.cmake b/client/cmake/android.cmake index 2d08b4b6d..7ffa680e4 100644 --- a/client/cmake/android.cmake +++ b/client/cmake/android.cmake @@ -27,7 +27,7 @@ link_directories(${CMAKE_CURRENT_SOURCE_DIR}/platforms/android) set(HEADERS ${HEADERS} ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_controller.h ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_notificationhandler.h - ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/androidutils.h + ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_utils.h ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/authResultReceiver.h ${CMAKE_CURRENT_SOURCE_DIR}/protocols/android_vpnprotocol.h ) @@ -35,7 +35,7 @@ set(HEADERS ${HEADERS} set(SOURCES ${SOURCES} ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_controller.cpp ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_notificationhandler.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/androidutils.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_utils.cpp ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/authResultReceiver.cpp ${CMAKE_CURRENT_SOURCE_DIR}/protocols/android_vpnprotocol.cpp ) diff --git a/client/cmake/ios.cmake b/client/cmake/ios.cmake index 7aa9f1a95..3234578e4 100644 --- a/client/cmake/ios.cmake +++ b/client/cmake/ios.cmake @@ -97,7 +97,7 @@ target_compile_options(${PROJECT} PRIVATE -DVPN_NE_BUNDLEID=\"${BUILD_IOS_APP_IDENTIFIER}.network-extension\" ) -set(WG_APPLE_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/3rd/awg-apple/Sources) +set(WG_APPLE_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/3rd/amneziawg-apple/Sources) target_sources(${PROJECT} PRIVATE # ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosvpnprotocol.swift diff --git a/client/configurators/openvpn_configurator.cpp b/client/configurators/openvpn_configurator.cpp index e33622366..8b201fbf3 100644 --- a/client/configurators/openvpn_configurator.cpp +++ b/client/configurators/openvpn_configurator.cpp @@ -118,31 +118,33 @@ QString OpenVpnConfigurator::genOpenVpnConfig(const ServerCredentials &credentia return QJsonDocument(jConfig).toJson(); } -QString OpenVpnConfigurator::processConfigWithLocalSettings(QString jsonConfig) +QString OpenVpnConfigurator::processConfigWithLocalSettings(QString jsonConfig, const int serverIndex) { QJsonObject json = QJsonDocument::fromJson(jsonConfig.toUtf8()).object(); QString config = json[config_key::config].toString(); - QRegularExpression regex("redirect-gateway.*"); - config.replace(regex, ""); + if (!m_settings->server(serverIndex).value(config_key::configVersion).toInt()) { + QRegularExpression regex("redirect-gateway.*"); + config.replace(regex, ""); - if (m_settings->routeMode() == Settings::VpnAllSites) { - config.append("\nredirect-gateway def1 ipv6 bypass-dhcp\n"); - // Prevent ipv6 leak - config.append("ifconfig-ipv6 fd15:53b6:dead::2/64 fd15:53b6:dead::1\n"); - config.append("block-ipv6\n"); - } - if (m_settings->routeMode() == Settings::VpnOnlyForwardSites) { + if (m_settings->routeMode() == Settings::VpnAllSites) { + config.append("\nredirect-gateway def1 ipv6 bypass-dhcp\n"); + // Prevent ipv6 leak + config.append("ifconfig-ipv6 fd15:53b6:dead::2/64 fd15:53b6:dead::1\n"); + config.append("block-ipv6\n"); + } + if (m_settings->routeMode() == Settings::VpnOnlyForwardSites) { - // no redirect-gateway - } - if (m_settings->routeMode() == Settings::VpnAllExceptSites) { -#ifndef Q_OS_ANDROID - config.append("\nredirect-gateway ipv6 !ipv4 bypass-dhcp\n"); -#endif - // Prevent ipv6 leak - config.append("ifconfig-ipv6 fd15:53b6:dead::2/64 fd15:53b6:dead::1\n"); - config.append("block-ipv6\n"); + // no redirect-gateway + } + if (m_settings->routeMode() == Settings::VpnAllExceptSites) { + #ifndef Q_OS_ANDROID + config.append("\nredirect-gateway ipv6 !ipv4 bypass-dhcp\n"); + #endif + // Prevent ipv6 leak + config.append("ifconfig-ipv6 fd15:53b6:dead::2/64 fd15:53b6:dead::1\n"); + config.append("block-ipv6\n"); + } } #ifndef MZ_WINDOWS diff --git a/client/configurators/openvpn_configurator.h b/client/configurators/openvpn_configurator.h index cc66d13ff..424a20e1b 100644 --- a/client/configurators/openvpn_configurator.h +++ b/client/configurators/openvpn_configurator.h @@ -26,7 +26,7 @@ public: QString genOpenVpnConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig, QString &clientId, ErrorCode *errorCode = nullptr); - QString processConfigWithLocalSettings(QString jsonConfig); + QString processConfigWithLocalSettings(QString jsonConfig, const int serverIndex); QString processConfigWithExportSettings(QString jsonConfig); ErrorCode signCert(DockerContainer container, diff --git a/client/configurators/vpn_configurator.cpp b/client/configurators/vpn_configurator.cpp index 3018b52fc..c74a3d4f9 100644 --- a/client/configurators/vpn_configurator.cpp +++ b/client/configurators/vpn_configurator.cpp @@ -92,7 +92,7 @@ QString &VpnConfigurator::processConfigWithLocalSettings(int serverIndex, Docker processConfigWithDnsSettings(serverIndex, container, proto, config); if (proto == Proto::OpenVpn) { - config = openVpnConfigurator->processConfigWithLocalSettings(config); + config = openVpnConfigurator->processConfigWithLocalSettings(config, serverIndex); } return config; } diff --git a/client/configurators/vpn_configurator.h b/client/configurators/vpn_configurator.h index 61dc2ac6f..7164bd8e3 100644 --- a/client/configurators/vpn_configurator.h +++ b/client/configurators/vpn_configurator.h @@ -46,6 +46,7 @@ public: signals: void newVpnConfigCreated(const QString &clientId, const QString &clientName, const DockerContainer container, ServerCredentials credentials); + void clientModelUpdated(); }; #endif // VPN_CONFIGURATOR_H diff --git a/client/core/controllers/serverController.cpp b/client/core/controllers/serverController.cpp index 36560e102..afb6263d4 100644 --- a/client/core/controllers/serverController.cpp +++ b/client/core/controllers/serverController.cpp @@ -168,7 +168,7 @@ ErrorCode ServerController::uploadTextFileToContainer(DockerContainer container, } else return ErrorCode::NotImplementedError; - if (stdOut.contains("Error: No such container:")) { + if (stdOut.contains("Error") && stdOut.contains("No such container")) { return ErrorCode::ServerContainerMissingError; } @@ -211,8 +211,14 @@ ErrorCode ServerController::uploadFileToHost(const ServerCredentials &credential localFile.write(data); localFile.close(); +#ifdef Q_OS_WINDOWS + error = m_sshClient.sftpFileCopy(overwriteMode, localFile.fileName().toLocal8Bit().toStdString(), remotePath.toStdString(), + "non_desc"); +#else error = m_sshClient.sftpFileCopy(overwriteMode, localFile.fileName().toStdString(), remotePath.toStdString(), "non_desc"); +#endif + if (error != ErrorCode::NoError) { return error; } diff --git a/client/ios/networkextension/CMakeLists.txt b/client/ios/networkextension/CMakeLists.txt index 16769ea39..fb1bd3c1b 100644 --- a/client/ios/networkextension/CMakeLists.txt +++ b/client/ios/networkextension/CMakeLists.txt @@ -58,7 +58,7 @@ target_link_libraries(networkextension PRIVATE ${FW_UI_KIT}) target_compile_options(networkextension PRIVATE -DGROUP_ID=\"${BUILD_IOS_GROUP_IDENTIFIER}\") target_compile_options(networkextension PRIVATE -DNETWORK_EXTENSION=1) -set(WG_APPLE_SOURCE_DIR ${CLIENT_ROOT_DIR}/3rd/awg-apple/Sources) +set(WG_APPLE_SOURCE_DIR ${CLIENT_ROOT_DIR}/3rd/amneziawg-apple/Sources) target_sources(networkextension PRIVATE ${WG_APPLE_SOURCE_DIR}/WireGuardKit/WireGuardAdapter.swift diff --git a/client/ios/networkextension/WireGuardNetworkExtension-Bridging-Header.h b/client/ios/networkextension/WireGuardNetworkExtension-Bridging-Header.h index 44d0b6b0b..2cca0fc8e 100644 --- a/client/ios/networkextension/WireGuardNetworkExtension-Bridging-Header.h +++ b/client/ios/networkextension/WireGuardNetworkExtension-Bridging-Header.h @@ -1,6 +1,6 @@ #include "wireguard-go-version.h" -#include "3rd/awg-apple/Sources/WireGuardKitGo/wireguard.h" -#include "3rd/awg-apple/Sources/WireGuardKitC/WireGuardKitC.h" +#include "3rd/amneziawg-apple/Sources/WireGuardKitGo/wireguard.h" +#include "3rd/amneziawg-apple/Sources/WireGuardKitC/WireGuardKitC.h" #include #include diff --git a/client/logger.cpp b/client/logger.cpp index 46ce53f09..75c2d6523 100644 --- a/client/logger.cpp +++ b/client/logger.cpp @@ -29,7 +29,7 @@ void debugMessageHandler(QtMsgType type, const QMessageLogContext& context, cons } // Skip annoying messages from Qt - if (msg.startsWith("Unknown property") || msg.startsWith("Could not create pixmap") || msg.startsWith("Populating font")) { + if (msg.startsWith("Unknown property") || msg.startsWith("Could not create pixmap") || msg.startsWith("Populating font") || msg.startsWith("stale focus object")) { return; } diff --git a/client/platforms/android/android_controller.cpp b/client/platforms/android/android_controller.cpp index a739bee38..767004fc1 100644 --- a/client/platforms/android/android_controller.cpp +++ b/client/platforms/android/android_controller.cpp @@ -1,8 +1,10 @@ -#include #include #include +#include +#include #include "android_controller.h" +#include "android_utils.h" #include "ui/controllers/importController.h" namespace @@ -10,13 +12,15 @@ namespace AndroidController *s_instance = nullptr; constexpr auto QT_ANDROID_CONTROLLER_CLASS = "org/amnezia/vpn/qt/QtAndroidController"; + constexpr auto ANDROID_LOG_CLASS = "org/amnezia/vpn/util/Log"; + constexpr auto TAG = "AmneziaQt"; } // namespace AndroidController::AndroidController() : QObject() { connect(this, &AndroidController::status, this, [this](AndroidController::ConnectionState state) { - qDebug() << "Android event: status; state:" << textConnectionState(state); + qDebug() << "Android event: status =" << textConnectionState(state); if (isWaitingStatus) { qDebug() << "Initialization by service status"; isWaitingStatus = false; @@ -106,6 +110,7 @@ bool AndroidController::initialize() {"onVpnDisconnected", "()V", reinterpret_cast(onVpnDisconnected)}, {"onVpnReconnecting", "()V", reinterpret_cast(onVpnReconnecting)}, {"onStatisticsUpdate", "(JJ)V", reinterpret_cast(onStatisticsUpdate)}, + {"onFileOpened", "(Ljava/lang/String;)V", reinterpret_cast(onFileOpened)}, {"onConfigImported", "(Ljava/lang/String;)V", reinterpret_cast(onConfigImported)}, {"decodeQrCode", "(Ljava/lang/String;)Z", reinterpret_cast(decodeQrCode)} }; @@ -123,24 +128,19 @@ bool AndroidController::initialize() // static template -auto AndroidController::callActivityMethod(const char *methodName, const char *signature, - const std::function &defValue, Args &&...args) +auto AndroidController::callActivityMethod(const char *methodName, const char *signature, Args &&...args) { qDebug() << "Call activity method:" << methodName; - QJniObject activity = QNativeInterface::QAndroidApplication::context(); - if (activity.isValid()) { - return activity.callMethod(methodName, signature, std::forward(args)...); - } else { - qCritical() << "Activity is not valid"; - return defValue(); - } + QJniObject activity = AndroidUtils::getActivity(); + Q_ASSERT(activity.isValid()); + return activity.callMethod(methodName, signature, std::forward(args)...); } // static template void AndroidController::callActivityMethod(const char *methodName, const char *signature, Args &&...args) { - callActivityMethod(methodName, signature, [] {}, std::forward(args)...); + callActivityMethod(methodName, signature, std::forward(args)...); } ErrorCode AndroidController::start(const QJsonObject &vpnConfig) @@ -165,6 +165,24 @@ void AndroidController::saveFile(const QString &fileName, const QString &data) QJniObject::fromString(data).object()); } +QString AndroidController::openFile(const QString &filter) +{ + QEventLoop wait; + QString fileName; + connect(this, &AndroidController::fileOpened, this, + [&fileName, &wait](const QString &uri) { + qDebug() << "Android event: file opened; uri:" << uri; + fileName = QQmlFile::urlToLocalFileOrQrc(uri); + qDebug() << "Android opened filename:" << fileName; + wait.quit(); + }, + static_cast(Qt::QueuedConnection | Qt::SingleShotConnection)); + callActivityMethod("openFile", "(Ljava/lang/String;)V", + QJniObject::fromString(filter).object()); + wait.exec(); + return fileName; +} + void AndroidController::setNotificationText(const QString &title, const QString &message, int timerSec) { callActivityMethod("setNotificationText", "(Ljava/lang/String;Ljava/lang/String;I)V", @@ -173,11 +191,114 @@ void AndroidController::setNotificationText(const QString &title, const QString (jint) timerSec); } +bool AndroidController::isCameraPresent() +{ + return callActivityMethod("isCameraPresent", "()Z"); +} + void AndroidController::startQrReaderActivity() { callActivityMethod("startQrCodeReader", "()V"); } +void AndroidController::setSaveLogs(bool enabled) +{ + callActivityMethod("setSaveLogs", "(Z)V", enabled); +} + +void AndroidController::exportLogsFile(const QString &fileName) +{ + callActivityMethod("exportLogsFile", "(Ljava/lang/String;)V", + QJniObject::fromString(fileName).object()); +} + +void AndroidController::clearLogs() +{ + callActivityMethod("clearLogs", "()V"); +} + +// Moving log processing to the Android side +jclass AndroidController::log; +jmethodID AndroidController::logDebug; +jmethodID AndroidController::logInfo; +jmethodID AndroidController::logWarning; +jmethodID AndroidController::logError; +jmethodID AndroidController::logFatal; + +// static +bool AndroidController::initLogging() +{ + QJniEnvironment env; + + log = env.findClass(ANDROID_LOG_CLASS); + if (log == nullptr) { + qCritical() << "Android log class" << ANDROID_LOG_CLASS << "not found"; + return false; + } + + auto logMethodSignature = "(Ljava/lang/String;Ljava/lang/String;)V"; + + logDebug = env.findStaticMethod(log, "d", logMethodSignature); + if (logDebug == nullptr) { + qCritical() << "Android debug log method not found"; + return false; + } + + logInfo = env.findStaticMethod(log, "i", logMethodSignature); + if (logInfo == nullptr) { + qCritical() << "Android info log method not found"; + return false; + } + + logWarning = env.findStaticMethod(log, "w", logMethodSignature); + if (logWarning == nullptr) { + qCritical() << "Android warning log method not found"; + return false; + } + + logError = env.findStaticMethod(log, "e", logMethodSignature); + if (logError == nullptr) { + qCritical() << "Android error log method not found"; + return false; + } + + logFatal = env.findStaticMethod(log, "f", logMethodSignature); + if (logFatal == nullptr) { + qCritical() << "Android fatal log method not found"; + return false; + } + + qInstallMessageHandler(messageHandler); + return true; +} + +// static +void AndroidController::messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &message) +{ + jmethodID logMethod = logDebug; + switch (type) { + case QtDebugMsg: + logMethod = logDebug; + break; + case QtInfoMsg: + logMethod = logInfo; + break; + case QtWarningMsg: + logMethod = logWarning; + break; + case QtCriticalMsg: + logMethod = logError; + break; + case QtFatalMsg: + logMethod = logFatal; + break; + } + QString formattedMessage = qFormatLogMessage(type, context, message); + QJniObject::callStaticMethod(log, logMethod, + QJniObject::fromString(TAG).object(), + QJniObject::fromString(formattedMessage).object()); +} + void AndroidController::qtAndroidControllerInitialized() { callActivityMethod("qtAndroidControllerInitialized", "()V"); @@ -285,20 +406,19 @@ void AndroidController::onStatisticsUpdate(JNIEnv *env, jobject thiz, jlong rxBy } // static -void AndroidController::onConfigImported(JNIEnv *env, jobject thiz, jstring data) +void AndroidController::onFileOpened(JNIEnv *env, jobject thiz, jstring uri) { - Q_UNUSED(env); Q_UNUSED(thiz); - const char *buffer = env->GetStringUTFChars(data, nullptr); - if (!buffer) { - return; - } + emit AndroidController::instance()->fileOpened(AndroidUtils::convertJString(env, uri)); +} - QString config(buffer); - env->ReleaseStringUTFChars(data, buffer); +// static +void AndroidController::onConfigImported(JNIEnv *env, jobject thiz, jstring data) +{ + Q_UNUSED(thiz); - emit AndroidController::instance()->configImported(config); + emit AndroidController::instance()->configImported(AndroidUtils::convertJString(env, data)); } // static @@ -306,12 +426,5 @@ bool AndroidController::decodeQrCode(JNIEnv *env, jobject thiz, jstring data) { Q_UNUSED(thiz); - const char *buffer = env->GetStringUTFChars(data, nullptr); - if (!buffer) { - return false; - } - - QString code(buffer); - env->ReleaseStringUTFChars(data, buffer); - return ImportController::decodeQrCode(code); + return ImportController::decodeQrCode(AndroidUtils::convertJString(env, data)); } diff --git a/client/platforms/android/android_controller.h b/client/platforms/android/android_controller.h index 4e72cbdfc..86b117f7e 100644 --- a/client/platforms/android/android_controller.h +++ b/client/platforms/android/android_controller.h @@ -18,7 +18,8 @@ public: bool initialize(); // keep synchronized with org.amnezia.vpn.protocol.ProtocolState - enum class ConnectionState { + enum class ConnectionState + { CONNECTED, CONNECTING, DISCONNECTED, @@ -30,8 +31,16 @@ public: ErrorCode start(const QJsonObject &vpnConfig); void stop(); void setNotificationText(const QString &title, const QString &message, int timerSec); - void saveFile(const QString& fileName, const QString &data); + void saveFile(const QString &fileName, const QString &data); + QString openFile(const QString &filter); + bool isCameraPresent(); void startQrReaderActivity(); + void setSaveLogs(bool enabled); + void exportLogsFile(const QString &fileName); + void clearLogs(); + + static bool initLogging(); + static void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &message); signals: void connectionStateChanged(Vpn::ConnectionState state); @@ -43,6 +52,7 @@ signals: void vpnDisconnected(); void vpnReconnecting(); void statisticsUpdated(quint64 rxBytes, quint64 txBytes); + void fileOpened(QString uri); void configImported(QString config); void importConfigFromOutside(QString config); void initConnectionState(Vpn::ConnectionState state); @@ -50,6 +60,13 @@ signals: private: bool isWaitingStatus = true; + static jclass log; + static jmethodID logDebug; + static jmethodID logInfo; + static jmethodID logWarning; + static jmethodID logError; + static jmethodID logFatal; + void qtAndroidControllerInitialized(); static Vpn::ConnectionState convertState(ConnectionState state); @@ -65,11 +82,11 @@ private: static void onVpnReconnecting(JNIEnv *env, jobject thiz); static void onStatisticsUpdate(JNIEnv *env, jobject thiz, jlong rxBytes, jlong txBytes); static void onConfigImported(JNIEnv *env, jobject thiz, jstring data); + static void onFileOpened(JNIEnv *env, jobject thiz, jstring uri); static bool decodeQrCode(JNIEnv *env, jobject thiz, jstring data); template - static auto callActivityMethod(const char *methodName, const char *signature, - const std::function &defValue, Args &&...args); + static auto callActivityMethod(const char *methodName, const char *signature, Args &&...args); template static void callActivityMethod(const char *methodName, const char *signature, Args &&...args); }; diff --git a/client/platforms/android/android_utils.cpp b/client/platforms/android/android_utils.cpp new file mode 100644 index 000000000..4a994ab02 --- /dev/null +++ b/client/platforms/android/android_utils.cpp @@ -0,0 +1,30 @@ +#include +#include "android_utils.h" + +namespace AndroidUtils +{ + +QJniObject getActivity() +{ + return QNativeInterface::QAndroidApplication::context(); +} + +QString convertJString(JNIEnv *env, jstring data) +{ + int len = env->GetStringLength(data); + QString res(len, Qt::Uninitialized); + env->GetStringRegion(data, 0, len, reinterpret_cast(res.data())); + return res; +} + +void runOnAndroidThreadSync(const std::function &runnable) +{ + QNativeInterface::QAndroidApplication::runOnAndroidMainThread(runnable).waitForFinished(); +} + +void runOnAndroidThreadAsync(const std::function &runnable) +{ + QNativeInterface::QAndroidApplication::runOnAndroidMainThread(runnable); +} + +} diff --git a/client/platforms/android/android_utils.h b/client/platforms/android/android_utils.h new file mode 100644 index 000000000..9ed58b75f --- /dev/null +++ b/client/platforms/android/android_utils.h @@ -0,0 +1,16 @@ +#ifndef ANDROID_UTILS_H +#define ANDROID_UTILS_H + +#include + +namespace AndroidUtils +{ +QJniObject getActivity(); + +QString convertJString(JNIEnv *env, jstring data); + +void runOnAndroidThreadSync(const std::function &runnable); +void runOnAndroidThreadAsync(const std::function &runnable); +}; + +#endif // ANDROID_UTILS_H diff --git a/client/platforms/android/androidutils.cpp b/client/platforms/android/androidutils.cpp deleted file mode 100644 index 7cc39824f..000000000 --- a/client/platforms/android/androidutils.cpp +++ /dev/null @@ -1,183 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -#include "androidutils.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "jni.h" - -namespace -{ - AndroidUtils *s_instance = nullptr; -} // namespace - -// static -QString AndroidUtils::GetDeviceName() -{ - QJniEnvironment env; - jclass BUILD = env->FindClass("android/os/Build"); - jfieldID model = env->GetStaticFieldID(BUILD, "MODEL", "Ljava/lang/String;"); - jstring value = (jstring)env->GetStaticObjectField(BUILD, model); - - if (!value) { - return QString("Android Device"); - } - - const char *buffer = env->GetStringUTFChars(value, nullptr); - if (!buffer) { - return QString("Android Device"); - } - - QString res(buffer); - env->ReleaseStringUTFChars(value, buffer); - - return res; -}; - -// static -AndroidUtils *AndroidUtils::instance() -{ - if (!s_instance) { - Q_ASSERT(qApp); - s_instance = new AndroidUtils(qApp); - } - - return s_instance; -} - -AndroidUtils::AndroidUtils(QObject *parent) : QObject(parent) -{ - Q_ASSERT(!s_instance); - s_instance = this; -} - -AndroidUtils::~AndroidUtils() -{ - Q_ASSERT(s_instance == this); - s_instance = nullptr; -} - -// static -void AndroidUtils::dispatchToMainThread(std::function callback) -{ - QTimer *timer = new QTimer(); - timer->moveToThread(qApp->thread()); - timer->setSingleShot(true); - QObject::connect(timer, &QTimer::timeout, [=]() { - callback(); - timer->deleteLater(); - }); - QMetaObject::invokeMethod(timer, "start", Qt::QueuedConnection); -} - -// static -QByteArray AndroidUtils::getQByteArrayFromJString(JNIEnv *env, jstring data) -{ - const char *buffer = env->GetStringUTFChars(data, nullptr); - if (!buffer) { - qDebug() << "getQByteArrayFromJString - failed to parse data."; - return QByteArray(); - } - - QByteArray out(buffer); - env->ReleaseStringUTFChars(data, buffer); - return out; -} - -// static -QString AndroidUtils::getQStringFromJString(JNIEnv *env, jstring data) -{ - const char *buffer = env->GetStringUTFChars(data, nullptr); - if (!buffer) { - qDebug() << "getQStringFromJString - failed to parse data."; - return QString(); - } - - QString out(buffer); - env->ReleaseStringUTFChars(data, buffer); - return out; -} - -// static -QJsonObject AndroidUtils::getQJsonObjectFromJString(JNIEnv *env, jstring data) -{ - QByteArray raw(getQByteArrayFromJString(env, data)); - QJsonParseError jsonError; - QJsonDocument json = QJsonDocument::fromJson(raw, &jsonError); - if (QJsonParseError::NoError != jsonError.error) { - qDebug() << "getQJsonObjectFromJstring - error parsing json. Code: " << jsonError.error - << "Offset: " << jsonError.offset << "Message: " << jsonError.errorString() << "Data: " << raw; - return QJsonObject(); - } - - if (!json.isObject()) { - qDebug() << "getQJsonObjectFromJString - object expected."; - return QJsonObject(); - } - - return json.object(); -} - -QJniObject AndroidUtils::getActivity() -{ - return QNativeInterface::QAndroidApplication::context(); -} - -int AndroidUtils::GetSDKVersion() -{ - QJniEnvironment env; - jclass versionClass = env->FindClass("android/os/Build$VERSION"); - jfieldID sdkIntFieldID = env->GetStaticFieldID(versionClass, "SDK_INT", "I"); - int sdk = env->GetStaticIntField(versionClass, sdkIntFieldID); - - return sdk; -} - -QString AndroidUtils::GetManufacturer() -{ - QJniEnvironment env; - jclass buildClass = env->FindClass("android/os/Build"); - jfieldID manuFacturerField = env->GetStaticFieldID(buildClass, "MANUFACTURER", "Ljava/lang/String;"); - jstring value = (jstring)env->GetStaticObjectField(buildClass, manuFacturerField); - - const char *buffer = env->GetStringUTFChars(value, nullptr); - - if (!buffer) { - qDebug() << "Failed to fetch MANUFACTURER"; - return QByteArray(); - } - - QString res(buffer); - qDebug() << "MANUFACTURER: " << res; - env->ReleaseStringUTFChars(value, buffer); - return res; -} - -void AndroidUtils::runOnAndroidThreadSync(const std::function runnable) -{ - QNativeInterface::QAndroidApplication::runOnAndroidMainThread(runnable).waitForFinished(); -} - -void AndroidUtils::runOnAndroidThreadAsync(const std::function runnable) -{ - QNativeInterface::QAndroidApplication::runOnAndroidMainThread(runnable); -} - -// Static -// Creates a copy of the passed QByteArray in the JVM and passes back a ref -jbyteArray AndroidUtils::tojByteArray(const QByteArray &data) -{ - QJniEnvironment env; - jbyteArray out = env->NewByteArray(data.size()); - env->SetByteArrayRegion(out, 0, data.size(), reinterpret_cast(data.constData())); - return out; -} diff --git a/client/platforms/android/androidutils.h b/client/platforms/android/androidutils.h deleted file mode 100644 index 8559400c8..000000000 --- a/client/platforms/android/androidutils.h +++ /dev/null @@ -1,49 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -#ifndef ANDROIDUTILS_H -#define ANDROIDUTILS_H - -#include - -#include -#include -#include -#include -#include - -class AndroidUtils final : public QObject -{ - Q_OBJECT - Q_DISABLE_COPY_MOVE(AndroidUtils) - -public: - static QString GetDeviceName(); - - static int GetSDKVersion(); - static QString GetManufacturer(); - - static AndroidUtils* instance(); - - static void dispatchToMainThread(std::function callback); - - static QByteArray getQByteArrayFromJString(JNIEnv* env, jstring data); - - static jbyteArray tojByteArray(const QByteArray& data); - - static QString getQStringFromJString(JNIEnv* env, jstring data); - - static QJsonObject getQJsonObjectFromJString(JNIEnv* env, jstring data); - - static QJniObject getActivity(); - - static void runOnAndroidThreadSync(const std::function runnable); - static void runOnAndroidThreadAsync(const std::function runnable); - -private: - AndroidUtils(QObject* parent); - ~AndroidUtils(); -}; - -#endif // ANDROIDUTILS_H diff --git a/client/platforms/ios/MobileUtils.mm b/client/platforms/ios/MobileUtils.mm index fbf26ffde..58faad886 100644 --- a/client/platforms/ios/MobileUtils.mm +++ b/client/platforms/ios/MobileUtils.mm @@ -43,6 +43,7 @@ bool MobileUtils::shareText(const QStringList& filesToSend) { UIPopoverPresentationController *popController = activityController.popoverPresentationController; if (popController) { popController.sourceView = qtController.view; + popController.sourceRect = CGRectMake(100, 100, 100, 100); } QEventLoop wait; diff --git a/client/platforms/ios/WireGuard-Bridging-Header.h b/client/platforms/ios/WireGuard-Bridging-Header.h index fbccb2d4c..0183367b7 100644 --- a/client/platforms/ios/WireGuard-Bridging-Header.h +++ b/client/platforms/ios/WireGuard-Bridging-Header.h @@ -2,7 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -#include "3rd/awg-apple/Sources/WireGuardKitC/WireGuardKitC.h" +#include "3rd/amneziawg-apple/Sources/WireGuardKitC/WireGuardKitC.h" #include #include diff --git a/client/platforms/ios/ios_controller.mm b/client/platforms/ios/ios_controller.mm index 79eddefdf..2ff657755 100644 --- a/client/platforms/ios/ios_controller.mm +++ b/client/platforms/ios/ios_controller.mm @@ -249,8 +249,107 @@ void IosController::vpnStatusDidChange(void *pNotification) NETunnelProviderSession *session = (NETunnelProviderSession *)pNotification; if (session /* && session == TunnelManager.session */ ) { - qDebug() << "IosController::vpnStatusDidChange" << iosStatusToState(session.status) << session; - emit connectionStateChanged(iosStatusToState(session.status)); + qDebug() << "IosController::vpnStatusDidChange" << iosStatusToState(session.status) << session; + + if (session.status == NEVPNStatusDisconnected) { + if (@available(iOS 16.0, *)) { + [session fetchLastDisconnectErrorWithCompletionHandler:^(NSError * _Nullable error) { + if (error != nil) { + qDebug() << "Disconnect error" << error.domain << error.code << error.localizedDescription; + + if ([error.domain isEqualToString:NEVPNConnectionErrorDomain]) { + switch (error.code) { + case NEVPNConnectionErrorOverslept: + qDebug() << "Disconnect error info" << "The VPN connection was terminated because the system slept for an extended period of time."; + break; + case NEVPNConnectionErrorNoNetworkAvailable: + qDebug() << "Disconnect error info" << "The VPN connection could not be established because the system is not connected to a network."; + break; + case NEVPNConnectionErrorUnrecoverableNetworkChange: + qDebug() << "Disconnect error info" << "The VPN connection was terminated because the network conditions changed in such a way that the VPN connection could not be maintained."; + break; + case NEVPNConnectionErrorConfigurationFailed: + qDebug() << "Disconnect error info" << "The VPN connection could not be established because the configuration is invalid. "; + break; + case NEVPNConnectionErrorServerAddressResolutionFailed: + qDebug() << "Disconnect error info" << "The address of the VPN server could not be determined."; + break; + case NEVPNConnectionErrorServerNotResponding: + qDebug() << "Disconnect error info" << "Network communication with the VPN server has failed."; + break; + case NEVPNConnectionErrorServerDead: + qDebug() << "Disconnect error info" << "The VPN server is no longer functioning."; + break; + case NEVPNConnectionErrorAuthenticationFailed: + qDebug() << "Disconnect error info" << "The user credentials were rejected by the VPN server."; + break; + case NEVPNConnectionErrorClientCertificateInvalid: + qDebug() << "Disconnect error info" << "The client certificate is invalid."; + break; + case NEVPNConnectionErrorClientCertificateNotYetValid: + qDebug() << "Disconnect error info" << "The client certificate will not be valid until some future point in time."; + break; + case NEVPNConnectionErrorClientCertificateExpired: + qDebug() << "Disconnect error info" << "The validity period of the client certificate has passed."; + break; + case NEVPNConnectionErrorPluginFailed: + qDebug() << "Disconnect error info" << "The VPN plugin died unexpectedly."; + break; + case NEVPNConnectionErrorConfigurationNotFound: + qDebug() << "Disconnect error info" << "The VPN configuration could not be found."; + break; + case NEVPNConnectionErrorPluginDisabled: + qDebug() << "Disconnect error info" << "The VPN plugin could not be found or needed to be updated."; + break; + case NEVPNConnectionErrorNegotiationFailed: + qDebug() << "Disconnect error info" << "The VPN protocol negotiation failed."; + break; + case NEVPNConnectionErrorServerDisconnected: + qDebug() << "Disconnect error info" << "The VPN server terminated the connection."; + break; + case NEVPNConnectionErrorServerCertificateInvalid: + qDebug() << "Disconnect error info" << "The server certificate is invalid."; + break; + case NEVPNConnectionErrorServerCertificateNotYetValid: + qDebug() << "Disconnect error info" << "The server certificate will not be valid until some future point in time."; + break; + case NEVPNConnectionErrorServerCertificateExpired: + qDebug() << "Disconnect error info" << "The validity period of the server certificate has passed."; + break; + default: + qDebug() << "Disconnect error info" << "Unknown code."; + break; + } + } + + NSError *underlyingError = error.userInfo[@"NSUnderlyingError"]; + if (underlyingError != nil) { + qDebug() << "Disconnect underlying error" << underlyingError.domain << underlyingError.code << underlyingError.localizedDescription; + + if ([underlyingError.domain isEqualToString:@"NEAgentErrorDomain"]) { + switch (underlyingError.code) { + case 1: + qDebug() << "Disconnect underlying error" << "General. Use sysdiagnose."; + break; + case 2: + qDebug() << "Disconnect underlying error" << "Plug-in unavailable. Use sysdiagnose."; + break; + default: + qDebug() << "Disconnect underlying error" << "Unknown code. Use sysdiagnose."; + break; + } + } + } + } else { + qDebug() << "Disconnect error is absent"; + } + }]; + } else { + qDebug() << "Disconnect error is unavailable on iOS < 16.0"; + } + } + + emit connectionStateChanged(iosStatusToState(session.status)); } } @@ -352,6 +451,15 @@ bool IosController::startWireGuard(const QString &config) void IosController::startTunnel() { + NSString *protocolName = @"Unknown"; + + NETunnelProviderProtocol *tunnelProtocol = (NETunnelProviderProtocol *)m_currentTunnel.protocolConfiguration; + if (tunnelProtocol.providerConfiguration[@"wireguard"] != nil) { + protocolName = @"WireGuard"; + } else if (tunnelProtocol.providerConfiguration[@"ovpn"] != nil) { + protocolName = @"OpenVPN"; + } + m_rxBytes = 0; m_txBytes = 0; @@ -373,7 +481,7 @@ void IosController::startTunnel() [m_currentTunnel loadFromPreferencesWithCompletionHandler:^(NSError *loadError) { if (loadError) { - qDebug() << "IosController::startOpenVPN : Connect OpenVPN Tunnel Load Error" << loadError.localizedDescription.UTF8String; + qDebug().nospace() << "IosController::start" << protocolName << ": Connect " << protocolName << " Tunnel Load Error" << loadError.localizedDescription.UTF8String; emit connectionStateChanged(Vpn::ConnectionState::Error); return; } @@ -401,11 +509,11 @@ void IosController::startTunnel() BOOL started = [m_currentTunnel.connection startVPNTunnelWithOptions:nil andReturnError:&startError]; if (!started || startError) { - qDebug() << "IosController::startOpenVPN : Connect OpenVPN Tunnel Start Error" + qDebug().nospace() << "IosController::start" << protocolName << " : Connect " << protocolName << " Tunnel Start Error" << (startError ? startError.localizedDescription.UTF8String : ""); emit connectionStateChanged(Vpn::ConnectionState::Error); } else { - qDebug() << "IosController::startOpenVPN : Starting the tunnel succeeded"; + qDebug().nospace() << "IosController::start" << protocolName << " : Starting the tunnel succeeded"; } }]; }); diff --git a/client/secure_qsettings.h b/client/secure_qsettings.h index 7421ce019..89147fc11 100644 --- a/client/secure_qsettings.h +++ b/client/secure_qsettings.h @@ -14,12 +14,14 @@ constexpr const char *keyChainName = "AmneziaVPN-Keychain"; class SecureQSettings : public QObject { + Q_OBJECT + public: explicit SecureQSettings(const QString &organization, const QString &application = QString(), QObject *parent = nullptr); - QVariant value(const QString &key, const QVariant &defaultValue = QVariant()) const; - void setValue(const QString &key, const QVariant &value); + Q_INVOKABLE QVariant value(const QString &key, const QVariant &defaultValue = QVariant()) const; + Q_INVOKABLE void setValue(const QString &key, const QVariant &value); void remove(const QString &key); void sync(); diff --git a/client/server_scripts/check_server_is_busy.sh b/client/server_scripts/check_server_is_busy.sh index e1ba63f81..4e6a2c268 100644 --- a/client/server_scripts/check_server_is_busy.sh +++ b/client/server_scripts/check_server_is_busy.sh @@ -1,5 +1,6 @@ if which apt-get > /dev/null 2>&1; then LOCK_FILE="/var/lib/dpkg/lock-frontend";\ elif which dnf > /dev/null 2>&1; then LOCK_FILE="/var/run/dnf.pid";\ elif which yum > /dev/null 2>&1; then LOCK_FILE="/var/run/yum.pid";\ +elif which pacman > /dev/null 2>&1; then LOCK_FILE="/var/lib/pacman/db.lck";\ else echo "Packet manager not found"; echo "Internal error"; exit 1; fi;\ -if command -v fuser > /dev/null 2>&1; then sudo fuser $LOCK_FILE 2>/dev/null; else echo "fuser not installed"; fi +if command -v fuser > /dev/null 2>&1; then sudo fuser $LOCK_FILE 2>/dev/null; else echo "fuser not installed"; fi diff --git a/client/server_scripts/install_docker.sh b/client/server_scripts/install_docker.sh index 6f19e0900..6fed78c0d 100644 --- a/client/server_scripts/install_docker.sh +++ b/client/server_scripts/install_docker.sh @@ -1,6 +1,7 @@ if which apt-get > /dev/null 2>&1; then pm=$(which apt-get); silent_inst="-yq install"; check_pkgs="-yq update"; docker_pkg="docker.io"; dist="debian";\ elif which dnf > /dev/null 2>&1; then pm=$(which dnf); silent_inst="-yq install"; check_pkgs="-yq check-update"; docker_pkg="docker"; dist="fedora";\ elif which yum > /dev/null 2>&1; then pm=$(which yum); silent_inst="-y -q install"; check_pkgs="-y -q check-update"; docker_pkg="docker"; dist="centos";\ +elif which pacman > /dev/null 2>&1; then pm=$(which pacman); silent_inst="-S --noconfirm --noprogressbar --quiet"; check_pkgs="> /dev/null 2>&1"; docker_pkg="docker"; dist="archlinux";\ else echo "Packet manager not found"; exit 1; fi;\ echo "Dist: $dist, Packet manager: $pm, Install command: $silent_inst, Check pkgs command: $check_pkgs, Docker pkg: $docker_pkg";\ if [ "$dist" = "debian" ]; then export DEBIAN_FRONTEND=noninteractive; fi;\ diff --git a/client/settings.cpp b/client/settings.cpp index 4757dba6e..475c52e71 100644 --- a/client/settings.cpp +++ b/client/settings.cpp @@ -1,4 +1,8 @@ #include "settings.h" + +#include "QThread" +#include "QCoreApplication" + #include "utilities.h" #include "version.h" @@ -12,10 +16,10 @@ Settings::Settings(QObject *parent) : QObject(parent), m_settings(ORGANIZATION_N { // Import old settings if (serversCount() == 0) { - QString user = m_settings.value("Server/userName").toString(); - QString password = m_settings.value("Server/password").toString(); - QString serverName = m_settings.value("Server/serverName").toString(); - int port = m_settings.value("Server/serverPort").toInt(); + QString user = value("Server/userName").toString(); + QString password = value("Server/password").toString(); + QString serverName = value("Server/serverName").toString(); + int port = value("Server/serverPort").toInt(); if (!user.isEmpty() && !password.isEmpty() && !serverName.isEmpty()) { QJsonObject server; @@ -211,7 +215,8 @@ QString Settings::nextAvailableServerName() const void Settings::setSaveLogs(bool enabled) { - m_settings.setValue("Conf/saveLogs", enabled); + setValue("Conf/saveLogs", enabled); +#ifndef Q_OS_ANDROID if (!isSaveLogs()) { Logger::deInit(); } else { @@ -219,7 +224,8 @@ void Settings::setSaveLogs(bool enabled) qWarning() << "Initialization of debug subsystem failed"; } } - emit saveLogsChanged(); +#endif + emit saveLogsChanged(enabled); } QString Settings::routeModeString(RouteMode mode) const @@ -233,7 +239,7 @@ QString Settings::routeModeString(RouteMode mode) const Settings::RouteMode Settings::routeMode() const { - return static_cast(m_settings.value("Conf/routeMode", 0).toInt()); + return static_cast(value("Conf/routeMode", 0).toInt()); } bool Settings::addVpnSite(RouteMode mode, const QString &site, const QString &ip) @@ -321,12 +327,12 @@ void Settings::removeAllVpnSites(RouteMode mode) QString Settings::primaryDns() const { - return m_settings.value("Conf/primaryDns", cloudFlareNs1).toString(); + return value("Conf/primaryDns", cloudFlareNs1).toString(); } QString Settings::secondaryDns() const { - return m_settings.value("Conf/secondaryDns", cloudFlareNs2).toString(); + return value("Conf/secondaryDns", cloudFlareNs2).toString(); } void Settings::clearSettings() @@ -351,3 +357,30 @@ ServerCredentials Settings::serverCredentials(int index) const return credentials; } + +QVariant Settings::value(const QString &key, const QVariant &defaultValue) const +{ + QVariant returnValue; + if (QThread::currentThread() == QCoreApplication::instance()->thread()) { + returnValue = m_settings.value(key, defaultValue); + } else { + QMetaObject::invokeMethod(&m_settings, "value", + Qt::BlockingQueuedConnection, + Q_RETURN_ARG(QVariant, returnValue), + Q_ARG(const QString&, key), + Q_ARG(const QVariant&, defaultValue)); + } + return returnValue; +} + +void Settings::setValue(const QString &key, const QVariant &value) +{ + if (QThread::currentThread() == QCoreApplication::instance()->thread()) { + m_settings.setValue(key, value); + } else { + QMetaObject::invokeMethod(&m_settings, "setValue", + Qt::BlockingQueuedConnection, + Q_ARG(const QString&, key), + Q_ARG(const QVariant&, value)); + } +} diff --git a/client/settings.h b/client/settings.h index f530f6c52..ed3026535 100644 --- a/client/settings.h +++ b/client/settings.h @@ -29,11 +29,11 @@ public: QJsonArray serversArray() const { - return QJsonDocument::fromJson(m_settings.value("Servers/serversList").toByteArray()).array(); + return QJsonDocument::fromJson(value("Servers/serversList").toByteArray()).array(); } void setServersArray(const QJsonArray &servers) { - m_settings.setValue("Servers/serversList", QJsonDocument(servers).toJson()); + setValue("Servers/serversList", QJsonDocument(servers).toJson()); } // Servers section @@ -45,11 +45,11 @@ public: int defaultServerIndex() const { - return m_settings.value("Servers/defaultServerIndex", 0).toInt(); + return value("Servers/defaultServerIndex", 0).toInt(); } void setDefaultServer(int index) { - m_settings.setValue("Servers/defaultServerIndex", index); + setValue("Servers/defaultServerIndex", index); } QJsonObject defaultServer() const { @@ -78,25 +78,25 @@ public: // App settings section bool isAutoConnect() const { - return m_settings.value("Conf/autoConnect", false).toBool(); + return value("Conf/autoConnect", false).toBool(); } void setAutoConnect(bool enabled) { - m_settings.setValue("Conf/autoConnect", enabled); + setValue("Conf/autoConnect", enabled); } bool isStartMinimized() const { - return m_settings.value("Conf/startMinimized", false).toBool(); + return value("Conf/startMinimized", false).toBool(); } void setStartMinimized(bool enabled) { - m_settings.setValue("Conf/startMinimized", enabled); + setValue("Conf/startMinimized", enabled); } bool isSaveLogs() const { - return m_settings.value("Conf/saveLogs", false).toBool(); + return value("Conf/saveLogs", false).toBool(); } void setSaveLogs(bool enabled); @@ -110,15 +110,15 @@ public: QString routeModeString(RouteMode mode) const; RouteMode routeMode() const; - void setRouteMode(RouteMode mode) { m_settings.setValue("Conf/routeMode", mode); } + void setRouteMode(RouteMode mode) { setValue("Conf/routeMode", mode); } QVariantMap vpnSites(RouteMode mode) const { - return m_settings.value("Conf/" + routeModeString(mode)).toMap(); + return value("Conf/" + routeModeString(mode)).toMap(); } void setVpnSites(RouteMode mode, const QVariantMap &sites) { - m_settings.setValue("Conf/" + routeModeString(mode), sites); + setValue("Conf/" + routeModeString(mode), sites); m_settings.sync(); } bool addVpnSite(RouteMode mode, const QString &site, const QString &ip = ""); @@ -132,11 +132,11 @@ public: bool useAmneziaDns() const { - return m_settings.value("Conf/useAmneziaDns", true).toBool(); + return value("Conf/useAmneziaDns", true).toBool(); } void setUseAmneziaDns(bool enabled) { - m_settings.setValue("Conf/useAmneziaDns", enabled); + setValue("Conf/useAmneziaDns", enabled); } QString primaryDns() const; @@ -145,13 +145,13 @@ public: // QString primaryDns() const { return m_primaryDns; } void setPrimaryDns(const QString &primaryDns) { - m_settings.setValue("Conf/primaryDns", primaryDns); + setValue("Conf/primaryDns", primaryDns); } // QString secondaryDns() const { return m_secondaryDns; } void setSecondaryDns(const QString &secondaryDns) { - m_settings.setValue("Conf/secondaryDns", secondaryDns); + setValue("Conf/secondaryDns", secondaryDns); } static const char cloudFlareNs1[]; @@ -171,29 +171,32 @@ public: QLocale getAppLanguage() { - return m_settings.value("Conf/appLanguage", QLocale()).toLocale(); + return value("Conf/appLanguage", QLocale()).toLocale(); }; void setAppLanguage(QLocale locale) { - m_settings.setValue("Conf/appLanguage", locale); + setValue("Conf/appLanguage", locale); }; bool isScreenshotsEnabled() const { - return m_settings.value("Conf/screenshotsEnabled", false).toBool(); + return value("Conf/screenshotsEnabled", false).toBool(); } void setScreenshotsEnabled(bool enabled) { - m_settings.setValue("Conf/screenshotsEnabled", enabled); + setValue("Conf/screenshotsEnabled", enabled); } void clearSettings(); signals: - void saveLogsChanged(); + void saveLogsChanged(bool enabled); private: - SecureQSettings m_settings; + QVariant value(const QString &key, const QVariant &defaultValue = QVariant()) const; + void setValue(const QString &key, const QVariant &value); + + mutable SecureQSettings m_settings; }; #endif // SETTINGS_H diff --git a/client/translations/amneziavpn_fa_IR.ts b/client/translations/amneziavpn_fa_IR.ts index 7471208b7..e843f8071 100644 --- a/client/translations/amneziavpn_fa_IR.ts +++ b/client/translations/amneziavpn_fa_IR.ts @@ -15,15 +15,13 @@ AndroidController - AmneziaVPN - AmneziaVPN + AmneziaVPN - VPN Connected Refers to the app - which is currently running the background and waiting - وی‎پی‎ان متصل است + وی‎پی‎ان متصل است @@ -31,7 +29,7 @@ Error when retrieving configuration from cloud server - + خطا در حین دریافت پیکربندی از سمت سرور @@ -46,7 +44,7 @@ Connection... - ارتباط + در حال ارتباط... @@ -62,12 +60,12 @@ Settings updated successfully - + تنظیمات با موفقیت به‎روز‎رسانی شدند Reconnection... - در حال اتصال دوباره... + اتصال دوباره... @@ -152,7 +150,7 @@ ImportController - + Scanned %1 of %2. ارزیابی %1 از %2. @@ -245,12 +243,12 @@ Already installed containers were found on the server. All installed containers VPN Connected - وی‎پی‎ان متصل است + وی‎پی‎ان وصل شد VPN Disconnected - وی‎پی‎ان قطع است + وی‎پی‎ان قطع شد @@ -948,6 +946,11 @@ Already installed containers were found on the server. All installed containers Show other methods on Github نمایش متد‎های دیگر در گیت هاب + + + https://github.com/amnezia-vpn/amnezia-client#donate + + Contacts @@ -1430,22 +1433,22 @@ Already installed containers were found on the server. All installed containers نام سرور - + Save ذخیره - + Protocols پروتکل‎ها - + Services سرویس‎ها - + Data داده @@ -1682,7 +1685,7 @@ and will not be shared or disclosed to the Amnezia or any third parties All data you enter will remain strictly confidential and will not be shared or disclosed to the Amnezia or any third parties - + تمام داده‎هایی که شما وارد می‎کنید به شدت محرمانه‎ است و با Amnezia یا هر شخص ثالث دیگری به اشتراک گذاشته نمی‎شود @@ -1766,7 +1769,7 @@ and will not be shared or disclosed to the Amnezia or any third parties Cancel installation - + لغو عملیات نصب @@ -1856,6 +1859,11 @@ and will not be shared or disclosed to the Amnezia or any third parties I have nothing من هیچی ندارم + + + https://amnezia.org/instructions/0_starter-guide + + PageSetupWizardTextKey @@ -1918,12 +1926,12 @@ and will not be shared or disclosed to the Amnezia or any third parties OpenVpn native format - فرمت محلی OpenVPN + فرمت OpenVPN WireGuard native format - فرمت محلی WireGuard + فرمت WireGuard VPN Access @@ -1943,8 +1951,8 @@ and will not be shared or disclosed to the Amnezia or any third parties Доступ к управлению сервером. Пользователь, с которым вы делитесь полным доступом к соединению, сможет добавлять и удалять ваши протоколы и службы на сервере, а также изменять настройки. - + Server سرور @@ -1959,7 +1967,7 @@ and will not be shared or disclosed to the Amnezia or any third parties Config revoked - + تنظیمات ابطال‎شد @@ -1984,12 +1992,12 @@ and will not be shared or disclosed to the Amnezia or any third parties Save ShadowSocks config - + ذخیره تنظیمات ShadowSocks Save Cloak config - + ذخیره تنظیمات Cloak @@ -1999,12 +2007,12 @@ and will not be shared or disclosed to the Amnezia or any third parties ShadowSocks native format - + فرمت ShadowSocks Cloak native format - + فرمت Cloak @@ -2014,68 +2022,77 @@ and will not be shared or disclosed to the Amnezia or any third parties Share full access to the server and VPN - + به اشتراک گذاشتن دسترسی کامل به سرور و وی‎پی‎ان Use for your own devices, or share with those you trust to manage the server. - + برای دستگاه‎های خودتان استفاده کنید یا با آنهایی که برای مدیریت سرور به آن‎ها اعتماد دارید به اشتراک بگذارید. - + Users - + کاربران User name - + نام کاربری - + Search + جستجو + + + + Creation date: - + Rename - + تغییر نام - + Client name - + نام کلاینت - + Save - ذخیره + ذخیره - + Revoke + ابطال + + + + Revoke the config for a user - %1? - Revoke the config for a user - - + ابطال تنظیمات برای کاربر - + The user will no longer be able to connect to your server. - + کاربر دیگر نمی‎تواند به سرور وصل شود. - + Continue - + ادامه - + Cancel - کنسل + کنسل Full access @@ -2091,20 +2108,20 @@ and will not be shared or disclosed to the Amnezia or any third parties به اشتراک گذاری دسترسی به مدیریت سرور. کاربری که دسترسی کامل سرور با او به اشتراک گذاشته می‎شود می‎تواند پروتکل‌‎ها و سرویس‎ها را در سرور حذف یا اضافه کند و یا تنظیمات سرور را تغییر دهد. - + Protocol پروتکل - + Connection format فرمت ارتباط - + Share اشتراک‎گذاری @@ -2114,49 +2131,50 @@ and will not be shared or disclosed to the Amnezia or any third parties Full access to the server and VPN - + دسترسی کامل به سرور و وی‎پی‎ان We recommend that you use full access to the server only for your own additional devices. - + ما پیشنهاد میکنیم که ازحالت دسترسی کامل به سرور فقط برای دستگاه‎های دیگر خودتان استفاده کنید + If you share full access with other people, they can remove and add protocols and services to the server, which will cause the VPN to work incorrectly for all users. - + اگر دسترسی کامل را با دیگران به اشتراک بگذارید، آن‎ها می‎توانند پروتکل‎ها و سرویس‎ها را حذف یا اضافه کنند که باعث می‎شود که وی‎پی‎ان دیگر برای سایر کاربران کار نکند. Server - + سرور - + Accessing - در حال دسترسی به + در حال دسترسی به - + File with accessing settings to - فایل شامل تنظیمات دسترسی به + فایل شامل تنظیمات دسترسی به Share - اشتراک‎گذاری + اشتراک‎گذاری Connection to - ارتباط با + ارتباط با File with connection settings to - فایل شامل تنظیمات ارتباط با + فایل شامل تنظیمات ارتباط با @@ -2170,38 +2188,38 @@ and will not be shared or disclosed to the Amnezia or any third parties QKeychain::DeletePasswordJobPrivate - + Password entry not found Password entry not found - + Could not decrypt data Could not decrypt data - - + + Unknown error Unknown error - + Could not open wallet: %1; %2 Could not open wallet: %1; %2 - + Password not found Password not found - + Could not open keystore Could not open keystore - + Could not remove private key from keystore Could not remove private key from keystore @@ -2209,12 +2227,12 @@ and will not be shared or disclosed to the Amnezia or any third parties QKeychain::JobPrivate - + Unknown error Unknown error - + Access to keychain denied Access to keychain denied @@ -2222,27 +2240,27 @@ and will not be shared or disclosed to the Amnezia or any third parties QKeychain::PlainTextStore - + Could not store data in settings: access error Could not store data in settings: access error - + Could not store data in settings: format error Could not store data in settings: format error - + Could not delete data from settings: access error Could not delete data from settings: access error - + Could not delete data from settings: format error Could not delete data from settings: format error - + Entry not found Entry not found @@ -2250,80 +2268,80 @@ and will not be shared or disclosed to the Amnezia or any third parties QKeychain::ReadPasswordJobPrivate - + Password entry not found Password entry not found - - + + Could not decrypt data Could not decrypt data - + D-Bus is not running D-Bus is not running - - + + Unknown error Unknown error - + No keychain service available No keychain service available - + Could not open wallet: %1; %2 Could not open wallet: %1; %2 - + Access to keychain denied Access to keychain denied - + Could not determine data type: %1; %2 Could not determine data type: %1; %2 - - + + Entry not found Entry not found - + Unsupported entry type 'Map' Unsupported entry type 'Map' - + Unknown kwallet entry type '%1' Unknown kwallet entry type '%1' - + Password not found Password not found - + Could not open keystore Could not open keystore - + Could not retrieve private key from keystore Could not retrieve private key from keystore - + Could not create decryption cipher Could not create decryption cipher @@ -2331,73 +2349,73 @@ and will not be shared or disclosed to the Amnezia or any third parties QKeychain::WritePasswordJobPrivate - + Credential size exceeds maximum size of %1 Credential size exceeds maximum size of %1 - + Credential key exceeds maximum size of %1 Credential key exceeds maximum size of %1 - + Writing credentials failed: Win32 error code %1 Writing credentials failed: Win32 error code %1 - + Encryption failed Encryption failed - + D-Bus is not running D-Bus is not running - - + + Unknown error Unknown error - + Could not open wallet: %1; %2 Could not open wallet: %1; %2 - + Password not found Password not found - + Could not open keystore Could not open keystore - + Could not create private key generator Could not create private key generator - + Could not generate new private key Could not generate new private key - + Could not retrieve private key from keystore Could not retrieve private key from keystore - + Could not create encryption cipher Could not create encryption cipher - + Could not encrypt data Could not encrypt data @@ -2547,6 +2565,11 @@ and will not be shared or disclosed to the Amnezia or any third parties The config does not contain any containers and credentials for connecting to the server + تنظیمات شامل هیچ کانتینر یا اعتبارنامه‎ای برای اتصال به سرور نیست + + + + VPN connection error @@ -2608,7 +2631,7 @@ and will not be shared or disclosed to the Amnezia or any third parties The config does not contain any containers and credentiaks for connecting to the server - + Internal error Internal error @@ -2628,31 +2651,31 @@ While it offers a blend of security, stability, and speed, it's essential t * Minimal configuration * Recognised by DPI analysis systems * Works over UDP network protocol, ports 500 and 4500. - IKEv2 в сочетании с уровнем шифрования IPSec это современный и стабильный протокол VPN. -Он может быстро переключаться между сетями и устройствами, что делает его особенно адаптивным в динамичных сетевых средах. -Несмотря на сочетание безопасности, стабильности и скорости, необходимо отметить, что IKEv2 легко обнаруживается и подвержен блокировке. + پروتکل IKEv2 به همراه لایه رمزنگاری IPSec به عنوان پروتکل وی‎پی‎ان مدرن و پایدار است. +یکی از قابلیت‎‎های متمایز این پروتکل قابلیت سوییچ بین شبکه‎ها و دستگاه‎هاست که قابلیت انطباق بالایی در محیط شبکه‎های دینامیک را دارد +در حالیکه ترکیبی از امنیت، پایداری و سرعت را ارائه میدهد اما مهم است که اشاره کنیم IKEv2 به راحتی قابل تشخیص در شبکه و بلاک شدن میباشد. -* Доступно в AmneziaVPN только для Windows. -* Низкое энергопотребление, на мобильных устройствах -* Минимальная конфигурация -* Распознается системами DPI-анализа -* Работает по сетевому протоколу UDP, порты 500 и 4500. +* در AmneziaVPN فقط بر روی ویندوز در دسترس است +* مصرف باتری کم روی دستگاه‎های موبایل +* تنظیمات ساده +* امکان شناسایی شدن در شبکه‎های تحلیل DPI +* روی پروتکل شبکه UDP، پورت‎های 500 و 4500 کار می‎کند. DNS Service - DNS Сервис + سرویس DNS Sftp file sharing service - Сервис обмена файлами Sftp + سرویس اشتراک گذاری فایل Sftp Website in Tor network - Веб-сайт в сети Tor + وب سایت در شبکه Tor @@ -2662,47 +2685,47 @@ While it offers a blend of security, stability, and speed, it's essential t OpenVPN is the most popular VPN protocol, with flexible configuration options. It uses its own security protocol with SSL/TLS for key exchange. - OpenVPN - популярный VPN-протокол, с гибкой настройкой. Имеет собственный протокол безопасности с SSL/TLS для обмена ключами. + پروتکل OpenVPN یکی از پروتکل‎های وی‎پی‎ان محبوب می‎باشد با تنظیمات و پیکربندی‎های قابل تغییر. از پروتکل امنیتی داخلی خود با تبادل کلید SSL/TLS استفاده می‎کند. ShadowSocks - masks VPN traffic, making it similar to normal web traffic, but is recognised by analysis systems in some highly censored regions. - ShadowSocks - маскирует VPN-трафик под обычный веб-трафик, но распознается системами анализа в некоторых регионах с высоким уровнем цензуры. + پروتکل ShadowSocks ترافیک وی‎پی‎ان را پنهان و آن را شبیه ترافیک عادی وب می‎کند، اما در مناطقی که سانسور شدیدی اعمال می‎شود با سیستم‎های تحلیلی قابل شناسایی است. OpenVPN over Cloak - OpenVPN with VPN masquerading as web traffic and protection against active-probbing detection. Ideal for bypassing blocking in regions with the highest levels of censorship. - OpenVPN over Cloak - OpenVPN с маскировкой VPN под web-трафик и защитой от обнаружения active-probbing. Подходит для регионов с самым высоким уровнем цензуры. + پروتکل OpenVPN over Cloak که همان پروتکل OpenVPN با قابلیت پنهان کردن ترافیک از سیستم‎های تحلیل فعال برروی شبکه. ایده‎آل برای گذر از ممنوعیت در مناطقی که سانسور شدیدی اعمال می‎کنند. WireGuard - New popular VPN protocol with high performance, high speed and low power consumption. Recommended for regions with low levels of censorship. - WireGuard - Популярный VPN-протокол с высокой производительностью, высокой скоростью и низким энергопотреблением. Для регионов с низким уровнем цензуры. + پروتکل WireGuard یک پروتکل وی‎پی‎ان جدید با عملکرد بسیار خوب، سرعت بالا و مصرف انرژی پایین. برای مناطقی که سطح سانسور پایینی دارند پیشنهاد می‎شود. AmneziaWG - Special protocol from Amnezia, based on WireGuard. It's fast like WireGuard, but very resistant to blockages. Recommended for regions with high levels of censorship. - AmneziaWG - Специальный протокол от Amnezia, основанный на протоколе WireGuard. Он такой же быстрый, как WireGuard, но очень устойчив к блокировкам. Рекомендуется для регионов с высоким уровнем цензуры. + پروتکل AmneziaWG یک پروتکل اختصاصی Amnezia که بر اساس WireGaurd کار میکند. به اندازه WireGaurd پرسرعت است و در عین حال بسیار مقاوم به بلاک شدن توسط شبکه ست. مناسب برای مناطق با سطح سانسور بالاست. IKEv2 - Modern stable protocol, a bit faster than others, restores connection after signal loss. It has native support on the latest versions of Android and iOS. - IKEv2 Современный стабильный протокол, немного быстрее других восстанавливает соединение после потери сигнала. Имеет нативную поддержку последних версиий Android и iOS. + پروتکل IKEv2 پروتکلی پایدار و مدرن که مقداری سریعتر از سایر پروتکل‎هاست. بعد از قطع سیگنال دوباره اتصال را بازیابی می‎کند. به صورت پیش‎فرض بر روی آخرین نسخه دستگاه‎های اندروید و iOS پیشتیبانی می‎شود. Deploy a WordPress site on the Tor network in two clicks. - Разверните сайт на WordPress в сети Tor в два клика. + با دو کلیک یک سایت وردپرس در شبکه Tor راه‎اندازی کنید. Replace the current DNS server with your own. This will increase your privacy level. - Замените DNS-сервер на Amnezia DNS. Это повысит уровень конфиденциальности. + سرور DNS را با مال خودتان جایگزین کنید. این کار سطح حریم خصوصی شما را افزایش می‎دهد. Creates a file vault on your server to securely store and transfer files. - Создайте на сервере файловое хранилище для безопасного хранения и передачи файлов. + یک محفظه ایمن بر روی سرور خودتان ایجاد کنید که به طور امن بتوانید فایل‎ها را ذخیره و جابجا کنید. @@ -2714,14 +2737,18 @@ It employs its unique security protocol, leveraging the strength of SSL/TLS for * Flexible customisation to suit user needs to work with different operating systems and devices * Recognised by DPI analysis systems and therefore susceptible to blocking * Can operate over both TCP and UDP network protocols. - OpenVPN однин из самых популярных и проверенных временем VPN-протоколов. -В нем используется уникальный протокол безопасности, опирающийся на протокол SSL/TLS для шифрования и обмена ключами. Кроме того, поддержка OpenVPN множества методов аутентификации делает его универсальным и адаптируемым к широкому спектру устройств и операционных систем. Благодаря открытому исходному коду OpenVPN подвергается тщательному анализу со стороны мирового сообщества, что постоянно повышает его безопасность. Благодаря оптимальному соотношению производительности, безопасности и совместимости OpenVPN остается лучшим выбором как для частных лиц, так и для компаний, заботящихся о конфиденциальности. + پروتکل OpenVPN یکی از پروتکل‎های محبوب و تست شده در دسترس می‎باشد که از پروتکل امنیتی مخصوص خودش استفاده میکند. +از امتیازات SSL/TLS برای رمزنگاری و تبادل کلید استفاده میکند. +همچنین OpenVPN از روش‎های چندگانه‎ای برای احراز هویت پشتیبانی می‎کند که آن را قابل انطباق و منعطف میکند. +از طیف وسیعی از دستگاه‎ها و سیستم عامل‎ها نیز پشتیبانی می‎کند. +به دلیل طبیعت متن-باز آن، OpenVPN از بررسی گسترده توسط یک جامعه جهانی سود می‎برد که باعث بهتر شدن وضعیت امنیتی آن می‎شود. +به دلیل تعادل قوی بین عملکرد، امنیت و سازگاری OpenVPN تبدیل به یکی از انتخاب‎های اصلی برای اشخاص آگاه بر حریم خصوصی و تجارت‎های مشابه شده است. -* Доступность AmneziaVPN для всех платформ -* Нормальное энергопотребление на мобильных устройствах -* Гибкая настройка под нужды пользователя для работы с различными операционными системами и устройствами -* Распознается системами DPI-анализа и поэтому подвержен блокировке -* Может работать по сетевым протоколам TCP и UDP. +* بر روی تمام سیستم‎عامل‎ها در AmneziaVPN در دسترس است. +* مصرف انرژی عادی بر روی دستگاه‎های موبایل +* قابلیت شخصی‎سازی منعطف مطابق با نیاز شما که امکان کار بر روی دستگاه‎ها و سیستم عامل‎های مختلف را می‎دهد. +* قابل شناسایی توسط سیستم‎های تحلیل عمیق DPI در شبکه و در نتیجه امکان بلاک شدن +* امکان کار بر روی دو پروتکل TCP و UDP @@ -2733,12 +2760,13 @@ It employs its unique security protocol, leveraging the strength of SSL/TLS for * Configurable encryption protocol * Detectable by some DPI systems * Works over TCP network protocol. - Shadowsocks, создан на основе протокола SOCKS5, защищает соединение с помощью шифра AEAD. Несмотря на то, что протокол Shadowsocks разработан таким образом, чтобы быть незаметным и сложным для идентификации, он не идентичен стандартному HTTPS-соединению. Однако некоторые системы анализа трафика все же могут обнаружить соединение Shadowsocks. В связи с ограниченной поддержкой в Amnezia рекомендуется использовать протокол AmneziaWG, или OpenVPN over Cloak. + پروتکل Shadowsocks، الهام گرفته از پروتکل Socks5، اتصال را با استفاده از رمزگذاری AEAD امن میکند. اگرچه Shadowsocks طوری طراحی شده که برای شناسایی در شبکه چالش‎برانگیز باشد و محتاط عمل کند اما این پروتکل مانند یک اتصال استاندارد HTTPS نیست و برخی از سیستم‎های تحلیل ترافیک مشخص ممکن است بتوانند اتصال Shadowsocks را شناسایی کنند. به دلیل محدودیت پشتیبانی در Amnezia پیشنهاد می‎شود که از َAmneziaWG استفاده شود. -* Доступен в AmneziaVPN только на ПК ноутбуках. -* Настраиваемый протокол шифрования -* Обнаруживается некоторыми DPI-системами -* Работает по сетевому протоколу TCP. +* فقط بر روی پلتفرم دسکتاپ بر روی Amnezia قابل دسترس است +* مصرف انرژی عادی در دستگاه‎های موبایل +* پروتکل رمزنگاری قابل پیکربندی +* قابل شناسایی توسط برخی سیستم‎های تحلیل عمیق DPI +* عملکرد بر روی پروتکل شبکه TCP @@ -2760,23 +2788,24 @@ If there is a extreme level of Internet censorship in your region, we advise you * Not recognised by DPI analysis systems * Works over TCP network protocol, 443 port. - OpenVPN over Cloak - это комбинация протокола OpenVPN и плагина Cloak, разработанного специально для защиты от блокировок. + این یک ترکیب از پروتکل OpenVPN و افزونه Cloak می‎باشد که به طور خاص برای محافظت از بلاک شدن طراحی شده است. -OpenVPN обеспечивает безопасное VPN-соединение за счет шифрования всего интернет-трафика между клиентом и сервером. +پروتکل OpenVPN با رمزنگاری تمام ترافیک اینترنت بین دستگاه و سرور یک اتصال وی‎پی‎ان امن را فراهم می‎کند. -Cloak защищает OpenVPN от обнаружения и блокировок. +افزونه Cloak از OpenVPN در مقابل شناسایی و بلاک شدن محافظت می‎کند -Cloak может изменять метаданные пакетов. Он полностью маскирует VPN-трафик под обычный веб-трафик, а также защищает VPN от обнаружения с помощью Active Probing. Это делает его очень устойчивым к обнаружению +افزونه Cloak می‎تواند داده‎های بسته ترافیکی را تغییر دهد و در نتیجه ترافیک وی‎پی‎ان شبیه ترافیک عادی وب می‎شود و همچنین از وی‎پی‎ان در مقابل شناسایی شدن توسط DPI محافظت می‎کند. این باعث می‎شود که این پروتکل به شناسایی‎شدن بسیار مقاوم باشد -Сразу же после получения первого пакета данных Cloak проверяет подлинность входящего соединения. Если аутентификация не проходит, плагин маскирует сервер под поддельный сайт, и ваш VPN становится невидимым для аналитических систем. +درست بعد از دریافت اولین بسته داده،افزونه Cloak اتصال ورودی را احراز هویت می‎کند و اگر عملیات احراز هویت انجام نشود Cloak سرور را به عنوان یک وب سایت جعلی در‎ می‎آورد و وی‎پی‎ان شما را از تحلیل شبکه پنهان می‎کند. -Если в вашем регионе существует экстремальный уровень цензуры в Интернете, мы советуем вам при первом подключении использовать только OpenVPN через Cloak +اگر در منطقه شما سطح بالایی از سانسور وجود دارد ما به شما پیشنهاد می‎کنیم از اولین ارتباط تنها از OpenVPN over Cloak استفاده کنید. -* Доступность AmneziaVPN на всех платформах -* Высокое энергопотребление на мобильных устройствах -* Гибкие настройки -* Не распознается системами DPI-анализа -* Работает по сетевому протоколу TCP, 443 порт. + +* بر روی تمام پلتفرم‎ها در AmneziaVPN در دسترس است +* مصرف بالای انرژی در دستگاه‎های موبایل +* تنظیمات منطعف +* غیرقابل تشخیص و شناسایی توسط سیستم‎های تحلیل عمیق DPI +* کار کردن روی پروتکل شبکه TCP، پورت 443 @@ -2790,15 +2819,15 @@ WireGuard is very susceptible to blocking due to its distinct packet signatures. * Minimum number of settings * Easily recognised by DPI analysis systems, susceptible to blocking * Works over UDP network protocol. - WireGuard - относительно новый популярный VPN-протокол с упрощенной архитектурой. -Обеспечивает стабильное VPN-соединение, высокую производительность на всех устройствах. Использует жестко заданные настройки шифрования. WireGuard по сравнению с OpenVPN имеет меньшую задержку и лучшую пропускную способность при передаче данных. -WireGuard очень восприимчив к блокированию из-за особенностей сигнатур пакетов. В отличие от некоторых других VPN-протоколов, использующих методы обфускации, последовательные сигнатуры пакетов WireGuard легче выявляются и, соответственно, блокируются современными системами глубокой проверки пакетов (DPI) и другими средствами сетевого мониторинга. + یک پروتکل نسبتا محبوب وی‎پی‎ان با معماری ساده +اتصال وی‎پی‎‎ان پایدار با عملکرد بالا بر روی تمام دستگاه‎‌ها فراهم می‎کند. از تنظیمات ثابت برای رمزنگاری استفاده می‎کند و در مقایسه با OpenVPN سرعت بهتری در انتقال اطلاعات دارد. +پروتکل WireGaurd به دلیل امضای بسته داده مخصوص، احتمال بسیار بالایی برای شناسایی و بلاک شدن دارد.برعکس سایر پروتکل‎های وی‎پی‎ان که از روش‎های مخفی کردن استفاده می‎کنند، امضای ثابت WireGuard به راحتی می‎تواند توسط سیستم‎های تحلیل عمیق DPI یا سایر روش‎های بررسی شبکه شناسایی و بلاک شود. -* Доступность AmneziaVPN для всех платформ -* Низкое энергопотребление -* Минимальное количество настроек -* Легко распознается системами DPI-анализа, подвержен блокировке -* Работает по сетевому протоколу UDP. +* بر روی تمام پلتفرم‌ها در AmneziaVPN قابل دسترسی است. +* مصرف انرژی پایین +* کمترین میزان تنظیمات +* امکان شناسایی شدن توسط سیستم‎های تحلیل عمیق DPI به آسانی و بلاک شدن +* کار بر روی پروتکل شبکه UDP @@ -2811,15 +2840,15 @@ This means that AmneziaWG keeps the fast performance of the original while addin * Minimum number of settings * Not recognised by DPI analysis systems, resistant to blocking * Works over UDP network protocol. - AmneziaWG - усовершенствованная версия популярного VPN-протокола Wireguard. AmneziaWG опирается на фундамент, заложенный WireGuard, сохраняя упрощенную архитектуру и высокопроизводительные возможности работы на разных устройствах. -Хотя WireGuard известен своей эффективностью, у него были проблемы с обнаружением из-за характерных сигнатур пакетов. AmneziaWG решает эту проблему за счет использования более совершенных методов обфускации, благодаря чему его трафик сливается с обычным интернет-трафиком. -Таким образом, AmneziaWG сохраняет высокую производительность оригинала, добавляя при этом дополнительный уровень скрытности, что делает его отличным выбором для тех, кому нужно быстрое и незаметное VPN-соединение. + یک نسخه مدرن از پروتکل وی‎پی‎ان محبوب، AmneziaWG بر روی پایه‎های WireGuard ساخته شده و معماری ساده و عملکرد بالای آن را بر روی تمام دستگاه‎ها حفظ کرده است. +در حالی‎که WireGuard به دلیل بازدهی آن شناخته می‎شود اما امکان شناسایی شدن بالا به دلیل امضای ثابت بسته داده‎های آن یکی از مشکلات آن است. AmneziaWG این مشکل را با استفاده از متدهای مخفی سازی حل کرده و در نتیجه ترافیک آن همانند با ترافیک عادی اینترنت است. +این بدین معنی است که AmneziaWG عملکرد سریع اصلی را حفظ کرده و یک لایه پنهان سازی به آن اضافه کرده که باعث می‎شود که به انتخابی عالی برای آنها که وی‎پی‎ان امن و سریع می‎خواهند تبدیل شود. -* Доступность AmneziaVPN на всех платформах -* Низкое энергопотребление -* Минимальное количество настроек -* Не распознается системами DPI-анализа, устойчив к блокировке -* Работает по сетевому протоколу UDP. +* بر روی تمام پلتفرم‌ها در AmneziaVPN قابل دسترسی است. +* مصرف انرژی پایین +* کمترین میزان تنظیمات +* غیرقابل تشخیص توسط سیستم‎های تحلیل عمیق DPI و مقاوم به بلاک شدن +* کار بر روی پروتکل شبکه UDP AmneziaWG container @@ -2828,73 +2857,81 @@ This means that AmneziaWG keeps the fast performance of the original while addin Sftp file sharing service - is secure FTP service - Сервис обмена файлами Sftp - безопасный FTP-сервис + سرویس اشتراک فایل Sftp یک سرویس امن FTP می‎باشد Sftp service - Сервис SFTP + سرویس Sftp - + Entry not found Entry not found - + Access to keychain denied Access to keychain denied - + No keyring daemon No keyring daemon - + Already unlocked Already unlocked - + No such keyring No such keyring - + Bad arguments Bad arguments - + I/O error I/O error - + Cancelled Cancelled - + Keyring already exists Keyring already exists - + No match No match - + Unknown error Unknown error - + error 0x%1: %2 error 0x%1: %2 + + WireGuard Configuration Highlighter + هایلایتر پیکربندی WireGuard + + + &Randomize colors + رنگ‎های تصادفی + SelectLanguageDrawer @@ -2907,13 +2944,13 @@ This means that AmneziaWG keeps the fast performance of the original while addin Settings - + Server #1 Server #1 - - + + Server Server @@ -2921,22 +2958,22 @@ This means that AmneziaWG keeps the fast performance of the original while addin SettingsController - + Software version نسخه نرم‎افزار - + All settings have been reset to default values تمام تنظیمات به مقادیر پیش فرض ریست شد - + Cached profiles cleared پروفایل ذخیره شده پاک شد - + Backup file is corrupted فایل بک‎آپ خراب شده است @@ -2968,7 +3005,7 @@ This means that AmneziaWG keeps the fast performance of the original while addin Copy config string - + کپی‎کردن متن تنظیمات @@ -3048,7 +3085,7 @@ This means that AmneziaWG keeps the fast performance of the original while addin Visit Website - بازدید وب‎سایت + بازدید وب سایت @@ -3068,7 +3105,7 @@ This means that AmneziaWG keeps the fast performance of the original while addin VpnConnection - + Mbps Mbps diff --git a/client/translations/amneziavpn_ru.ts b/client/translations/amneziavpn_ru.ts index 4f24e5408..ddd7b0e96 100644 --- a/client/translations/amneziavpn_ru.ts +++ b/client/translations/amneziavpn_ru.ts @@ -15,15 +15,13 @@ AndroidController - AmneziaVPN - AmneziaVPN + AmneziaVPN - VPN Connected Refers to the app - which is currently running the background and waiting - VPN Подключен + VPN Подключен @@ -92,7 +90,7 @@ Configure your server - Настроить ваш сервер + Настроить свой сервер @@ -151,7 +149,7 @@ ImportController - + Scanned %1 of %2. Отсканировано %1 из%2. @@ -174,7 +172,7 @@ Added containers that were already installed on the server - + Добавлены сервисы и протоколы, которые были ранее установлены на сервер @@ -196,7 +194,7 @@ Already installed containers were found on the server. All installed containers All containers from server '%1' have been removed - Все протоклы и сервисы были удалены с сервера '%1' + Все протоколы и сервисы были удалены с сервера '%1' @@ -418,7 +416,7 @@ Already installed containers were found on the server. All installed containers OpenVPN settings - Настройки OpenVPN + OpenVPN настройки @@ -946,6 +944,11 @@ Already installed containers were found on the server. All installed containers Show other methods on Github Показать другие способы на Github + + + https://github.com/amnezia-vpn/amnezia-client#donate + + Contacts @@ -1012,17 +1015,17 @@ Already installed containers were found on the server. All installed containers Allow application screenshots - + Разрешить скриншоты приложения Auto start - + Автозапуск Launch the application every time the device is starts - + Открывать приложение при запуске устройства @@ -1189,7 +1192,7 @@ Already installed containers were found on the server. All installed containers If AmneziaDNS is not used or installed - Эти серверы будут использоваться, если не включен AmneziaDNS + Эти адреса будут использоваться, если не включен AmneziaDNS @@ -1353,7 +1356,7 @@ Already installed containers were found on the server. All installed containers Clear cached profiles? - Удалить кэш Amnezia с сервера? + Удалить кэш Amnezia? @@ -1428,22 +1431,22 @@ Already installed containers were found on the server. All installed containers Имя сервера - + Save Сохранить - + Protocols Протоколы - + Services Сервисы - + Data Данные @@ -1680,7 +1683,7 @@ and will not be shared or disclosed to the Amnezia or any third parties All data you enter will remain strictly confidential and will not be shared or disclosed to the Amnezia or any third parties - + Все введенные вами данные останутся строго конфиденциальными и не будут переданы или раскрыты Amnezia или третьим лицам @@ -1703,7 +1706,7 @@ and will not be shared or disclosed to the Amnezia or any third parties What is the level of internet control in your region? - Какой уровень контроля интеренета в вашем регионе? + Какой уровень контроля интернета в вашем регионе? @@ -1854,6 +1857,11 @@ and will not be shared or disclosed to the Amnezia or any third parties I have nothing У меня ничего нет + + + https://amnezia.org/instructions/0_starter-guide + + PageSetupWizardTextKey @@ -1941,8 +1949,8 @@ and will not be shared or disclosed to the Amnezia or any third parties Доступ к управлению сервером. Пользователь, с которым вы делитесь полным доступом к соединению, сможет добавлять и удалять ваши протоколы и службы на сервере, а также изменять настройки. - + Server Сервер @@ -1978,12 +1986,12 @@ and will not be shared or disclosed to the Amnezia or any third parties Save ShadowSocks config - + Сохранить конфигурацию ShadowSocks Save Cloak config - + Сохранить конфигурацию Cloak @@ -1993,12 +2001,12 @@ and will not be shared or disclosed to the Amnezia or any third parties ShadowSocks native format - + ShadowSocks нативный формат Cloak native format - + Cloak нативный формат @@ -2008,66 +2016,71 @@ and will not be shared or disclosed to the Amnezia or any third parties Share full access to the server and VPN - + Поделиться полным доступом к серверу Use for your own devices, or share with those you trust to manage the server. - + Используйте для собственных устройств или передайте управление сервером тем, кому вы доверяете. - + Users - + Пользователи User name - + Имя пользователя - + Search - - Rename + + Creation date: - + + Rename + Переименовать + + + Client name - + Save Сохранить - + Revoke - + Отозвать - - Revoke the config for a user - - + + Revoke the config for a user - %1? + Отозвать доступ для пользователя - %1? - + The user will no longer be able to connect to your server. - + Пользователь больше не сможет подключаться к вашему серверу - + Continue Продолжить - + Cancel Отменить @@ -2081,20 +2094,20 @@ and will not be shared or disclosed to the Amnezia or any third parties Поделиться доступом к VPN, без возможности управления сервером - + Protocol Протокол - + Connection format Формат подключения - + Share Поделиться @@ -2104,32 +2117,32 @@ and will not be shared or disclosed to the Amnezia or any third parties Full access to the server and VPN - + Полный доступ к серверу и VPN We recommend that you use full access to the server only for your own additional devices. - + Мы рекомендуем использовать полный доступ к серверу только для собственных устройств. If you share full access with other people, they can remove and add protocols and services to the server, which will cause the VPN to work incorrectly for all users. - + Если вы поделитесь полным доступом с другими людьми, то они смогут удалять и добавлять протоколы и сервисы на сервер, что приведет к некорректной работе VPN для всех пользователей. Server - + Сервер - + Accessing Доступ - + File with accessing settings to @@ -2160,38 +2173,38 @@ and will not be shared or disclosed to the Amnezia or any third parties QKeychain::DeletePasswordJobPrivate - + Password entry not found Password entry not found - + Could not decrypt data Could not decrypt data - - + + Unknown error Unknown error - + Could not open wallet: %1; %2 Could not open wallet: %1; %2 - + Password not found Password not found - + Could not open keystore Could not open keystore - + Could not remove private key from keystore Could not remove private key from keystore @@ -2199,12 +2212,12 @@ and will not be shared or disclosed to the Amnezia or any third parties QKeychain::JobPrivate - + Unknown error Unknown error - + Access to keychain denied Access to keychain denied @@ -2212,27 +2225,27 @@ and will not be shared or disclosed to the Amnezia or any third parties QKeychain::PlainTextStore - + Could not store data in settings: access error Could not store data in settings: access error - + Could not store data in settings: format error Could not store data in settings: format error - + Could not delete data from settings: access error Could not delete data from settings: access error - + Could not delete data from settings: format error Could not delete data from settings: format error - + Entry not found Entry not found @@ -2240,80 +2253,80 @@ and will not be shared or disclosed to the Amnezia or any third parties QKeychain::ReadPasswordJobPrivate - + Password entry not found Password entry not found - - + + Could not decrypt data Could not decrypt data - + D-Bus is not running D-Bus is not running - - + + Unknown error Unknown error - + No keychain service available No keychain service available - + Could not open wallet: %1; %2 Could not open wallet: %1; %2 - + Access to keychain denied Access to keychain denied - + Could not determine data type: %1; %2 Could not determine data type: %1; %2 - - + + Entry not found Entry not found - + Unsupported entry type 'Map' Unsupported entry type 'Map' - + Unknown kwallet entry type '%1' Unknown kwallet entry type '%1' - + Password not found Password not found - + Could not open keystore Could not open keystore - + Could not retrieve private key from keystore Could not retrieve private key from keystore - + Could not create decryption cipher Could not create decryption cipher @@ -2321,73 +2334,73 @@ and will not be shared or disclosed to the Amnezia or any third parties QKeychain::WritePasswordJobPrivate - + Credential size exceeds maximum size of %1 Credential size exceeds maximum size of %1 - + Credential key exceeds maximum size of %1 Credential key exceeds maximum size of %1 - + Writing credentials failed: Win32 error code %1 Writing credentials failed: Win32 error code %1 - + Encryption failed Encryption failed - + D-Bus is not running D-Bus is not running - - + + Unknown error Unknown error - + Could not open wallet: %1; %2 Could not open wallet: %1; %2 - + Password not found Password not found - + Could not open keystore Could not open keystore - + Could not create private key generator Could not create private key generator - + Could not generate new private key Could not generate new private key - + Could not retrieve private key from keystore Could not retrieve private key from keystore - + Could not create encryption cipher Could not create encryption cipher - + Could not encrypt data Could not encrypt data @@ -2537,7 +2550,7 @@ and will not be shared or disclosed to the Amnezia or any third parties The config does not contain any containers and credentials for connecting to the server - + Конфиг не содержит контейнеров и учетных данных для подключения к серверу Failed to save config to disk @@ -2594,7 +2607,12 @@ and will not be shared or disclosed to the Amnezia or any third parties VPN pool error: no available addresses - + + VPN connection error + + + + Internal error Internal error @@ -2814,7 +2832,7 @@ This means that AmneziaWG keeps the fast performance of the original while addin Sftp file sharing service - is secure FTP service - Сервис обмена файлами Sftp - безопасный FTP-сервис + Файловое хранилище для безопасного хранения данных @@ -2822,62 +2840,62 @@ This means that AmneziaWG keeps the fast performance of the original while addin Сервис SFTP - + Entry not found Entry not found - + Access to keychain denied Access to keychain denied - + No keyring daemon No keyring daemon - + Already unlocked Already unlocked - + No such keyring No such keyring - + Bad arguments Bad arguments - + I/O error I/O error - + Cancelled Cancelled - + Keyring already exists Keyring already exists - + No match No match - + Unknown error Unknown error - + error 0x%1: %2 error 0x%1: %2 @@ -2893,13 +2911,13 @@ This means that AmneziaWG keeps the fast performance of the original while addin Settings - + Server #1 Server #1 - - + + Server Server @@ -2907,22 +2925,22 @@ This means that AmneziaWG keeps the fast performance of the original while addin SettingsController - + Software version Версия ПО - + All settings have been reset to default values Все настройки были сброшены к значению "По умолчанию" - + Cached profiles cleared Кэш профиля очищен - + Backup file is corrupted Backup файл поврежден @@ -2959,7 +2977,7 @@ This means that AmneziaWG keeps the fast performance of the original while addin Show connection settings - + Показать настройки подключения @@ -3054,7 +3072,7 @@ This means that AmneziaWG keeps the fast performance of the original while addin VpnConnection - + Mbps Mbps diff --git a/client/translations/amneziavpn_zh_CN.ts b/client/translations/amneziavpn_zh_CN.ts index e63344d83..7143c76b6 100644 --- a/client/translations/amneziavpn_zh_CN.ts +++ b/client/translations/amneziavpn_zh_CN.ts @@ -11,15 +11,9 @@ AndroidController - - AmneziaVPN - - - - VPN Connected Refers to the app - which is currently running the background and waiting - VPN已连接 + VPN已连接 @@ -158,7 +152,7 @@ ImportController - + Scanned %1 of %2. 扫描 %1 of %2. @@ -997,6 +991,11 @@ And if you don't like the app, all the more support it - the donation will Show other methods on Github 其他捐款途径 + + + https://github.com/amnezia-vpn/amnezia-client#donate + + Contacts @@ -1511,22 +1510,22 @@ And if you don't like the app, all the more support it - the donation will 服务器名 - + Save 保存 - + Protocols 协议 - + Services 服务 - + Data 数据 @@ -1957,6 +1956,11 @@ and will not be shared or disclosed to the Amnezia or any third parties I have nothing 我没有 + + + https://amnezia.org/instructions/0_starter-guide + + PageSetupWizardTextKey @@ -2078,7 +2082,7 @@ and will not be shared or disclosed to the Amnezia or any third parties - + Users @@ -2088,47 +2092,52 @@ and will not be shared or disclosed to the Amnezia or any third parties 共享 VPN 访问,无需管理服务器 - + Search - + + Creation date: + + + + Rename - + Client name - + Save 保存 - + Revoke - - Revoke the config for a user - + + Revoke the config for a user - %1? - + The user will no longer be able to connect to your server. - + Continue 继续 - + Cancel 取消 @@ -2170,8 +2179,8 @@ and will not be shared or disclosed to the Amnezia or any third parties 服务器 - + Server 服务器 @@ -2193,8 +2202,8 @@ and will not be shared or disclosed to the Amnezia or any third parties 协议 - + Protocol 协议 @@ -2214,14 +2223,14 @@ and will not be shared or disclosed to the Amnezia or any third parties - + Connection format 连接格式 - + Share 共享 @@ -2251,12 +2260,12 @@ and will not be shared or disclosed to the Amnezia or any third parties 服务器 - + Accessing 访问 - + File with accessing settings to 访问配置文件的内容为: @@ -2287,38 +2296,38 @@ and will not be shared or disclosed to the Amnezia or any third parties QKeychain::DeletePasswordJobPrivate - + Password entry not found 未发现秘密 - + Could not decrypt data 数据无法加密 - - + + Unknown error 未知错误 - + Could not open wallet: %1; %2 无法打开钱包: %1; %2 - + Password not found 未发现密码 - + Could not open keystore 无法打开密钥库 - + Could not remove private key from keystore 无法从密钥库中删除私钥 @@ -2326,12 +2335,12 @@ and will not be shared or disclosed to the Amnezia or any third parties QKeychain::JobPrivate - + Unknown error 未知错误 - + Access to keychain denied 访问钥匙串被拒绝 @@ -2339,27 +2348,27 @@ and will not be shared or disclosed to the Amnezia or any third parties QKeychain::PlainTextStore - + Could not store data in settings: access error 无法在配置中存储数据:访问错误 - + Could not store data in settings: format error 无法在陪置中存储数据:格式错误 - + Could not delete data from settings: access error 无法在配置中删除数据:访问错误 - + Could not delete data from settings: format error 无法在配置中删除数据:格式错误 - + Entry not found 未找到条目 @@ -2367,80 +2376,80 @@ and will not be shared or disclosed to the Amnezia or any third parties QKeychain::ReadPasswordJobPrivate - + Password entry not found 未发现密码 - - + + Could not decrypt data 数据无法加密 - + D-Bus is not running D-Bus未运行 - - + + Unknown error 未知错误 - + No keychain service available 没有有效的钥匙串服务 - + Could not open wallet: %1; %2 无法打开钱包: %1; %2 - + Access to keychain denied 访问钥匙串被拒绝 - + Could not determine data type: %1; %2 无法确定数据类型: %1; %2 - - + + Entry not found 未找到记录 - + Unsupported entry type 'Map' 不支持的记录类型 'Map' - + Unknown kwallet entry type '%1' 未知钱包类型 '%1' - + Password not found 未发现密码 - + Could not open keystore 无法打开密钥库 - + Could not retrieve private key from keystore 无法从密钥存储库中检索私钥 - + Could not create decryption cipher 无法创建解密算法 @@ -2448,73 +2457,73 @@ and will not be shared or disclosed to the Amnezia or any third parties QKeychain::WritePasswordJobPrivate - + Credential size exceeds maximum size of %1 证书大小超过上限,最大为: %1 - + Credential key exceeds maximum size of %1 凭证密钥大小超过上限,最大为: %1 - + Writing credentials failed: Win32 error code %1 写入凭证失败,Win32错误码: %1 - + Encryption failed 加密失败 - + D-Bus is not running D-Bus未运行 - - + + Unknown error 未知错误 - + Could not open wallet: %1; %2 无法打开钱包: %1; %2 - + Password not found 未发现密码 - + Could not open keystore 无法打开密钥库 - + Could not create private key generator 无法创建私钥生成器 - + Could not generate new private key 无法生成新的私钥 - + Could not retrieve private key from keystore 无法从密钥库检索私钥 - + Could not create encryption cipher 无法创建加密密码 - + Could not encrypt data 无法加密数据 @@ -2666,6 +2675,11 @@ and will not be shared or disclosed to the Amnezia or any third parties Sftp error: No media was in remote drive Sftp 错误: 远程驱动器中没有媒介 + + + VPN connection error + + Failed to save config to disk 配置保存到磁盘失败 @@ -2730,7 +2744,7 @@ and will not be shared or disclosed to the Amnezia or any third parties 该配置不包含任何用于连接到服务器的容器和凭据。 - + Internal error 内部错误 @@ -2970,62 +2984,62 @@ While it offers a blend of security, stability, and speed, it's essential t Sftp 文件共享服务 - 安全的 FTP 服务 - + Entry not found 未找到记录 - + Access to keychain denied 访问钥匙串被拒绝 - + No keyring daemon 没有密钥环守护进程 - + Already unlocked 已经解锁 - + No such keyring 没有这样的密钥环 - + Bad arguments 错误参数 - + I/O error I/O错误 - + Cancelled 已取消 - + Keyring already exists 密匙环已经存在 - + No match 不匹配 - + Unknown error 未知错误 - + error 0x%1: %2 错误 0x%1: %2 @@ -3041,13 +3055,13 @@ While it offers a blend of security, stability, and speed, it's essential t Settings - + Server #1 - - + + Server 服务器 @@ -3055,22 +3069,22 @@ While it offers a blend of security, stability, and speed, it's essential t SettingsController - + Software version 软件版本 - + Backup file is corrupted 备份文件已损坏 - + All settings have been reset to default values 所配置恢复为默认值 - + Cached profiles cleared 缓存的配置文件已清除 @@ -3206,7 +3220,7 @@ While it offers a blend of security, stability, and speed, it's essential t VpnConnection - + Mbps diff --git a/client/ui/controllers/exportController.cpp b/client/ui/controllers/exportController.cpp index 9930926f1..4f3fe7d57 100644 --- a/client/ui/controllers/exportController.cpp +++ b/client/ui/controllers/exportController.cpp @@ -15,7 +15,7 @@ #include "core/errorstrings.h" #include "systemController.h" #ifdef Q_OS_ANDROID - #include "platforms/android/androidutils.h" + #include "platforms/android/android_utils.h" #endif #include "qrcodegen.hpp" @@ -48,6 +48,22 @@ void ExportController::generateFullAccessConfig() int serverIndex = m_serversModel->getCurrentlyProcessedServerIndex(); QJsonObject config = m_settings->server(serverIndex); + QJsonArray containers = config.value(config_key::containers).toArray(); + for (auto i = 0; i < containers.size(); i++) { + auto containerConfig = containers.at(i).toObject(); + auto containerType = ContainerProps::containerFromString(containerConfig.value(config_key::container).toString()); + + for (auto protocol : ContainerProps::protocolsForContainer(containerType)) { + auto protocolConfig = containerConfig.value(ProtocolProps::protoToString(protocol)).toObject(); + + protocolConfig.remove(config_key::last_config); + containerConfig[ProtocolProps::protoToString(protocol)] = protocolConfig; + } + + containers.replace(i, containerConfig); + } + config[config_key::containers] = containers; + QByteArray compressedConfig = QJsonDocument(config).toJson(); compressedConfig = qCompress(compressedConfig, 8); m_config = QString("vpn://%1") diff --git a/client/ui/controllers/importController.cpp b/client/ui/controllers/importController.cpp index 44fc1cb96..59aab046c 100644 --- a/client/ui/controllers/importController.cpp +++ b/client/ui/controllers/importController.cpp @@ -239,7 +239,12 @@ QJsonObject ImportController::extractWireGuardConfig(const QString &data) // && !configMap.value("PresharedKey").isEmpty() && !configMap.value("PublicKey").isEmpty()) { lastConfig[config_key::client_priv_key] = configMap.value("PrivateKey"); lastConfig[config_key::client_ip] = configMap.value("Address"); - lastConfig[config_key::psk_key] = configMap.value("PresharedKey"); + if (!configMap.value("PresharedKey").isEmpty()) { + lastConfig[config_key::psk_key] = configMap.value("PresharedKey"); + } else if (!configMap.value("PreSharedKey").isEmpty()) { + lastConfig[config_key::psk_key] = configMap.value("PreSharedKey"); + } + lastConfig[config_key::server_pub_key] = configMap.value("PublicKey"); // } else { // qDebug() << "Failed to import profile"; diff --git a/client/ui/controllers/pageController.cpp b/client/ui/controllers/pageController.cpp index ed60500a6..105f2115a 100644 --- a/client/ui/controllers/pageController.cpp +++ b/client/ui/controllers/pageController.cpp @@ -7,7 +7,7 @@ #endif #ifdef Q_OS_ANDROID - #include "../../platforms/android/androidutils.h" + #include "platforms/android/android_utils.h" #include #endif #if defined Q_OS_MAC diff --git a/client/ui/controllers/settingsController.cpp b/client/ui/controllers/settingsController.cpp index 9fa4d76ba..c7306a108 100644 --- a/client/ui/controllers/settingsController.cpp +++ b/client/ui/controllers/settingsController.cpp @@ -7,19 +7,21 @@ #include "ui/qautostart.h" #include "version.h" #ifdef Q_OS_ANDROID - #include "../../platforms/android/android_controller.h" - #include "../../platforms/android/androidutils.h" + #include "platforms/android/android_utils.h" + #include "platforms/android/android_controller.h" #include #endif SettingsController::SettingsController(const QSharedPointer &serversModel, const QSharedPointer &containersModel, const QSharedPointer &languageModel, + const QSharedPointer &sitesModel, const std::shared_ptr &settings, QObject *parent) : QObject(parent), m_serversModel(serversModel), m_containersModel(containersModel), m_languageModel(languageModel), + m_sitesModel(sitesModel), m_settings(settings) { m_appVersion = QString("%1: %2 (%3)").arg(tr("Software version"), QString(APP_VERSION), __DATE__); @@ -90,13 +92,21 @@ void SettingsController::openLogsFolder() void SettingsController::exportLogsFile(const QString &fileName) { +#ifdef Q_OS_ANDROID + AndroidController::instance()->exportLogsFile(fileName); +#else SystemController::saveFile(fileName, Logger::getLogFile()); +#endif } void SettingsController::clearLogs() { +#ifdef Q_OS_ANDROID + AndroidController::instance()->clearLogs(); +#else Logger::clearLogs(); Logger::clearServiceLogs(); +#endif } void SettingsController::backupAppConfig(const QString &fileName) @@ -134,6 +144,7 @@ void SettingsController::clearSettings() m_serversModel->resetModel(); m_languageModel->changeLanguage( static_cast(m_languageModel->getCurrentLanguageIndex())); + m_sitesModel->setRouteMode(Settings::RouteMode::VpnAllSites); emit changeSettingsFinished(tr("All settings have been reset to default values")); } @@ -195,3 +206,14 @@ void SettingsController::toggleScreenshotsEnabled(bool enable) }); #endif } + +bool SettingsController::isCameraPresent() +{ +#if defined Q_OS_IOS + return true; +#elif defined Q_OS_ANDROID + return AndroidController::instance()->isCameraPresent(); +#else + return false; +#endif +} diff --git a/client/ui/controllers/settingsController.h b/client/ui/controllers/settingsController.h index 710d255f1..5b30b0951 100644 --- a/client/ui/controllers/settingsController.h +++ b/client/ui/controllers/settingsController.h @@ -6,6 +6,7 @@ #include "ui/models/containers_model.h" #include "ui/models/languageModel.h" #include "ui/models/servers_model.h" +#include "ui/models/sites_model.h" class SettingsController : public QObject { @@ -14,6 +15,7 @@ public: explicit SettingsController(const QSharedPointer &serversModel, const QSharedPointer &containersModel, const QSharedPointer &languageModel, + const QSharedPointer &sitesModel, const std::shared_ptr &settings, QObject *parent = nullptr); Q_PROPERTY(QString primaryDns READ getPrimaryDns WRITE setPrimaryDns NOTIFY primaryDnsChanged) @@ -57,6 +59,8 @@ public slots: bool isScreenshotsEnabled(); void toggleScreenshotsEnabled(bool enable); + bool isCameraPresent(); + signals: void primaryDnsChanged(); void secondaryDnsChanged(); @@ -76,6 +80,7 @@ private: QSharedPointer m_serversModel; QSharedPointer m_containersModel; QSharedPointer m_languageModel; + QSharedPointer m_sitesModel; std::shared_ptr m_settings; QString m_appVersion; diff --git a/client/ui/controllers/systemController.cpp b/client/ui/controllers/systemController.cpp index 96fc27925..ecd68c8f5 100644 --- a/client/ui/controllers/systemController.cpp +++ b/client/ui/controllers/systemController.cpp @@ -60,6 +60,11 @@ QString SystemController::getFileName(const QString &acceptLabel, const QString const QString &selectedFile, const bool isSaveMode, const QString &defaultSuffix) { QString fileName; +#ifdef Q_OS_ANDROID + Q_ASSERT(!isSaveMode); + return AndroidController::instance()->openFile(nameFilter); +#endif + #ifdef Q_OS_IOS MobileUtils mobileUtils; @@ -108,20 +113,6 @@ QString SystemController::getFileName(const QString &acceptLabel, const QString } fileName = mainFileDialog->property("selectedFile").toString(); - -#ifdef Q_OS_ANDROID - // patch for files containing spaces etc - const QString sep { "raw%3A%2F" }; - if (fileName.startsWith("content://") && fileName.contains(sep)) { - QString contentUrl = fileName.split(sep).at(0); - QString rawUrl = fileName.split(sep).at(1); - rawUrl.replace(" ", "%20"); - fileName = contentUrl + sep + rawUrl; - } - - return fileName; -#endif - return QUrl(fileName).toLocalFile(); } diff --git a/client/ui/models/clientManagementModel.cpp b/client/ui/models/clientManagementModel.cpp index 8ec31d02f..0b1be2cc4 100644 --- a/client/ui/models/clientManagementModel.cpp +++ b/client/ui/models/clientManagementModel.cpp @@ -15,6 +15,7 @@ namespace constexpr char clientName[] = "clientName"; constexpr char container[] = "container"; constexpr char userData[] = "userData"; + constexpr char creationDate[] = "creationDate"; } } @@ -40,11 +41,29 @@ QVariant ClientManagementModel::data(const QModelIndex &index, int role) const switch (role) { case ClientNameRole: return userData.value(configKey::clientName).toString(); + case CreationDateRole: return userData.value(configKey::creationDate).toString(); } return QVariant(); } +void ClientManagementModel::migration(const QByteArray &clientsTableString) +{ + QJsonObject clientsTable = QJsonDocument::fromJson(clientsTableString).object(); + + for (auto &clientId : clientsTable.keys()) { + QJsonObject client; + client[configKey::clientId] = clientId; + + QJsonObject userData; + userData[configKey::clientName] = clientsTable.value(clientId).toObject().value(configKey::clientName); + client[configKey::userData] = userData; + + m_clientsTable.push_back(client); + } + +} + ErrorCode ClientManagementModel::updateModel(DockerContainer container, ServerCredentials credentials) { beginResetModel(); @@ -54,8 +73,14 @@ ErrorCode ClientManagementModel::updateModel(DockerContainer container, ServerCr ErrorCode error = ErrorCode::NoError; - const QString clientsTableFile = - QString("/opt/amnezia/%1/clientsTable").arg(ContainerProps::containerTypeToString(container)); + QString clientsTableFile = QString("/opt/amnezia/%1/clientsTable"); + if (container == DockerContainer::OpenVpn || container == DockerContainer::ShadowSocks + || container == DockerContainer::Cloak) { + clientsTableFile = clientsTableFile.arg(ContainerProps::containerTypeToString(DockerContainer::OpenVpn)); + } else { + clientsTableFile = clientsTableFile.arg(ContainerProps::containerTypeToString(container)); + } + const QByteArray clientsTableString = serverController.getTextFileFromContainer(container, credentials, clientsTableFile, &error); if (error != ErrorCode::NoError) { @@ -67,6 +92,8 @@ ErrorCode ClientManagementModel::updateModel(DockerContainer container, ServerCr m_clientsTable = QJsonDocument::fromJson(clientsTableString).array(); if (m_clientsTable.isEmpty()) { + migration(clientsTableString); + int count = 0; if (container == DockerContainer::OpenVpn || container == DockerContainer::ShadowSocks @@ -200,25 +227,31 @@ ErrorCode ClientManagementModel::appendClient(const QString &clientId, const QSt for (int i = 0; i < m_clientsTable.size(); i++) { if (m_clientsTable.at(i).toObject().value(configKey::clientId) == clientId) { - return renameClient(i, clientName, container, credentials); + return renameClient(i, clientName, container, credentials, true); } } - beginResetModel(); + beginInsertRows(QModelIndex(), rowCount(), rowCount() + 1); QJsonObject client; client[configKey::clientId] = clientId; QJsonObject userData; userData[configKey::clientName] = clientName; + userData[configKey::creationDate] = QDateTime::currentDateTime().toString(); client[configKey::userData] = userData; m_clientsTable.push_back(client); - endResetModel(); + endInsertRows(); const QByteArray clientsTableString = QJsonDocument(m_clientsTable).toJson(); ServerController serverController(m_settings); - const QString clientsTableFile = - QString("/opt/amnezia/%1/clientsTable").arg(ContainerProps::containerTypeToString(container)); + QString clientsTableFile = QString("/opt/amnezia/%1/clientsTable"); + if (container == DockerContainer::OpenVpn || container == DockerContainer::ShadowSocks + || container == DockerContainer::Cloak) { + clientsTableFile = clientsTableFile.arg(ContainerProps::containerTypeToString(DockerContainer::OpenVpn)); + } else { + clientsTableFile = clientsTableFile.arg(ContainerProps::containerTypeToString(container)); + } error = serverController.uploadTextFileToContainer(container, credentials, clientsTableString, clientsTableFile); if (error != ErrorCode::NoError) { @@ -229,11 +262,14 @@ ErrorCode ClientManagementModel::appendClient(const QString &clientId, const QSt } ErrorCode ClientManagementModel::renameClient(const int row, const QString &clientName, const DockerContainer container, - ServerCredentials credentials) + ServerCredentials credentials, bool addTimeStamp) { auto client = m_clientsTable.at(row).toObject(); auto userData = client[configKey::userData].toObject(); userData[configKey::clientName] = clientName; + if (addTimeStamp) { + userData[configKey::creationDate] = QDateTime::currentDateTime().toString(); + } client[configKey::userData] = userData; m_clientsTable.replace(row, client); @@ -242,8 +278,13 @@ ErrorCode ClientManagementModel::renameClient(const int row, const QString &clie const QByteArray clientsTableString = QJsonDocument(m_clientsTable).toJson(); ServerController serverController(m_settings); - const QString clientsTableFile = - QString("/opt/amnezia/%1/clientsTable").arg(ContainerProps::containerTypeToString(container)); + QString clientsTableFile = QString("/opt/amnezia/%1/clientsTable"); + if (container == DockerContainer::OpenVpn || container == DockerContainer::ShadowSocks + || container == DockerContainer::Cloak) { + clientsTableFile = clientsTableFile.arg(ContainerProps::containerTypeToString(DockerContainer::OpenVpn)); + } else { + clientsTableFile = clientsTableFile.arg(ContainerProps::containerTypeToString(container)); + } ErrorCode error = serverController.uploadTextFileToContainer(container, credentials, clientsTableString, clientsTableFile); @@ -257,13 +298,33 @@ ErrorCode ClientManagementModel::renameClient(const int row, const QString &clie ErrorCode ClientManagementModel::revokeClient(const int row, const DockerContainer container, ServerCredentials credentials) { + ErrorCode errorCode = ErrorCode::NoError; + if (container == DockerContainer::OpenVpn || container == DockerContainer::ShadowSocks || container == DockerContainer::Cloak) { - return revokeOpenVpn(row, container, credentials); + errorCode = revokeOpenVpn(row, container, credentials); } else if (container == DockerContainer::WireGuard || container == DockerContainer::Awg) { - return revokeWireGuard(row, container, credentials); + errorCode = revokeWireGuard(row, container, credentials); } - return ErrorCode::NoError; + + if (errorCode == ErrorCode::NoError) { + auto client = m_clientsTable.at(row).toObject(); + QString clientId = client.value(configKey::clientId).toString(); + + const auto server = m_settings->defaultServer(); + QJsonArray containers = server.value(config_key::containers).toArray(); + for (auto i = 0; i < containers.size(); i++) { + auto containerConfig = containers.at(i).toObject(); + auto containerType = ContainerProps::containerFromString(containerConfig.value(config_key::container).toString()); + auto protocolConfig = containerConfig.value(ContainerProps::containerTypeToString(containerType)).toObject(); + + if (protocolConfig.value(config_key::last_config).toString().contains(clientId)) { + emit adminConfigRevoked(container); + } + } + } + + return errorCode; } ErrorCode ClientManagementModel::revokeOpenVpn(const int row, const DockerContainer container, @@ -294,8 +355,13 @@ ErrorCode ClientManagementModel::revokeOpenVpn(const int row, const DockerContai const QByteArray clientsTableString = QJsonDocument(m_clientsTable).toJson(); - const QString clientsTableFile = - QString("/opt/amnezia/%1/clientsTable").arg(ContainerProps::containerTypeToString(container)); + QString clientsTableFile = QString("/opt/amnezia/%1/clientsTable"); + if (container == DockerContainer::OpenVpn || container == DockerContainer::ShadowSocks + || container == DockerContainer::Cloak) { + clientsTableFile = clientsTableFile.arg(ContainerProps::containerTypeToString(DockerContainer::OpenVpn)); + } else { + clientsTableFile = clientsTableFile.arg(ContainerProps::containerTypeToString(container)); + } error = serverController.uploadTextFileToContainer(container, credentials, clientsTableString, clientsTableFile); if (error != ErrorCode::NoError) { logger.error() << "Failed to upload the clientsTable file to the server"; @@ -344,8 +410,13 @@ ErrorCode ClientManagementModel::revokeWireGuard(const int row, const DockerCont const QByteArray clientsTableString = QJsonDocument(m_clientsTable).toJson(); - const QString clientsTableFile = - QString("/opt/amnezia/%1/clientsTable").arg(ContainerProps::containerTypeToString(container)); + QString clientsTableFile = QString("/opt/amnezia/%1/clientsTable"); + if (container == DockerContainer::OpenVpn || container == DockerContainer::ShadowSocks + || container == DockerContainer::Cloak) { + clientsTableFile = clientsTableFile.arg(ContainerProps::containerTypeToString(DockerContainer::OpenVpn)); + } else { + clientsTableFile = clientsTableFile.arg(ContainerProps::containerTypeToString(container)); + } error = serverController.uploadTextFileToContainer(container, credentials, clientsTableString, clientsTableFile); if (error != ErrorCode::NoError) { logger.error() << "Failed to upload the clientsTable file to the server"; @@ -369,5 +440,6 @@ QHash ClientManagementModel::roleNames() const { QHash roles; roles[ClientNameRole] = "clientName"; + roles[CreationDateRole] = "creationDate"; return roles; } diff --git a/client/ui/models/clientManagementModel.h b/client/ui/models/clientManagementModel.h index 6b6adf682..ba36c26f7 100644 --- a/client/ui/models/clientManagementModel.h +++ b/client/ui/models/clientManagementModel.h @@ -14,6 +14,7 @@ class ClientManagementModel : public QAbstractListModel public: enum Roles { ClientNameRole = Qt::UserRole + 1, + CreationDateRole }; ClientManagementModel(std::shared_ptr settings, QObject *parent = nullptr); @@ -26,15 +27,20 @@ public slots: ErrorCode appendClient(const QString &clientId, const QString &clientName, const DockerContainer container, ServerCredentials credentials); ErrorCode renameClient(const int row, const QString &userName, const DockerContainer container, - ServerCredentials credentials); + ServerCredentials credentials, bool addTimeStamp = false); ErrorCode revokeClient(const int index, const DockerContainer container, ServerCredentials credentials); protected: QHash roleNames() const override; +signals: + void adminConfigRevoked(const DockerContainer container); + private: bool isClientExists(const QString &clientId); + void migration(const QByteArray &clientsTableString); + ErrorCode revokeOpenVpn(const int row, const DockerContainer container, ServerCredentials credentials); ErrorCode revokeWireGuard(const int row, const DockerContainer container, ServerCredentials credentials); diff --git a/client/ui/models/servers_model.cpp b/client/ui/models/servers_model.cpp index ad927fceb..ad9b00593 100644 --- a/client/ui/models/servers_model.cpp +++ b/client/ui/models/servers_model.cpp @@ -15,6 +15,10 @@ ServersModel::ServersModel(std::shared_ptr settings, QObject *parent) auto defaultContainer = ContainerProps::containerFromString(m_servers.at(serverIndex).toObject().value(config_key::defaultContainer).toString()); emit ServersModel::defaultContainerChanged(defaultContainer); }); + connect(this, &ServersModel::currentlyProcessedServerIndexChanged, this, [this](const int serverIndex) { + auto defaultContainer = ContainerProps::containerFromString(m_servers.at(serverIndex).toObject().value(config_key::defaultContainer).toString()); + emit ServersModel::defaultContainerChanged(defaultContainer); + }); } int ServersModel::rowCount(const QModelIndex &parent) const @@ -247,10 +251,9 @@ void ServersModel::addServer(const QJsonObject &server) void ServersModel::editServer(const QJsonObject &server) { - beginResetModel(); m_settings->editServer(m_currentlyProcessedServerIndex, server); - m_servers = m_settings->serversArray(); - endResetModel(); + m_servers.replace(m_currentlyProcessedServerIndex, m_settings->serversArray().at(m_currentlyProcessedServerIndex)); + emit dataChanged(index(m_currentlyProcessedServerIndex, 0), index(m_currentlyProcessedServerIndex, 0)); updateContainersModel(); } @@ -269,6 +272,7 @@ void ServersModel::removeServer() if (m_settings->serversCount() == 0) { setDefaultServerIndex(-1); } + setCurrentlyProcessedServerIndex(m_defaultServerIndex); endResetModel(); } @@ -479,6 +483,14 @@ void ServersModel::clearCachedProfiles() updateContainersModel(); } +void ServersModel::clearCachedProfile(const DockerContainer container) +{ + m_settings->clearLastConnectionConfig(m_currentlyProcessedServerIndex, container); + + m_servers.replace(m_currentlyProcessedServerIndex, m_settings->server(m_currentlyProcessedServerIndex)); + updateContainersModel(); +} + bool ServersModel::isAmneziaDnsContainerInstalled(const int serverIndex) { QJsonObject server = m_servers.at(serverIndex).toObject(); @@ -518,3 +530,8 @@ void ServersModel::toggleAmneziaDns(bool enabled) emit defaultServerDescriptionChanged(); } +bool ServersModel::isDefaultServerFromApi() +{ + return m_settings->server(m_defaultServerIndex).value(config_key::configVersion).toInt(); +} + diff --git a/client/ui/models/servers_model.h b/client/ui/models/servers_model.h index 901605e29..38f2bdd4c 100644 --- a/client/ui/models/servers_model.h +++ b/client/ui/models/servers_model.h @@ -84,6 +84,7 @@ public slots: void addContainerConfig(const int containerIndex, const QJsonObject config); void clearCachedProfiles(); + void clearCachedProfile(const DockerContainer container); ErrorCode removeContainer(const int containerIndex); ErrorCode removeAllContainers(); @@ -96,6 +97,8 @@ public slots: void toggleAmneziaDns(bool enabled); + bool isDefaultServerFromApi(); + protected: QHash roleNames() const override; diff --git a/client/ui/qautostart.cpp b/client/ui/qautostart.cpp index a7f49b2da..b0165dc4d 100644 --- a/client/ui/qautostart.cpp +++ b/client/ui/qautostart.cpp @@ -124,8 +124,13 @@ void Autostart::setAutostart(bool autostart) { if (file.open(QIODevice::ReadWrite)) { QTextStream stream(&file); stream << "[Desktop Entry]" << Qt::endl; - stream << "Exec=" << appPath() << Qt::endl; + stream << "Exec=AmneziaVPN" << Qt::endl; stream << "Type=Application" << Qt::endl; + stream << "Name=AmneziaVPN" << Qt::endl; + stream << "Comment=Client of your self-hosted VPN" << Qt::endl; + stream << "Icon=/usr/share/pixmaps/AmneziaVPN.png" << Qt::endl; + stream << "Categories=Network;Qt;Security;" << Qt::endl; + stream << "Terminal=false" << Qt::endl; } } } diff --git a/client/ui/qml/Components/HomeContainersListView.qml b/client/ui/qml/Components/HomeContainersListView.qml index 78ea93304..3043e97fa 100644 --- a/client/ui/qml/Components/HomeContainersListView.qml +++ b/client/ui/qml/Components/HomeContainersListView.qml @@ -26,6 +26,24 @@ ListView { id: containersRadioButtonGroup } + Connections { + target: ServersModel + + function onCurrentlyProcessedServerIndexChanged() { + if (ContainersModel.getDefaultContainer()) { + menuContent.checkCurrentItem() + } + } + } + + function checkCurrentItem() { + var item = menuContent.itemAtIndex(currentIndex) + if (item !== null) { + var radioButton = item.children[0].children[0] + radioButton.checked = true + } + } + delegate: Item { implicitWidth: rootWidth implicitHeight: content.implicitHeight @@ -60,9 +78,8 @@ ListView { } if (checked) { - ServersModel.setDefaultContainer(proxyContainersModel.mapToSource(index)) - containersDropDown.menuVisible = false + ServersModel.setDefaultContainer(proxyContainersModel.mapToSource(index)) } else { if (!isSupported && isInstalled) { PageController.showErrorMessage(qsTr("The selected protocol is not supported on the current platform")) diff --git a/client/ui/qml/Components/ShareConnectionDrawer.qml b/client/ui/qml/Components/ShareConnectionDrawer.qml index e354e9517..45ef84a63 100644 --- a/client/ui/qml/Components/ShareConnectionDrawer.qml +++ b/client/ui/qml/Components/ShareConnectionDrawer.qml @@ -25,7 +25,7 @@ DrawerType { property string configExtension: ".vpn" property string configCaption: qsTr("Save AmneziaVPN config") - property string configFileName: "amnezia_config.vpn" + property string configFileName: "amnezia_config" width: parent.width height: parent.height * 0.9 diff --git a/client/ui/qml/Pages2/PageHome.qml b/client/ui/qml/Pages2/PageHome.qml index 8374dbc3b..5b44bc7cc 100644 --- a/client/ui/qml/Pages2/PageHome.qml +++ b/client/ui/qml/Pages2/PageHome.qml @@ -31,6 +31,7 @@ PageType { containersDropDown.rootButtonClickedFunction() } } + function onForceCloseDrawer() { buttonContent.state = "collapsed" } diff --git a/client/ui/qml/Pages2/PageSettingsAbout.qml b/client/ui/qml/Pages2/PageSettingsAbout.qml index eaa9eb3dc..b387cc64b 100644 --- a/client/ui/qml/Pages2/PageSettingsAbout.qml +++ b/client/ui/qml/Pages2/PageSettingsAbout.qml @@ -101,7 +101,7 @@ PageType { text: qsTr("Show other methods on Github") - onClicked: Qt.openUrlExternally("https://github.com/amnezia-vpn/amnezia-client#donate") + onClicked: Qt.openUrlExternally(qsTr("https://github.com/amnezia-vpn/amnezia-client#donate")) } ParagraphTextType { diff --git a/client/ui/qml/Pages2/PageSettingsDns.qml b/client/ui/qml/Pages2/PageSettingsDns.qml index 5670464f4..1970da522 100644 --- a/client/ui/qml/Pages2/PageSettingsDns.qml +++ b/client/ui/qml/Pages2/PageSettingsDns.qml @@ -28,6 +28,14 @@ PageType { anchors.bottom: parent.bottom contentHeight: content.height + enabled: !ServersModel.isDefaultServerFromApi() + + Component.onCompleted: { + if (ServersModel.isDefaultServerFromApi()) { + PageController.showNotificationMessage(qsTr("Default server does not support custom dns")) + } + } + ColumnLayout { id: content diff --git a/client/ui/qml/Pages2/PageSettingsLogging.qml b/client/ui/qml/Pages2/PageSettingsLogging.qml index 840c41d45..b302bffcb 100644 --- a/client/ui/qml/Pages2/PageSettingsLogging.qml +++ b/client/ui/qml/Pages2/PageSettingsLogging.qml @@ -66,7 +66,8 @@ PageType { ColumnLayout { Layout.alignment: Qt.AlignBaseline - Layout.preferredWidth: root.width / 3 + Layout.preferredWidth: GC.isMobile() ? 0 : root.width / 3 + visible: !GC.isMobile() ImageButtonType { Layout.alignment: Qt.AlignHCenter @@ -90,7 +91,7 @@ PageType { ColumnLayout { Layout.alignment: Qt.AlignBaseline - Layout.preferredWidth: root.width / 3 + Layout.preferredWidth: root.width / ( GC.isMobile() ? 2 : 3 ) ImageButtonType { Layout.alignment: Qt.AlignHCenter @@ -131,7 +132,7 @@ PageType { ColumnLayout { Layout.alignment: Qt.AlignBaseline - Layout.preferredWidth: root.width / 3 + Layout.preferredWidth: root.width / ( GC.isMobile() ? 2 : 3 ) ImageButtonType { Layout.alignment: Qt.AlignHCenter diff --git a/client/ui/qml/Pages2/PageSettingsServerProtocols.qml b/client/ui/qml/Pages2/PageSettingsServerProtocols.qml index 21401bf5b..9fc2abde2 100644 --- a/client/ui/qml/Pages2/PageSettingsServerProtocols.qml +++ b/client/ui/qml/Pages2/PageSettingsServerProtocols.qml @@ -55,6 +55,9 @@ PageType { model: SortFilterProxyModel { id: proxyContainersModel sourceModel: ContainersModel + sorters: [ + RoleSorter { roleName: "isInstalled"; sortOrder: Qt.DescendingOrder } + ] } Component.onCompleted: updateContainersModelFilters() diff --git a/client/ui/qml/Pages2/PageSettingsSplitTunneling.qml b/client/ui/qml/Pages2/PageSettingsSplitTunneling.qml index 873ae9977..4300d5912 100644 --- a/client/ui/qml/Pages2/PageSettingsSplitTunneling.qml +++ b/client/ui/qml/Pages2/PageSettingsSplitTunneling.qml @@ -21,7 +21,13 @@ PageType { id: root property bool pageEnabled: { - return !ConnectionController.isConnected + return !ConnectionController.isConnected && !ServersModel.isDefaultServerFromApi() + } + + Component.onCompleted: { + if (ServersModel.isDefaultServerFromApi()) { + PageController.showNotificationMessage(qsTr("Default server does not support split tunneling function")) + } } Connections { diff --git a/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml b/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml index 07189eb7b..519aa6cc7 100644 --- a/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml +++ b/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml @@ -90,7 +90,7 @@ It's okay as long as it's from someone you trust.") LabelWithButtonType { Layout.fillWidth: true - visible: GC.isMobile() + visible: SettingsController.isCameraPresent() text: qsTr("QR-code") rightImageSource: "qrc:/images/controls/chevron-right.svg" @@ -105,7 +105,7 @@ It's okay as long as it's from someone you trust.") } DividerType { - visible: GC.isMobile() + visible: SettingsController.isCameraPresent() } LabelWithButtonType { diff --git a/client/ui/qml/Pages2/PageSetupWizardEasy.qml b/client/ui/qml/Pages2/PageSetupWizardEasy.qml index c393d0ce5..95951507d 100644 --- a/client/ui/qml/Pages2/PageSetupWizardEasy.qml +++ b/client/ui/qml/Pages2/PageSetupWizardEasy.qml @@ -112,17 +112,19 @@ PageType { var defaultContainerProto = ContainerProps.defaultProtocol(dockerContainer) containers.dockerContainer = dockerContainer - containers.containerDefaultPort = ProtocolProps.defaultPort(defaultContainerProto) + containers.containerDefaultPort = ProtocolProps.getPortForInstall(defaultContainerProto) containers.containerDefaultTransportProto = ProtocolProps.defaultTransportProto(defaultContainerProto) } } } + } - Component.onCompleted: { - if (index === containers.currentIndex) { - card.checked = true - card.clicked() - } + Component.onCompleted: { + var item = containers.itemAtIndex(containers.currentIndex) + if (item !== null) { + var button = item.children[0].children[0] + button.checked = true + button.clicked() } } } diff --git a/client/ui/qml/Pages2/PageSetupWizardInstalling.qml b/client/ui/qml/Pages2/PageSetupWizardInstalling.qml index 9811d87d5..1a3e7c07f 100644 --- a/client/ui/qml/Pages2/PageSetupWizardInstalling.qml +++ b/client/ui/qml/Pages2/PageSetupWizardInstalling.qml @@ -42,6 +42,7 @@ PageType { function onInstallServerFinished(finishedMessage) { if (!ConnectionController.isConnected) { ServersModel.setDefaultServerIndex(ServersModel.getServersCount() - 1); + ServersModel.currentlyProcessedIndex = ServersModel.defaultIndex } PageController.goToStartPage() diff --git a/client/ui/qml/Pages2/PageSetupWizardStart.qml b/client/ui/qml/Pages2/PageSetupWizardStart.qml index 2f89bc575..a820ac715 100644 --- a/client/ui/qml/Pages2/PageSetupWizardStart.qml +++ b/client/ui/qml/Pages2/PageSetupWizardStart.qml @@ -135,7 +135,7 @@ PageType { text: qsTr("I have nothing") - onClicked: Qt.openUrlExternally("https://amnezia.org/instructions/0_starter-guide") + onClicked: Qt.openUrlExternally(qsTr("https://amnezia.org/instructions/0_starter-guide")) } } diff --git a/client/ui/qml/Pages2/PageSetupWizardViewConfig.qml b/client/ui/qml/Pages2/PageSetupWizardViewConfig.qml index 65a6f319b..6df26fc06 100644 --- a/client/ui/qml/Pages2/PageSetupWizardViewConfig.qml +++ b/client/ui/qml/Pages2/PageSetupWizardViewConfig.qml @@ -26,6 +26,7 @@ PageType { function onImportFinished() { if (!ConnectionController.isConnected) { ServersModel.setDefaultServerIndex(ServersModel.getServersCount() - 1); + ServersModel.currentlyProcessedIndex = ServersModel.defaultIndex } PageController.goToStartPage() diff --git a/client/ui/qml/Pages2/PageShare.qml b/client/ui/qml/Pages2/PageShare.qml index 803543306..3a769ebe5 100644 --- a/client/ui/qml/Pages2/PageShare.qml +++ b/client/ui/qml/Pages2/PageShare.qml @@ -581,7 +581,7 @@ PageType { Layout.bottomMargin: 24 headerText: clientName - descriptionText: serverSelector.text + descriptionText: qsTr("Creation date: ") + creationDate } BasicButtonType { diff --git a/client/vpnconnection.cpp b/client/vpnconnection.cpp index 6b3dae9b0..d3588fe46 100644 --- a/client/vpnconnection.cpp +++ b/client/vpnconnection.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -63,24 +64,26 @@ void VpnConnection::onConnectionStateChanged(Vpn::ConnectionState state) IpcClient::Interface()->resetIpStack(); IpcClient::Interface()->flushDns(); - if (m_settings->routeMode() != Settings::VpnAllSites) { - IpcClient::Interface()->routeDeleteList(m_vpnProtocol->vpnGateway(), QStringList() << "0.0.0.0"); - // qDebug() << "VpnConnection::onConnectionStateChanged :: adding custom routes, count:" << forwardIps.size(); - } - QString dns1 = m_vpnConfiguration.value(config_key::dns1).toString(); - QString dns2 = m_vpnConfiguration.value(config_key::dns2).toString(); + if (!m_vpnConfiguration.value(config_key::configVersion).toInt()) { + if (m_settings->routeMode() != Settings::VpnAllSites) { + IpcClient::Interface()->routeDeleteList(m_vpnProtocol->vpnGateway(), QStringList() << "0.0.0.0"); + // qDebug() << "VpnConnection::onConnectionStateChanged :: adding custom routes, count:" << forwardIps.size(); + } + QString dns1 = m_vpnConfiguration.value(config_key::dns1).toString(); + QString dns2 = m_vpnConfiguration.value(config_key::dns2).toString(); - IpcClient::Interface()->routeAddList(m_vpnProtocol->vpnGateway(), QStringList() << dns1 << dns2); + IpcClient::Interface()->routeAddList(m_vpnProtocol->vpnGateway(), QStringList() << dns1 << dns2); - if (m_settings->routeMode() == Settings::VpnOnlyForwardSites) { - QTimer::singleShot(1000, m_vpnProtocol.data(), - [this]() { addSitesRoutes(m_vpnProtocol->vpnGateway(), m_settings->routeMode()); }); - } else if (m_settings->routeMode() == Settings::VpnAllExceptSites) { - IpcClient::Interface()->routeAddList(m_vpnProtocol->vpnGateway(), QStringList() << "0.0.0.0/1"); - IpcClient::Interface()->routeAddList(m_vpnProtocol->vpnGateway(), QStringList() << "128.0.0.0/1"); + if (m_settings->routeMode() == Settings::VpnOnlyForwardSites) { + QTimer::singleShot(1000, m_vpnProtocol.data(), + [this]() { addSitesRoutes(m_vpnProtocol->vpnGateway(), m_settings->routeMode()); }); + } else if (m_settings->routeMode() == Settings::VpnAllExceptSites) { + IpcClient::Interface()->routeAddList(m_vpnProtocol->vpnGateway(), QStringList() << "0.0.0.0/1"); + IpcClient::Interface()->routeAddList(m_vpnProtocol->vpnGateway(), QStringList() << "128.0.0.0/1"); - IpcClient::Interface()->routeAddList(m_vpnProtocol->routeGateway(), QStringList() << remoteAddress()); - addSitesRoutes(m_vpnProtocol->routeGateway(), m_settings->routeMode()); + IpcClient::Interface()->routeAddList(m_vpnProtocol->routeGateway(), QStringList() << remoteAddress()); + addSitesRoutes(m_vpnProtocol->routeGateway(), m_settings->routeMode()); + } } } else if (state == Vpn::ConnectionState::Error) { @@ -250,7 +253,13 @@ QString VpnConnection::createVpnConfigurationForProto(int serverIndex, const Ser m_settings->setProtocolConfig(serverIndex, container, proto, protoObject); } - emit m_configurator->newVpnConfigCreated(clientId, "unnamed client", container, credentials); + if ((container != DockerContainer::Cloak && container != DockerContainer::ShadowSocks) || + ((container == DockerContainer::Cloak || container == DockerContainer::ShadowSocks) && proto == Proto::OpenVpn)) { + QEventLoop wait; + emit m_configurator->newVpnConfigCreated(clientId, QString("Admin [%1]").arg(QSysInfo::prettyProductName()), container, credentials); + QObject::connect(m_configurator.get(), &VpnConfigurator::clientModelUpdated, &wait, &QEventLoop::quit); + wait.exec(); + } } return configData; @@ -292,6 +301,7 @@ QJsonObject VpnConnection::createVpnConfiguration(int serverIndex, const ServerC vpnConfiguration[config_key::hostName] = server.value(config_key::hostName).toString(); vpnConfiguration[config_key::description] = server.value(config_key::description).toString(); + vpnConfiguration[config_key::configVersion] = server.value(config_key::configVersion).toInt(); // TODO: try to get hostName, port, description for 3rd party configs // vpnConfiguration[config_key::port] = ...; diff --git a/deploy/build_android.sh b/deploy/build_android.sh index 0c5a80c8d..151cabc23 100755 --- a/deploy/build_android.sh +++ b/deploy/build_android.sh @@ -7,15 +7,18 @@ usage() { cat < -Build AmneziaVPN android client. By default, a signed Android App Bundle (AAB) is built. +Build AmneziaVPN android client. -Options: - -d, --debug Build debug version +Artifact types: + -u, --aab Build Android App Bundle (AAB) -a, --apk ( | all) Build APKs for the specified ABIs or for all available ABIs Available ABIs: 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a' - list of ABIs delimited by ';' + +Options: + -d, --debug Build debug version -b, --build-platform The SDK platform used for building the Java code of the application By default, the latest available platform is used -m, --move Move the build result to the root of the build directory @@ -25,14 +28,14 @@ EOT } BUILD_TYPE="release" -AAB=1 -opts=$(getopt -l debug,apk:,build-platform:,move,help -o "da:b:mh" -- "$@") +opts=$(getopt -l debug,aab,apk:,build-platform:,move,help -o "dua:b:mh" -- "$@") eval set -- "$opts" while true; do case "$1" in -d | --debug) BUILD_TYPE="debug"; shift;; - -a | --apk) ABIS=$2; unset AAB; shift 2;; + -u | --aab) AAB=1; shift;; + -a | --apk) ABIS=$2; shift 2;; -b | --build-platform) ANDROID_BUILD_PLATFORM=$2; shift 2;; -m | --move) MOVE_RESULT=1; shift;; -h | --help) usage; exit 0;; @@ -49,6 +52,11 @@ if [[ -v ABIS && \ exit 1 fi +# At least one artifact type must be specified +if [[ ! (-v AAB || -v ABIS) ]]; then + usage; exit 0 +fi + echo "Build script started..." PROJECT_DIR=$(pwd) @@ -137,7 +145,8 @@ gradle_opts=() if [ -v AAB ]; then gradle_opts+=(bundle"${BUILD_TYPE^}") -else +fi +if [ -v ABIS ]; then gradle_opts+=(assemble"${BUILD_TYPE^}") fi @@ -151,7 +160,9 @@ if [[ -v CI || -v MOVE_RESULT ]]; then if [ -v AAB ]; then mv -u $OUT_APP_DIR/android-build/build/outputs/bundle/$BUILD_TYPE/AmneziaVPN-$BUILD_TYPE.aab \ $PROJECT_DIR/deploy/build/ - else + fi + + if [ -v ABIS ]; then if [ "$ABIS" = "all" ]; then ABIS="x86;x86_64;armeabi-v7a;arm64-v8a" fi diff --git a/deploy/build_linux.sh b/deploy/build_linux.sh index 1b9c698a3..c90e781a4 100755 --- a/deploy/build_linux.sh +++ b/deploy/build_linux.sh @@ -83,6 +83,4 @@ ldd $CQTDEPLOYER_DIR/bin/binarycreator cp -r $PROJECT_DIR/deploy/installer $BUILD_DIR -$CQTDEPLOYER_DIR/binarycreator.sh --offline-only -v -c $BUILD_DIR/installer/config/linux.xml -p $BUILD_DIR/installer/packages -f $PROJECT_DIR/deploy/AmneziaVPN_Linux_Installer - - +$CQTDEPLOYER_DIR/binarycreator.sh --offline-only -v -c $BUILD_DIR/installer/config/linux.xml -p $BUILD_DIR/installer/packages -f $PROJECT_DIR/deploy/AmneziaVPN_Linux_Installer.bin diff --git a/deploy/data/macos/update-resolv-conf.sh b/deploy/data/macos/update-resolv-conf.sh index 09759269f..6d5bf3b5e 100755 --- a/deploy/data/macos/update-resolv-conf.sh +++ b/deploy/data/macos/update-resolv-conf.sh @@ -2,7 +2,7 @@ # Mac name-resolution updater based on @cl's script here: # https://blog.netnerds.net/2011/10/openvpn-update-client-dns-on-mac-os-x-using-from-the-command-line/ -# Openvpn envvar parsing taken from the script in debian's openvpn package. +# Openvpn envar parsing taken from the script in debian's openvpn package. # Smushed together and improved by @andrewgdotcom. # Parses DHCP options from openvpn to update resolv.conf @@ -10,6 +10,8 @@ # up /etc/openvpn/update-resolv-conf # down /etc/openvpn/update-resolv-conf +echo "*** starting update-resolv-config script ***" + [ "$script_type" ] || exit 0 [ "$dev" ] || exit 0 @@ -34,11 +36,11 @@ update_all_dns() echo updating dns for $adapter # set dns server to the vpn dns server if [[ "${SRCHS[@]}" ]]; then - networksetup -setsearchdomains "$adapter" "${SRCHS[@]}" + networksetup -setsearchdomains "$adapter" "${SRCHS[@]}" fi if [[ "${NMSRVRS[@]}" ]]; then - networksetup -setdnsservers "$adapter" "${NMSRVRS[@]}" - fi + networksetup -setdnsservers "$adapter" "${NMSRVRS[@]}" + fi done } @@ -61,7 +63,7 @@ case "$script_type" in if [ "$part1" = "dhcp-option" ] ; then if [ "$part2" = "DNS" ] ; then NMSRVRS=(${NMSRVRS[@]} $part3) - elif [ "$part2" = "DOMAIN" ] ; then + elif [ "$part2" = "DOMAIN" ] || [ "$part2" = "DOMAIN-SEARCH" ]; then SRCHS=(${SRCHS[@]} $part3) fi fi @@ -72,3 +74,5 @@ case "$script_type" in clear_all_dns ;; esac + +echo "*** finished update-resolv-config script ***"