mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-08-05 15:27:02 +03:00
Compare commits
39 Commits
feature/lo
...
update-tra
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4bf8f52149 | ||
|
|
e38a233904 | ||
|
|
9b0495f5e1 | ||
|
|
30ec46a567 | ||
|
|
4055e1b05e | ||
|
|
4d28f81965 | ||
|
|
5c035bb6cf | ||
|
|
26c22327af | ||
|
|
5e9def4184 | ||
|
|
a98c60d967 | ||
|
|
0faf3e6d2f | ||
|
|
a32a26bb67 | ||
|
|
431ece7fff | ||
|
|
5e82c6a558 | ||
|
|
84a2d13b68 | ||
|
|
324b09cca3 | ||
|
|
e70563a071 | ||
|
|
bc2a0ca649 | ||
|
|
3c6ac431b4 | ||
|
|
e96a2b171c | ||
|
|
06d219b92b | ||
|
|
28c77e570b | ||
|
|
6691d2e21c | ||
|
|
c7b5ee1867 | ||
|
|
e4127f47f3 | ||
|
|
4b4563818e | ||
|
|
dbb43272e6 | ||
|
|
5e70eb20c9 | ||
|
|
15176cd3ae | ||
|
|
25df36ad73 | ||
|
|
ea8e3fda56 | ||
|
|
31ae949ba0 | ||
|
|
a17847a90b | ||
|
|
2ca497cd06 | ||
|
|
bd003b149d | ||
|
|
35c1014a44 | ||
|
|
27857cde8a | ||
|
|
b39f583097 | ||
|
|
6c7b65cac6 |
44
.github/workflows/deploy.yml
vendored
44
.github/workflows/deploy.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
- 'cmake/recipes_bootstrap.cmake'
|
||||
|
||||
Bake-Prebuilts-Linux:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
needs: Detect-Changes
|
||||
if: needs.Detect-Changes.outputs.recipes_changed == 'true'
|
||||
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
# ------------------------------------------------------
|
||||
|
||||
Build-Linux-Ubuntu:
|
||||
runs-on: android-runner
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
needs: Bake-Prebuilts-Linux
|
||||
if: ${{ always() }}
|
||||
@@ -88,7 +88,7 @@ jobs:
|
||||
host: 'linux'
|
||||
target: 'desktop'
|
||||
arch: 'linux_gcc_64'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools'
|
||||
dir: ${{ runner.temp }}
|
||||
setup-python: 'true'
|
||||
tools: 'tools_ifw'
|
||||
@@ -201,7 +201,7 @@ jobs:
|
||||
host: 'windows'
|
||||
target: 'desktop'
|
||||
arch: 'win64_msvc2022_64'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools'
|
||||
dir: ${{ runner.temp }}
|
||||
setup-python: 'true'
|
||||
tools: 'tools_ifw'
|
||||
@@ -365,7 +365,7 @@ jobs:
|
||||
version: ${{ env.QT_VERSION }}
|
||||
host: 'mac'
|
||||
target: 'desktop'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools qtmultimedia qthttpserver qtwebsockets'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools qtmultimedia'
|
||||
arch: 'clang_64'
|
||||
dir: ${{ runner.temp }}
|
||||
set-env: 'true'
|
||||
@@ -377,7 +377,7 @@ jobs:
|
||||
version: ${{ env.QT_VERSION }}
|
||||
host: 'mac'
|
||||
target: 'ios'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools qtmultimedia qthttpserver qtwebsockets'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools qtmultimedia'
|
||||
dir: ${{ runner.temp }}
|
||||
setup-python: 'true'
|
||||
set-env: 'true'
|
||||
@@ -414,6 +414,10 @@ jobs:
|
||||
matrix:
|
||||
xcode-version: [16.2, 16.4, 26.4]
|
||||
include:
|
||||
- xcode-version: 16.2
|
||||
os: macos-15
|
||||
- xcode-version: 16.4
|
||||
os: macos-15
|
||||
- xcode-version: 26.4
|
||||
os: macos-26
|
||||
|
||||
@@ -450,7 +454,7 @@ jobs:
|
||||
# ------------------------------------------------------
|
||||
|
||||
Build-MacOS:
|
||||
runs-on: macos-latest
|
||||
runs-on: macos-15
|
||||
|
||||
needs: Bake-Prebuilts-MacOS
|
||||
if: ${{ always() }}
|
||||
@@ -497,7 +501,7 @@ jobs:
|
||||
- name: 'Setup xcode'
|
||||
uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: '16.2.0'
|
||||
xcode-version: '26.3.0'
|
||||
|
||||
- name: 'Install Qt'
|
||||
uses: jurplel/install-qt-action@v4
|
||||
@@ -506,7 +510,7 @@ jobs:
|
||||
host: 'mac'
|
||||
target: 'desktop'
|
||||
arch: 'clang_64'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools'
|
||||
dir: ${{ runner.temp }}
|
||||
setup-python: 'true'
|
||||
set-env: 'true'
|
||||
@@ -527,6 +531,8 @@ jobs:
|
||||
QT_INSTALL_DIR: ${{ runner.temp }}
|
||||
CODESIGN_SIGNATURE: ${{ secrets.MAC_SIGNER_ID }}
|
||||
CODESIGN_INSTALLER_SIGNATURE: ${{ secrets.MAC_INSTALLER_SIGNER_ID }}
|
||||
CODESIGN_KEYCHAIN: ${{ steps.setup-keychain.outputs.keychain-path }}
|
||||
CODESIGN_INSTALLER_KEYCHAIN: ${{ steps.setup-keychain.outputs.keychain-path }}
|
||||
NOTARYTOOL_TEAM_ID: ${{ secrets.MAC_TEAM_ID }}
|
||||
NOTARYTOOL_EMAIL: ${{ secrets.APPLE_DEV_EMAIL }}
|
||||
NOTARYTOOL_PASSWORD: ${{ secrets.APPLE_DEV_PASSWORD }}
|
||||
@@ -550,6 +556,10 @@ jobs:
|
||||
matrix:
|
||||
xcode-version: [16.2, 16.4, 26.4]
|
||||
include:
|
||||
- xcode-version: 16.2
|
||||
os: macos-15
|
||||
- xcode-version: 16.4
|
||||
os: macos-15
|
||||
- xcode-version: 26.4
|
||||
os: macos-26
|
||||
|
||||
@@ -586,7 +596,7 @@ jobs:
|
||||
# ------------------------------------------------------
|
||||
|
||||
Build-MacOS-NE:
|
||||
runs-on: macos-latest
|
||||
runs-on: macos-26
|
||||
|
||||
needs: Bake-Prebuilts-MacOS-NE
|
||||
if: ${{ always() }}
|
||||
@@ -610,6 +620,12 @@ jobs:
|
||||
PREM_V1_ENDPOINT: ${{ secrets.PREM_V1_ENDPOINT }}
|
||||
|
||||
steps:
|
||||
- name: 'Get sources'
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: 'true'
|
||||
fetch-depth: 10
|
||||
|
||||
- uses: ./.github/actions/apple-setup-provisioning-profile
|
||||
with:
|
||||
provisioning_profile_base64: ${{ secrets.MAC_APP_PROVISIONING_PROFILE }}
|
||||
@@ -647,12 +663,6 @@ jobs:
|
||||
go install golang.org/x/mobile/cmd/gomobile@latest
|
||||
gomobile init
|
||||
|
||||
- name: 'Get sources'
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: 'true'
|
||||
fetch-depth: 10
|
||||
|
||||
- name: 'Setup python'
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
@@ -739,7 +749,7 @@ jobs:
|
||||
ANDROID_PLATFORM: android-28
|
||||
NDK_VERSION: 27.0.11718014
|
||||
QT_VERSION: 6.10.3
|
||||
QT_MODULES: 'qtremoteobjects qt5compat qtimageformats qtshadertools qthttpserver qtwebsockets'
|
||||
QT_MODULES: 'qtremoteobjects qt5compat qtimageformats qtshadertools'
|
||||
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
||||
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
||||
FALLBACK_S3_ENDPOINT: ${{ secrets.FALLBACK_S3_ENDPOINT }}
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -10,9 +10,7 @@ deploy/build_64/*
|
||||
winbuild*.bat
|
||||
.cache/
|
||||
.vscode/
|
||||
.cursorignore
|
||||
.cursor/
|
||||
.venv/
|
||||
|
||||
|
||||
# Qt-es
|
||||
/.qmake.cache
|
||||
|
||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -7,9 +7,6 @@
|
||||
[submodule "client/3rd/amneziawg-apple"]
|
||||
path = client/3rd/amneziawg-apple
|
||||
url = https://github.com/amnezia-vpn/amneziawg-apple
|
||||
[submodule "client/3rd/QSimpleCrypto"]
|
||||
path = client/3rd/QSimpleCrypto
|
||||
url = https://github.com/amnezia-vpn/QSimpleCrypto.git
|
||||
[submodule "client/3rd/qtgamepad"]
|
||||
path = client/3rd/qtgamepad
|
||||
url = https://github.com/amnezia-vpn/qtgamepad.git
|
||||
|
||||
@@ -4,7 +4,7 @@ set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
set(PROJECT AmneziaVPN)
|
||||
set(AMNEZIAVPN_VERSION 4.9.0.3)
|
||||
set(AMNEZIAVPN_VERSION 5.0.0.5)
|
||||
|
||||
set(QT_CREATOR_SKIP_PACKAGE_MANAGER_SETUP ON CACHE BOOL "" FORCE)
|
||||
set(CMAKE_PROJECT_TOP_LEVEL_INCLUDES
|
||||
@@ -28,7 +28,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 2126)
|
||||
set(APP_ANDROID_VERSION_CODE 2138)
|
||||
|
||||
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
|
||||
set(MZ_PLATFORM_NAME "linux")
|
||||
|
||||
14
README.md
14
README.md
@@ -38,14 +38,14 @@
|
||||
|
||||
## Links
|
||||
|
||||
- [https://amnezia.org](https://amnezia.org) - Project website | [Alternative link (mirror)](https://storage.googleapis.com/kldscp/amnezia.org)
|
||||
- [https://docs.amnezia.org](https://docs.amnezia.org) - Documentation
|
||||
- [https://amnezia.org](https://amnezia.org/?utm_source=github&utm_campaign=amnezia_website-read) - Project website | [Alternative link (mirror)](https://storage.googleapis.com/amnezia/amnezia.org?utm_source=github&utm_campaign=amnezia_website-read)
|
||||
- [https://docs.amnezia.org](https://docs.amnezia.org/?utm_source=github&utm_campaign=amnezia_website-read) - Documentation | [Alternative link (mirror)](https://storage.googleapis.com/amnezia/docs?utm_source=github&utm_campaign=amnezia_website-read)
|
||||
- [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_ir](https://t.me/amnezia_vpn_ir) - Telegram support channel (Farsi)
|
||||
- [https://t.me/amnezia_vpn_mm](https://t.me/amnezia_vpn_mm) - Telegram support channel (Myanmar)
|
||||
- [https://t.me/amnezia_vpn](https://t.me/amnezia_vpn) - Telegram support channel (Russian)
|
||||
- [https://vpnpay.io/en/amnezia-premium/](https://vpnpay.io/en/amnezia-premium/) - Amnezia Premium
|
||||
- [https://telegram.me/amnezia_vpn_en](https://telegram.me/amnezia_vpn_en) - Telegram support channel (English)
|
||||
- [https://telegram.me/amnezia_vpn_ir](https://telegram.me/amnezia_vpn_ir) - Telegram support channel (Farsi)
|
||||
- [https://telegram.me/amnezia_vpn_mm](https://telegram.me/amnezia_vpn_mm) - Telegram support channel (Myanmar)
|
||||
- [https://telegram.me/amnezia_vpn](https://telegram.me/amnezia_vpn) - Telegram support channel (Russian)
|
||||
- [Get Premium for 6 or 12 months](https://storage.googleapis.com/amnezia/pay?utm_source=github&utm_campaign=ampay-read)
|
||||
|
||||
## Tech
|
||||
|
||||
|
||||
16
README_RU.md
16
README_RU.md
@@ -28,21 +28,21 @@
|
||||
|
||||
- Простой в использовании — введите IP-адрес, SSH-логин и пароль, и Amnezia автоматически установит VPN-контейнеры Docker на ваш сервер и подключится к VPN.
|
||||
- Классические VPN-протоколы: OpenVPN, WireGuard и IKEv2.
|
||||
- Протоколы с маскировкой трафика (обфускацией): OpenVPN с плагином [Cloak](https://github.com/cbeuw/Cloak), Shadowsocks (OpenVPN over Shadowsocks), [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) and XRay.
|
||||
- Протоколы с маскировкой трафика (обфускацией): OpenVPN с плагином [Cloak](https://github.com/cbeuw/Cloak), Shadowsocks (OpenVPN over Shadowsocks), [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) и XRay.
|
||||
- Поддержка Split Tunneling — добавляйте любые сайты или приложения в список, чтобы включить VPN только для них.
|
||||
- Поддерживает платформы: Windows, macOS, Linux, Android, iOS.
|
||||
- Поддержка конфигурации протокола AmneziaWG на [бета-прошивке Keenetic](https://docs.keenetic.com/ua/air/kn-1611/en/6319-latest-development-release.html#UUID-186c4108-5afd-c10b-f38a-cdff6c17fab3_section-idm33192196168192-improved).
|
||||
|
||||
## Ссылки
|
||||
|
||||
- [https://amnezia.org](https://amnezia.org) - Веб-сайт проекта | [Альтернативная ссылка (зеркало)](https://storage.googleapis.com/kldscp/amnezia.org)
|
||||
- [https://docs.amnezia.org](https://docs.amnezia.org) - Документация
|
||||
- [https://amnezia.org](https://amnezia.org/?utm_source=github&utm_campaign=amnezia_website-read) - Веб-сайт проекта | [Альтернативная ссылка (зеркало)](https://storage.googleapis.com/amnezia/amnezia.org?utm_source=github&utm_campaign=amnezia_website-read)
|
||||
- [https://docs.amnezia.org](https://docs.amnezia.org/?utm_source=github&utm_campaign=amnezia_website-read) - Документация | [Альтернативная ссылка (зеркало)](https://storage.googleapis.com/amnezia/docs?utm_source=github&utm_campaign=amnezia_website-read)
|
||||
- [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 (Английский)
|
||||
- [https://t.me/amnezia_vpn_ir](https://t.me/amnezia_vpn_ir) - Канал поддержки в Telegram (Фарси)
|
||||
- [https://t.me/amnezia_vpn_mm](https://t.me/amnezia_vpn_mm) - Канал поддержки в Telegram (Мьянма)
|
||||
- [https://t.me/amnezia_vpn](https://t.me/amnezia_vpn) - Канал поддержки в Telegram (Русский)
|
||||
- [https://vpnpay.io/en/amnezia-premium/](https://vpnpay.io/en/amnezia-premium/) - Amnezia Premium | [Зеркало](https://storage.googleapis.com/kldscp/vpnpay.io/ru/amnezia-premium\)
|
||||
- [https://telegram.me/amnezia_vpn_en](https://telegram.me/amnezia_vpn_en) - Канал поддержки в Telegram (Английский)
|
||||
- [https://telegram.me/amnezia_vpn_ir](https://telegram.me/amnezia_vpn_ir) - Канал поддержки в Telegram (Фарси)
|
||||
- [https://telegram.me/amnezia_vpn_mm](https://telegram.me/amnezia_vpn_mm) - Канал поддержки в Telegram (Мьянма)
|
||||
- [https://telegram.me/amnezia_vpn](https://telegram.me/amnezia_vpn) - Канал поддержки в Telegram (Русский)
|
||||
- [Оформите Premium на 6 или 12 месяцев](https://storage.googleapis.com/amnezia/pay?utm_source=github&utm_campaign=ampay-read)
|
||||
|
||||
## Технологии
|
||||
|
||||
|
||||
1
client/3rd/QSimpleCrypto
vendored
1
client/3rd/QSimpleCrypto
vendored
Submodule client/3rd/QSimpleCrypto deleted from c99b33f0e0
2
client/3rd/amneziawg-apple
vendored
2
client/3rd/amneziawg-apple
vendored
Submodule client/3rd/amneziawg-apple updated: cf63135331...4bafa5958a
@@ -14,10 +14,6 @@ set(PACKAGES
|
||||
Core5Compat Concurrent LinguistTools
|
||||
)
|
||||
|
||||
if(NOT ANDROID AND NOT IOS)
|
||||
list(APPEND PACKAGES HttpServer)
|
||||
endif()
|
||||
|
||||
execute_process(
|
||||
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
|
||||
COMMAND git rev-parse --short HEAD
|
||||
@@ -43,6 +39,7 @@ if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID))
|
||||
endif()
|
||||
|
||||
find_package(Qt6 REQUIRED COMPONENTS ${PACKAGES})
|
||||
find_package(OpenSSL REQUIRED)
|
||||
|
||||
set(LIBS ${LIBS}
|
||||
Qt6::Core Qt6::Gui
|
||||
@@ -51,10 +48,6 @@ set(LIBS ${LIBS}
|
||||
Qt6::Core5Compat Qt6::Concurrent
|
||||
)
|
||||
|
||||
if(NOT ANDROID AND NOT IOS)
|
||||
list(APPEND LIBS Qt6::HttpServer)
|
||||
endif()
|
||||
|
||||
if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID))
|
||||
set(LIBS ${LIBS} Qt6::Widgets)
|
||||
endif()
|
||||
@@ -111,6 +104,7 @@ include(${CMAKE_CURRENT_LIST_DIR}/cmake/sources.cmake)
|
||||
include_directories(
|
||||
${CMAKE_CURRENT_LIST_DIR}/../ipc
|
||||
${CMAKE_CURRENT_LIST_DIR}/../common/logger
|
||||
${CMAKE_CURRENT_LIST_DIR}/../common/crypto
|
||||
${CMAKE_CURRENT_LIST_DIR}
|
||||
${CMAKE_CURRENT_BINARY_DIR}
|
||||
)
|
||||
@@ -181,6 +175,7 @@ endif()
|
||||
if(LINUX AND NOT ANDROID)
|
||||
set(LIBS ${LIBS} -static-libstdc++ -static-libgcc -ldl)
|
||||
link_directories(${CMAKE_CURRENT_LIST_DIR}/platforms/linux)
|
||||
set(LIBS ${LIBS} -Wl,--no-as-needed $<TARGET_LINKER_FILE:Qt6::RemoteObjects> -Wl,--as-needed)
|
||||
endif()
|
||||
|
||||
if(WIN32 OR (APPLE AND NOT IOS AND NOT MACOS_NE) OR (LINUX AND NOT ANDROID))
|
||||
@@ -238,7 +233,7 @@ install(RUNTIME_DEPENDENCY_SET client_deps
|
||||
[[hvsifiletrust\.dll]]
|
||||
[[libc\.so\..*]] [[libgcc_s\.so\..*]] [[libm\.so\..*]] [[libstdc\+\+\.so\..*]]
|
||||
[[.*\.framework]]
|
||||
[[^[Qq]t.*]]
|
||||
[[^(lib)?[Qq]t.*]]
|
||||
POST_EXCLUDE_REGEXES
|
||||
[[^.*[\\/]system32[\\/].*\.dll$]]
|
||||
[[^/lib.*]]
|
||||
@@ -265,6 +260,10 @@ install(SCRIPT ${QT_DEPLOY_SCRIPT}
|
||||
|
||||
if (APPLE AND NOT IOS AND NOT MACOS_NE)
|
||||
list(APPEND OVPN_SCRIPTS "${CMAKE_SOURCE_DIR}/deploy/data/macos/update-resolv-conf.sh")
|
||||
set_target_properties(${PROJECT} PROPERTIES
|
||||
XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED "NO"
|
||||
XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED "NO"
|
||||
)
|
||||
endif()
|
||||
if (LINUX AND NOT ANDROID)
|
||||
list(APPEND OVPN_SCRIPTS "${CMAKE_SOURCE_DIR}/deploy/data/linux/update-resolv-conf.sh")
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "logger.h"
|
||||
#include "ui/controllers/qml/pageController.h"
|
||||
#include "ui/models/installedAppsModel.h"
|
||||
#include "ui/utils/mtProxyPublicHostInput.h"
|
||||
#include "version.h"
|
||||
|
||||
#include "platforms/ios/QRCodeReaderBase.h"
|
||||
@@ -144,6 +145,9 @@ void AmneziaApplication::init()
|
||||
|
||||
m_coreController.reset(new CoreController(m_vpnConnection, m_settings, m_engine));
|
||||
|
||||
m_marketplaceUpdateController.reset(new MarketplaceUpdateController());
|
||||
m_marketplaceUpdateController->start();
|
||||
|
||||
m_engine->addImportPath("qrc:/ui/qml/Modules/");
|
||||
|
||||
if (m_parser.isSet(m_optImport)) {
|
||||
@@ -221,6 +225,9 @@ void AmneziaApplication::registerTypes()
|
||||
|
||||
qmlRegisterType<InstalledAppsModel>("InstalledAppsModel", 1, 0, "InstalledAppsModel");
|
||||
|
||||
qmlRegisterType<PublicHostInputValidator>("MtProxyConfig", 1, 0, "PublicHostInputValidator");
|
||||
qmlRegisterType<PublicHostInputValidator>("TelemtConfig", 1, 0, "PublicHostInputValidator");
|
||||
|
||||
amnezia::declareQmlProtocolEnum();
|
||||
Vpn::declareQmlVpnConnectionStateEnum();
|
||||
PageLoader::declareQmlPageEnum();
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
#include "core/controllers/coreController.h"
|
||||
#include "secureQSettings.h"
|
||||
#include "ui/controllers/marketplaceUpdateController.h"
|
||||
#include "vpnConnection.h"
|
||||
#include "ui/models/containerProps.h"
|
||||
#include "ui/models/protocolProps.h"
|
||||
@@ -56,6 +57,7 @@ private:
|
||||
SecureQSettings* m_settings;
|
||||
|
||||
QScopedPointer<CoreController> m_coreController;
|
||||
QScopedPointer<MarketplaceUpdateController> m_marketplaceUpdateController;
|
||||
|
||||
QSharedPointer<ContainerProps> m_containerProps;
|
||||
QSharedPointer<ProtocolProps> m_protocolProps;
|
||||
|
||||
@@ -3,6 +3,13 @@ package org.amnezia.vpn
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.app.Dialog
|
||||
import android.graphics.Typeface
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.view.Gravity
|
||||
import android.widget.Button
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import android.app.NotificationManager
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.BroadcastReceiver
|
||||
@@ -42,6 +49,7 @@ import androidx.core.view.OnApplyWindowInsetsListener
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import kotlin.LazyThreadSafetyMode.NONE
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
@@ -99,6 +107,8 @@ class AmneziaActivity : QtActivity() {
|
||||
private var pendingOpenFileUri: String? = null
|
||||
private var openFileDeliveryScheduled = false
|
||||
|
||||
private var updateCoverDialog: Dialog? = null
|
||||
|
||||
private val vpnServiceEventHandler: Handler by lazy(NONE) {
|
||||
object : Handler(Looper.getMainLooper()) {
|
||||
override fun handleMessage(msg: Message) {
|
||||
@@ -486,6 +496,116 @@ class AmneziaActivity : QtActivity() {
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
fun showUpdateCover() {
|
||||
runOnUiThread {
|
||||
if (isFinishing || isDestroyed || updateCoverDialog != null) return@runOnUiThread
|
||||
val dialog = Dialog(this, android.R.style.Theme_Black_NoTitleBar_Fullscreen)
|
||||
dialog.setCancelable(false)
|
||||
val root = LinearLayout(this).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
gravity = Gravity.CENTER
|
||||
setBackgroundColor(0xFF0E0E11.toInt())
|
||||
}
|
||||
dialog.setContentView(root)
|
||||
dialog.show()
|
||||
updateCoverDialog = dialog
|
||||
}
|
||||
}
|
||||
|
||||
fun hideUpdateCover() {
|
||||
runOnUiThread {
|
||||
updateCoverDialog?.dismiss()
|
||||
updateCoverDialog = null
|
||||
}
|
||||
}
|
||||
|
||||
fun showUpdatePrompt(title: String, message: String, updateTitle: String, skipTitle: String, storeUrl: String) {
|
||||
runOnUiThread {
|
||||
if (isFinishing || isDestroyed) return@runOnUiThread
|
||||
|
||||
val dialog = updateCoverDialog ?: Dialog(this, android.R.style.Theme_Black_NoTitleBar_Fullscreen).also {
|
||||
it.setCancelable(false)
|
||||
it.show()
|
||||
updateCoverDialog = it
|
||||
}
|
||||
|
||||
val density = resources.displayMetrics.density
|
||||
fun dp(value: Int) = (value * density).toInt()
|
||||
|
||||
val root = LinearLayout(this).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
gravity = Gravity.CENTER
|
||||
setBackgroundColor(0xFF0E0E11.toInt())
|
||||
setPadding(dp(32), dp(32), dp(32), dp(32))
|
||||
}
|
||||
|
||||
val titleView = TextView(this).apply {
|
||||
text = title
|
||||
textSize = 22f
|
||||
setTextColor(0xFFFFFFFF.toInt())
|
||||
gravity = Gravity.CENTER
|
||||
typeface = Typeface.create(typeface, Typeface.BOLD)
|
||||
}
|
||||
|
||||
val messageView = TextView(this).apply {
|
||||
text = message
|
||||
textSize = 16f
|
||||
setTextColor(0xFFC7C8CB.toInt())
|
||||
gravity = Gravity.CENTER
|
||||
setPadding(0, dp(16), 0, dp(28))
|
||||
}
|
||||
|
||||
val updateButton = Button(this).apply {
|
||||
text = updateTitle
|
||||
isAllCaps = false
|
||||
textSize = 17f
|
||||
setTextColor(0xFF0E0E11.toInt())
|
||||
stateListAnimator = null
|
||||
background = GradientDrawable().apply {
|
||||
cornerRadius = dp(12).toFloat()
|
||||
setColor(0xFFFBB26A.toInt())
|
||||
}
|
||||
setOnClickListener {
|
||||
try {
|
||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(storeUrl)))
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Log.w(TAG, "open store failed: ${e.message}")
|
||||
}
|
||||
hideUpdateCover()
|
||||
}
|
||||
}
|
||||
|
||||
val skipButton = Button(this).apply {
|
||||
text = skipTitle
|
||||
isAllCaps = false
|
||||
textSize = 17f
|
||||
setTextColor(0xFFD7D8DB.toInt())
|
||||
stateListAnimator = null
|
||||
background = GradientDrawable().apply {
|
||||
cornerRadius = dp(12).toFloat()
|
||||
setColor(0x00000000)
|
||||
setStroke(dp(1), 0xFF2C2D30.toInt())
|
||||
}
|
||||
setOnClickListener { hideUpdateCover() }
|
||||
}
|
||||
|
||||
val updateParams = LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, dp(52)
|
||||
).apply { topMargin = dp(8) }
|
||||
|
||||
val skipParams = LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, dp(52)
|
||||
).apply { topMargin = dp(12) }
|
||||
|
||||
root.addView(titleView)
|
||||
root.addView(messageView)
|
||||
root.addView(updateButton, updateParams)
|
||||
root.addView(skipButton, skipParams)
|
||||
|
||||
dialog.setContentView(root)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
Log.d(TAG, "Process activity result, code: ${actionCodeToString(requestCode)}, " +
|
||||
"resultCode: $resultCode, data: $data")
|
||||
@@ -763,7 +883,13 @@ class AmneziaActivity : QtActivity() {
|
||||
fun openFile(filter: String?) {
|
||||
Log.v(TAG, "Open file with filter: $filter")
|
||||
mainScope.launch {
|
||||
val intent = if (!isOnTv()) {
|
||||
val systemPickerPackage = listOf("com.google.android.documentsui", "com.android.documentsui")
|
||||
.firstOrNull { pkg ->
|
||||
try { packageManager.getPackageInfo(pkg, 0); true }
|
||||
catch (_: PackageManager.NameNotFoundException) { false }
|
||||
}
|
||||
|
||||
val intent = if (!isOnTv() && systemPickerPackage != null) {
|
||||
val mimeTypes = if (!filter.isNullOrEmpty()) {
|
||||
val extensionRegex = "\\*\\.([a-z0-9]+)".toRegex(IGNORE_CASE)
|
||||
val mime = MimeTypeMap.getSingleton()
|
||||
@@ -789,6 +915,7 @@ class AmneziaActivity : QtActivity() {
|
||||
else -> type = "*/*"
|
||||
}
|
||||
}
|
||||
`package` = systemPickerPackage
|
||||
}
|
||||
} else {
|
||||
Intent(this@AmneziaActivity, TvFilePicker::class.java)
|
||||
@@ -800,8 +927,11 @@ class AmneziaActivity : QtActivity() {
|
||||
if (isOnTv() && it?.hasExtra("activityNotFound") == true) {
|
||||
showNoFileBrowserAlertDialog()
|
||||
}
|
||||
val uri = it?.data?.apply {
|
||||
grantUriPermission(packageName, this, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
val uri = it?.data?.let { u ->
|
||||
if (u.scheme == "content") {
|
||||
try { grantUriPermission(packageName, u, Intent.FLAG_GRANT_READ_URI_PERMISSION) } catch (_: Exception) {}
|
||||
}
|
||||
u
|
||||
}?.toString() ?: ""
|
||||
Log.v(TAG, "Open file: $uri")
|
||||
if (uri.isNotEmpty()) {
|
||||
@@ -841,7 +971,12 @@ class AmneziaActivity : QtActivity() {
|
||||
Log.v(TAG, "Get fd for $fileName")
|
||||
return blockingCall(Dispatchers.IO) {
|
||||
try {
|
||||
pfd = contentResolver.openFileDescriptor(Uri.parse(fileName), "r")
|
||||
val uri = Uri.parse(fileName)
|
||||
pfd = if (uri.scheme == "file") {
|
||||
ParcelFileDescriptor.open(File(uri.path!!), ParcelFileDescriptor.MODE_READ_ONLY)
|
||||
} else {
|
||||
contentResolver.openFileDescriptor(uri, "r")
|
||||
}
|
||||
pfd?.fd ?: -1
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to get fd: $e")
|
||||
@@ -1061,12 +1196,10 @@ class AmneziaActivity : QtActivity() {
|
||||
@Suppress("unused")
|
||||
fun sendTouch(x: Float, y: Float) {
|
||||
Log.v(TAG, "Send touch: $x, $y")
|
||||
blockingCall {
|
||||
findQtWindow(window.decorView)?.let {
|
||||
Log.v(TAG, "Send touch to $it")
|
||||
it.dispatchTouchEvent(createEvent(x, y, SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN))
|
||||
it.dispatchTouchEvent(createEvent(x, y, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP))
|
||||
}
|
||||
findQtWindow(window.decorView)?.let {
|
||||
Log.v(TAG, "Send touch to $it")
|
||||
it.dispatchTouchEvent(createEvent(x, y, SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN))
|
||||
it.dispatchTouchEvent(createEvent(x, y, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,30 +1,36 @@
|
||||
package org.amnezia.vpn
|
||||
|
||||
import android.Manifest
|
||||
import android.app.AlertDialog
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import org.amnezia.vpn.util.Log
|
||||
import java.io.File
|
||||
|
||||
private const val TAG = "TvFilePicker"
|
||||
private const val READ_STORAGE_REQUEST_CODE = 1001
|
||||
|
||||
class TvFilePicker : ComponentActivity() {
|
||||
|
||||
private val fileChooseResultLauncher = registerForActivityResult(object : ActivityResultContracts.OpenDocument() {
|
||||
// SAF launcher for Android 10+ where File API is blocked by scoped storage
|
||||
private val safLauncher = registerForActivityResult(object : ActivityResultContracts.OpenDocument() {
|
||||
override fun createIntent(context: Context, input: Array<String>): Intent {
|
||||
val intent = super.createIntent(context, input)
|
||||
|
||||
val activitiesToResolveIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
val activities = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
context.packageManager.queryIntentActivities(intent, PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong()))
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
context.packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)
|
||||
}
|
||||
if (activitiesToResolveIntent.all {
|
||||
if (activities.all {
|
||||
val name = it.activityInfo.packageName
|
||||
name.startsWith("com.google.android.tv.frameworkpackagestubs") || name.startsWith("com.android.tv.frameworkpackagestubs")
|
||||
}) {
|
||||
@@ -32,38 +38,140 @@ class TvFilePicker : ComponentActivity() {
|
||||
}
|
||||
return intent
|
||||
}
|
||||
}) {
|
||||
}) { uri ->
|
||||
setResult(RESULT_OK, Intent().apply {
|
||||
data = it
|
||||
data = uri
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
})
|
||||
finish()
|
||||
}
|
||||
|
||||
private val directoryStack = ArrayDeque<File>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
Log.v(TAG, "onCreate")
|
||||
getFile()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
launchSaf()
|
||||
} else {
|
||||
checkPermissionAndBrowse()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
Log.v(TAG, "onNewIntent")
|
||||
getFile()
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onBackPressed() {
|
||||
navigateBack()
|
||||
}
|
||||
|
||||
private fun getFile() {
|
||||
private fun launchSaf() {
|
||||
try {
|
||||
Log.v(TAG, "getFile")
|
||||
fileChooseResultLauncher.launch(arrayOf("*/*"))
|
||||
safLauncher.launch(arrayOf("*/*"))
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
Log.w(TAG, "Activity not found")
|
||||
Log.w(TAG, "No SAF activity found")
|
||||
setResult(RESULT_CANCELED, Intent().apply { putExtra("activityNotFound", true) })
|
||||
finish()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to get file: $e")
|
||||
Log.e(TAG, "SAF launch failed: $e")
|
||||
setResult(RESULT_CANCELED)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkPermissionAndBrowse() {
|
||||
if (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
||||
requestPermissions(arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), READ_STORAGE_REQUEST_CODE)
|
||||
} else {
|
||||
showRootDirectory()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
if (requestCode == READ_STORAGE_REQUEST_CODE &&
|
||||
grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED) {
|
||||
showRootDirectory()
|
||||
} else {
|
||||
setResult(RESULT_CANCELED)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showRootDirectory() {
|
||||
@Suppress("DEPRECATION")
|
||||
val primaryExternal = Environment.getExternalStorageDirectory()
|
||||
val storageDir = File("/storage")
|
||||
// Pre-seed stack with /storage so Back from primary storage goes there (USB drives etc.)
|
||||
if (storageDir.exists() && storageDir.canonicalPath != primaryExternal.canonicalPath) {
|
||||
directoryStack.addLast(storageDir)
|
||||
}
|
||||
showDirectory(primaryExternal)
|
||||
}
|
||||
|
||||
private fun navigateBack() {
|
||||
if (directoryStack.size > 1) {
|
||||
directoryStack.removeLast()
|
||||
val parent = directoryStack.removeLast()
|
||||
showDirectory(parent)
|
||||
} else {
|
||||
setResult(RESULT_CANCELED)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showDirectory(dir: File) {
|
||||
directoryStack.addLast(dir)
|
||||
Log.v(TAG, "Showing directory: ${dir.absolutePath}")
|
||||
|
||||
val entries = try {
|
||||
dir.listFiles()
|
||||
?.sortedWith(compareBy({ !it.isDirectory }, { it.name.lowercase() }))
|
||||
?: emptyList()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to list directory: $e")
|
||||
emptyList()
|
||||
}
|
||||
|
||||
val names = entries.map { if (it.isDirectory) "[${it.name}]" else it.name }.toTypedArray()
|
||||
|
||||
val builder = AlertDialog.Builder(this)
|
||||
.setTitle(dir.absolutePath)
|
||||
|
||||
if (entries.isEmpty()) {
|
||||
builder.setMessage("No files available")
|
||||
} else {
|
||||
builder.setItems(names) { dialog, which ->
|
||||
dialog.dismiss()
|
||||
val selected = entries[which]
|
||||
if (selected.isDirectory) {
|
||||
showDirectory(selected)
|
||||
} else {
|
||||
Log.v(TAG, "Selected file: ${selected.absolutePath}")
|
||||
setResult(RESULT_OK, Intent().apply {
|
||||
data = Uri.fromFile(selected)
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
})
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (directoryStack.size > 1) {
|
||||
builder.setNegativeButton("↑ Back") { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
navigateBack()
|
||||
}
|
||||
} else {
|
||||
builder.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
setResult(RESULT_CANCELED)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
builder.setOnCancelListener {
|
||||
setResult(RESULT_CANCELED)
|
||||
finish()
|
||||
}
|
||||
|
||||
builder.show()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ open class Wireguard : Protocol() {
|
||||
configExtensionParameters(configData)
|
||||
}
|
||||
|
||||
configData.optStringOrNull("persistent_keep_alive")?.let { setPersistentKeepalive(it.toInt()) }
|
||||
configData.optStringOrNull("persistent_keep_alive")?.let { setPersistentKeepalive(it) }
|
||||
configData.getString("client_priv_key").let { setPrivateKeyHex(it.base64ToHex()) }
|
||||
configData.getString("server_pub_key").let { setPublicKeyHex(it.base64ToHex()) }
|
||||
configData.optStringOrNull("psk_key")?.let { setPreSharedKeyHex(it.base64ToHex()) }
|
||||
@@ -134,6 +134,20 @@ open class Wireguard : Protocol() {
|
||||
configData.optStringOrNull("I3")?.let { setI3(it) }
|
||||
configData.optStringOrNull("I4")?.let { setI4(it) }
|
||||
configData.optStringOrNull("I5")?.let { setI5(it) }
|
||||
configData.optStringOrNull("HeaderProtectionKey")?.trim()?.takeIf { it.isNotEmpty() }
|
||||
?.let { setHeaderProtectionKey(it.base64ToHex()) }
|
||||
configData.optStringOrNull("ContentPaddingAddition")?.trim()?.takeIf { it.isNotEmpty() }
|
||||
?.let { setContentPaddingAddition(it) }
|
||||
configData.optStringOrNull("RekeyAfterTime")?.trim()?.takeIf { it.isNotEmpty() }
|
||||
?.let { setRekeyAfterTime(it) }
|
||||
configData.optStringOrNull("RekeyTimeout")?.trim()?.takeIf { it.isNotEmpty() }
|
||||
?.let { setRekeyTimeout(it) }
|
||||
configData.optStringOrNull("RejectAfterTime")?.trim()?.takeIf { it.isNotEmpty() }
|
||||
?.let { setRejectAfterTime(it) }
|
||||
configData.optStringOrNull("KeepaliveTimeout")?.trim()?.takeIf { it.isNotEmpty() }
|
||||
?.let { setKeepaliveTimeout(it) }
|
||||
configData.optStringOrNull("MaxHandshakeAttempts")?.trim()?.takeIf { it.isNotEmpty() }
|
||||
?.let { setMaxHandshakeAttempts(it) }
|
||||
}
|
||||
|
||||
private fun start(
|
||||
|
||||
@@ -10,7 +10,7 @@ private const val WIREGUARD_DEFAULT_MTU = 1280
|
||||
open class WireguardConfig protected constructor(
|
||||
protocolConfigBuilder: ProtocolConfig.Builder,
|
||||
val endpoint: InetEndpoint,
|
||||
val persistentKeepalive: Int,
|
||||
val persistentKeepalive: String?,
|
||||
val publicKeyHex: String,
|
||||
val preSharedKeyHex: String?,
|
||||
val privateKeyHex: String,
|
||||
@@ -31,6 +31,13 @@ open class WireguardConfig protected constructor(
|
||||
var i3: String?,
|
||||
var i4: String?,
|
||||
var i5: String?,
|
||||
val headerProtectionKeyHex: String?,
|
||||
val contentPaddingAddition: String?,
|
||||
val rekeyAfterTime: String?,
|
||||
val rekeyTimeout: String?,
|
||||
val rejectAfterTime: String?,
|
||||
val keepaliveTimeout: String?,
|
||||
val maxHandshakeAttempts: String?,
|
||||
) : ProtocolConfig(protocolConfigBuilder) {
|
||||
|
||||
protected constructor(builder: Builder) : this(
|
||||
@@ -57,6 +64,13 @@ open class WireguardConfig protected constructor(
|
||||
builder.i3,
|
||||
builder.i4,
|
||||
builder.i5,
|
||||
builder.headerProtectionKeyHex,
|
||||
builder.contentPaddingAddition,
|
||||
builder.rekeyAfterTime,
|
||||
builder.rekeyTimeout,
|
||||
builder.rejectAfterTime,
|
||||
builder.keepaliveTimeout,
|
||||
builder.maxHandshakeAttempts,
|
||||
)
|
||||
|
||||
fun toWgUserspaceString(): String = with(StringBuilder()) {
|
||||
@@ -87,6 +101,13 @@ open class WireguardConfig protected constructor(
|
||||
i4?.let { appendLine("i4=$it") }
|
||||
i5?.let { appendLine("i5=$it") }
|
||||
}
|
||||
headerProtectionKeyHex?.takeIf { it.isNotEmpty() }?.let { appendLine("header_protection_key=$it") }
|
||||
contentPaddingAddition?.takeIf { it.isNotEmpty() }?.let { appendLine("content_padding_addition=$it") }
|
||||
rekeyAfterTime?.takeIf { it.isNotEmpty() }?.let { appendLine("rekey_after_time=$it") }
|
||||
rekeyTimeout?.takeIf { it.isNotEmpty() }?.let { appendLine("rekey_timeout=$it") }
|
||||
rejectAfterTime?.takeIf { it.isNotEmpty() }?.let { appendLine("reject_after_time=$it") }
|
||||
keepaliveTimeout?.takeIf { it.isNotEmpty() }?.let { appendLine("keepalive_timeout=$it") }
|
||||
maxHandshakeAttempts?.takeIf { it.isNotEmpty() }?.let { appendLine("max_handshake_attempts=$it") }
|
||||
}
|
||||
|
||||
private fun validateProtocolExtensionParameters() {
|
||||
@@ -107,7 +128,7 @@ open class WireguardConfig protected constructor(
|
||||
appendLine("allowed_ip=${route.inetNetwork}")
|
||||
}
|
||||
appendLine("endpoint=$endpoint")
|
||||
if (persistentKeepalive != 0)
|
||||
if (!persistentKeepalive.isNullOrEmpty() && persistentKeepalive != "0")
|
||||
appendLine("persistent_keepalive_interval=$persistentKeepalive")
|
||||
if (preSharedKeyHex != null)
|
||||
appendLine("preshared_key=$preSharedKeyHex")
|
||||
@@ -117,7 +138,7 @@ open class WireguardConfig protected constructor(
|
||||
internal lateinit var endpoint: InetEndpoint
|
||||
private set
|
||||
|
||||
internal var persistentKeepalive: Int = 0
|
||||
internal var persistentKeepalive: String? = null
|
||||
private set
|
||||
|
||||
internal lateinit var publicKeyHex: String
|
||||
@@ -149,10 +170,17 @@ open class WireguardConfig protected constructor(
|
||||
internal var i3: String? = null
|
||||
internal var i4: String? = null
|
||||
internal var i5: String? = null
|
||||
internal var headerProtectionKeyHex: String? = null
|
||||
internal var contentPaddingAddition: String? = null
|
||||
internal var rekeyAfterTime: String? = null
|
||||
internal var rekeyTimeout: String? = null
|
||||
internal var rejectAfterTime: String? = null
|
||||
internal var keepaliveTimeout: String? = null
|
||||
internal var maxHandshakeAttempts: String? = null
|
||||
|
||||
fun setEndpoint(endpoint: InetEndpoint) = apply { this.endpoint = endpoint }
|
||||
|
||||
fun setPersistentKeepalive(persistentKeepalive: Int) = apply { this.persistentKeepalive = persistentKeepalive }
|
||||
fun setPersistentKeepalive(persistentKeepalive: String) = apply { this.persistentKeepalive = persistentKeepalive }
|
||||
|
||||
fun setPublicKeyHex(publicKeyHex: String) = apply { this.publicKeyHex = publicKeyHex }
|
||||
|
||||
@@ -178,6 +206,13 @@ open class WireguardConfig protected constructor(
|
||||
fun setI3(i3: String) = apply { this.i3 = i3 }
|
||||
fun setI4(i4: String) = apply { this.i4 = i4 }
|
||||
fun setI5(i5: String) = apply { this.i5 = i5 }
|
||||
fun setHeaderProtectionKey(headerProtectionKeyHex: String) = apply { this.headerProtectionKeyHex = headerProtectionKeyHex }
|
||||
fun setContentPaddingAddition(contentPaddingAddition: String) = apply { this.contentPaddingAddition = contentPaddingAddition }
|
||||
fun setRekeyAfterTime(rekeyAfterTime: String) = apply { this.rekeyAfterTime = rekeyAfterTime }
|
||||
fun setRekeyTimeout(rekeyTimeout: String) = apply { this.rekeyTimeout = rekeyTimeout }
|
||||
fun setRejectAfterTime(rejectAfterTime: String) = apply { this.rejectAfterTime = rejectAfterTime }
|
||||
fun setKeepaliveTimeout(keepaliveTimeout: String) = apply { this.keepaliveTimeout = keepaliveTimeout }
|
||||
fun setMaxHandshakeAttempts(maxHandshakeAttempts: String) = apply { this.maxHandshakeAttempts = maxHandshakeAttempts }
|
||||
|
||||
override fun build(): WireguardConfig = configBuild().run { WireguardConfig(this@Builder) }
|
||||
}
|
||||
|
||||
@@ -2,9 +2,8 @@ set(CLIENT_ROOT_DIR ${CMAKE_CURRENT_LIST_DIR}/..)
|
||||
|
||||
set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/Modules;${CMAKE_MODULE_PATH}")
|
||||
|
||||
add_subdirectory(${CLIENT_ROOT_DIR}/3rd/SortFilterProxyModel)
|
||||
add_subdirectory(${CLIENT_ROOT_DIR}/3rd/SortFilterProxyModel ${CMAKE_BINARY_DIR}/3rd/SortFilterProxyModel)
|
||||
set(LIBS ${LIBS} SortFilterProxyModel)
|
||||
include(${CLIENT_ROOT_DIR}/cmake/QSimpleCrypto.cmake)
|
||||
|
||||
include(${CLIENT_ROOT_DIR}/3rd/qrcodegen/qrcodegen.cmake)
|
||||
|
||||
@@ -12,20 +11,24 @@ add_compile_definitions(_WINSOCKAPI_)
|
||||
|
||||
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
|
||||
set(BUILD_WITH_QT6 ON)
|
||||
add_subdirectory(${CLIENT_ROOT_DIR}/3rd/qtkeychain EXCLUDE_FROM_ALL)
|
||||
add_subdirectory(${CLIENT_ROOT_DIR}/3rd/qtkeychain ${CMAKE_BINARY_DIR}/3rd/qtkeychain EXCLUDE_FROM_ALL)
|
||||
|
||||
if(ANDROID)
|
||||
# Use qtgamepad from amnezia-vpn/qtgamepad repository
|
||||
# Only if Qt6CorePrivate is available (required by qtgamepad)
|
||||
find_package(Qt6CorePrivate CONFIG QUIET)
|
||||
if(Qt6CorePrivate_FOUND)
|
||||
add_subdirectory(${CLIENT_ROOT_DIR}/3rd/qtgamepad)
|
||||
add_subdirectory(${CLIENT_ROOT_DIR}/3rd/qtgamepad ${CMAKE_BINARY_DIR}/3rd/qtgamepad)
|
||||
# Link both the C++ module and QML plugin
|
||||
if(TARGET GamepadLegacy)
|
||||
if(TARGET ${PROJECT})
|
||||
target_link_libraries(${PROJECT} PRIVATE GamepadLegacy)
|
||||
else()
|
||||
list(APPEND LIBS GamepadLegacy)
|
||||
endif()
|
||||
if(TARGET GamepadLegacyQuickPrivate)
|
||||
if(TARGET ${PROJECT})
|
||||
target_link_libraries(${PROJECT} PRIVATE GamepadLegacyQuickPrivate)
|
||||
else()
|
||||
list(APPEND LIBS GamepadLegacyQuickPrivate)
|
||||
endif()
|
||||
message(STATUS "Gamepad support enabled for Android")
|
||||
else()
|
||||
@@ -36,7 +39,6 @@ endif()
|
||||
set(LIBS ${LIBS} qt6keychain)
|
||||
|
||||
include_directories(
|
||||
${CLIENT_ROOT_DIR}/3rd/QSimpleCrypto/src/include
|
||||
${CLIENT_ROOT_DIR}/3rd/qtkeychain/qtkeychain
|
||||
${CMAKE_CURRENT_BINARY_DIR}/3rd/qtkeychain
|
||||
)
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
set(CLIENT_ROOT_DIR ${CMAKE_CURRENT_LIST_DIR}/..)
|
||||
set(QSIMPLECRYPTO_DIR ${CLIENT_ROOT_DIR}/3rd/QSimpleCrypto/src)
|
||||
|
||||
include_directories(${QSIMPLECRYPTO_DIR})
|
||||
|
||||
set(HEADERS ${HEADERS}
|
||||
${QSIMPLECRYPTO_DIR}/include/QAead.h
|
||||
${QSIMPLECRYPTO_DIR}/include/QBlockCipher.h
|
||||
${QSIMPLECRYPTO_DIR}/include/QRsa.h
|
||||
${QSIMPLECRYPTO_DIR}/include/QSimpleCrypto_global.h
|
||||
${QSIMPLECRYPTO_DIR}/include/QX509.h
|
||||
${QSIMPLECRYPTO_DIR}/include/QX509Store.h
|
||||
)
|
||||
|
||||
set(SOURCES ${SOURCES}
|
||||
${QSIMPLECRYPTO_DIR}/sources/QAead.cpp
|
||||
${QSIMPLECRYPTO_DIR}/sources/QBlockCipher.cpp
|
||||
${QSIMPLECRYPTO_DIR}/sources/QRsa.cpp
|
||||
${QSIMPLECRYPTO_DIR}/sources/QX509.cpp
|
||||
${QSIMPLECRYPTO_DIR}/sources/QX509Store.cpp
|
||||
)
|
||||
@@ -121,6 +121,11 @@ target_sources(${PROJECT} PRIVATE
|
||||
${CLIENT_ROOT_DIR}/platforms/ios/StoreKit2Helper.swift
|
||||
)
|
||||
|
||||
set_source_files_properties(
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/Media.xcassets
|
||||
PROPERTIES MACOSX_PACKAGE_LOCATION Resources
|
||||
)
|
||||
|
||||
target_sources(${PROJECT} PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/AmneziaVPNLaunchScreen.storyboard
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/Media.xcassets
|
||||
|
||||
@@ -133,6 +133,11 @@ target_sources(${PROJECT} PRIVATE
|
||||
${CLIENT_ROOT_DIR}/platforms/ios/StoreKit2Helper.swift
|
||||
)
|
||||
|
||||
set_source_files_properties(
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/macos/app/Images.xcassets
|
||||
PROPERTIES MACOSX_PACKAGE_LOCATION Resources
|
||||
)
|
||||
|
||||
target_sources(${PROJECT} PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/macos/app/Images.xcassets
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/PrivacyInfo.xcprivacy
|
||||
|
||||
@@ -52,13 +52,16 @@ set(HEADERS ${HEADERS}
|
||||
${CLIENT_ROOT_DIR}/core/protocols/qmlRegisterProtocols.h
|
||||
${CLIENT_ROOT_DIR}/ui/utils/pages.h
|
||||
${CLIENT_ROOT_DIR}/ui/utils/qAutoStart.h
|
||||
${CLIENT_ROOT_DIR}/ui/utils/mtProxyPublicHostInput.h
|
||||
${CLIENT_ROOT_DIR}/core/protocols/vpnProtocol.h
|
||||
${CMAKE_CURRENT_BINARY_DIR}/version.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/selfhosted/sshClient.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/networkUtilities.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/payloadSender.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/serialization/serialization.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/serialization/transfer.h
|
||||
${CLIENT_ROOT_DIR}/../common/logger/logger.h
|
||||
${CLIENT_ROOT_DIR}/../common/crypto/cryptoUtils.h
|
||||
${CLIENT_ROOT_DIR}/ui/utils/qmlUtils.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/api/apiUtils.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/osSignalHandler.h
|
||||
@@ -127,9 +130,11 @@ set(SOURCES ${SOURCES}
|
||||
${CLIENT_ROOT_DIR}/core/repositories/secureServersRepository.cpp
|
||||
${CLIENT_ROOT_DIR}/core/repositories/secureAppSettingsRepository.cpp
|
||||
${CLIENT_ROOT_DIR}/ui/utils/qAutoStart.cpp
|
||||
${CLIENT_ROOT_DIR}/ui/utils/mtProxyPublicHostInput.cpp
|
||||
${CLIENT_ROOT_DIR}/core/protocols/vpnProtocol.cpp
|
||||
${CLIENT_ROOT_DIR}/core/utils/selfhosted/sshClient.cpp
|
||||
${CLIENT_ROOT_DIR}/core/utils/networkUtilities.cpp
|
||||
${CLIENT_ROOT_DIR}/core/utils/payloadSender.cpp
|
||||
${CLIENT_ROOT_DIR}/core/utils/serialization/outbound.cpp
|
||||
${CLIENT_ROOT_DIR}/core/utils/serialization/inbound.cpp
|
||||
${CLIENT_ROOT_DIR}/core/utils/serialization/ss.cpp
|
||||
@@ -139,6 +144,7 @@ set(SOURCES ${SOURCES}
|
||||
${CLIENT_ROOT_DIR}/core/utils/serialization/vmess.cpp
|
||||
${CLIENT_ROOT_DIR}/core/utils/serialization/vmess_new.cpp
|
||||
${CLIENT_ROOT_DIR}/../common/logger/logger.cpp
|
||||
${CLIENT_ROOT_DIR}/../common/crypto/cryptoUtils.cpp
|
||||
${CLIENT_ROOT_DIR}/ui/utils/qmlUtils.cpp
|
||||
${CLIENT_ROOT_DIR}/core/utils/api/apiUtils.cpp
|
||||
${CLIENT_ROOT_DIR}/core/utils/serverConfigUtils.cpp
|
||||
@@ -201,11 +207,6 @@ file(GLOB CONFIGURATORS_CPP CONFIGURE_DEPENDS ${CLIENT_ROOT_DIR}/core/configurat
|
||||
file(GLOB_RECURSE CORE_MODELS_H CONFIGURE_DEPENDS ${CLIENT_ROOT_DIR}/core/models/*.h)
|
||||
file(GLOB_RECURSE CORE_MODELS_CPP CONFIGURE_DEPENDS ${CLIENT_ROOT_DIR}/core/models/*.cpp)
|
||||
|
||||
if(NOT ANDROID AND NOT IOS)
|
||||
file(GLOB LOCAL_PROXY_H CONFIGURE_DEPENDS ${CLIENT_ROOT_DIR}/core/local-proxy/*.h)
|
||||
file(GLOB LOCAL_PROXY_CPP CONFIGURE_DEPENDS ${CLIENT_ROOT_DIR}/core/local-proxy/*.cpp)
|
||||
endif()
|
||||
|
||||
file(GLOB UI_MODELS_H CONFIGURE_DEPENDS
|
||||
${CLIENT_ROOT_DIR}/ui/models/*.h
|
||||
${CLIENT_ROOT_DIR}/ui/models/protocols/*.h
|
||||
@@ -251,11 +252,6 @@ set(SOURCES ${SOURCES}
|
||||
${UI_CONTROLLERS_CPP}
|
||||
)
|
||||
|
||||
if(NOT ANDROID AND NOT IOS)
|
||||
list(APPEND HEADERS ${LOCAL_PROXY_H})
|
||||
list(APPEND SOURCES ${LOCAL_PROXY_CPP})
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
set(HEADERS ${HEADERS}
|
||||
${CLIENT_ROOT_DIR}/core/protocols/ikev2VpnProtocolWindows.h
|
||||
|
||||
@@ -266,7 +266,7 @@ ProtocolConfig WireguardConfigurator::createConfig(const ServerCredentials &cred
|
||||
clientConfig.presharedKey = connData.pskKey;
|
||||
clientConfig.clientId = connData.clientPubKey;
|
||||
clientConfig.allowedIps = QStringList { "0.0.0.0/0", "::/0" };
|
||||
clientConfig.persistentKeepAlive = "25";
|
||||
clientConfig.persistentKeepAlive = protocols::wireguard::defaultPersistentKeepAlive;
|
||||
clientConfig.mtu = mtu;
|
||||
clientConfig.isObfuscationEnabled = false;
|
||||
|
||||
|
||||
@@ -78,7 +78,8 @@ QFuture<QPair<ErrorCode, QJsonArray>> NewsController::fetchNews()
|
||||
m_appSettingsRepository->getGatewayEndpoint(),
|
||||
m_appSettingsRepository->isDevGatewayEnv(),
|
||||
apiDefs::requestTimeoutMsecs,
|
||||
m_appSettingsRepository->isStrictKillSwitchEnabled());
|
||||
m_appSettingsRepository->isStrictKillSwitchEnabled(),
|
||||
m_appSettingsRepository);
|
||||
|
||||
QJsonObject payload;
|
||||
payload.insert("locale", m_appSettingsRepository->getAppLanguage().name().split("_").first());
|
||||
|
||||
@@ -242,7 +242,7 @@ ErrorCode ServicesCatalogController::fillAvailableServices(QJsonObject &services
|
||||
ErrorCode ServicesCatalogController::executeRequest(const QString &endpoint, const QJsonObject &apiPayload, QByteArray &responseBody)
|
||||
{
|
||||
GatewayController gatewayController(m_appSettingsRepository->getGatewayEndpoint(), m_appSettingsRepository->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs,
|
||||
m_appSettingsRepository->isStrictKillSwitchEnabled());
|
||||
m_appSettingsRepository->isStrictKillSwitchEnabled(), m_appSettingsRepository);
|
||||
return gatewayController.post(endpoint, apiPayload, responseBody);
|
||||
}
|
||||
|
||||
|
||||
@@ -56,6 +56,38 @@ QString getSubscriptionStatusForRenewal(const ApiConfig &apiConfig)
|
||||
|
||||
return QStringLiteral("active");
|
||||
}
|
||||
|
||||
QString normalizeCaptchaSolution(const QString &captchaSolution)
|
||||
{
|
||||
QString normalizedSolution;
|
||||
normalizedSolution.reserve(captchaSolution.size());
|
||||
for (const QChar &ch : captchaSolution) {
|
||||
const ushort u = ch.unicode();
|
||||
if (u >= '0' && u <= '9') {
|
||||
normalizedSolution += ch;
|
||||
} else if (u >= 0xFF10 && u <= 0xFF19) {
|
||||
normalizedSolution += QChar(static_cast<char16_t>(u - 0xFF10 + '0'));
|
||||
}
|
||||
}
|
||||
return normalizedSolution.isEmpty() ? captchaSolution.trimmed() : normalizedSolution;
|
||||
}
|
||||
|
||||
bool fillCaptchaInfoFromResponse(const QByteArray &responseBody, SubscriptionController::CaptchaInfo &captchaInfo)
|
||||
{
|
||||
const QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
|
||||
if (!jsonDoc.isObject()) {
|
||||
return false;
|
||||
}
|
||||
const QJsonObject jsonObj = jsonDoc.object();
|
||||
if (!jsonObj.contains(QStringLiteral("captcha_id")) || !jsonObj.contains(QStringLiteral("captcha_image"))) {
|
||||
return false;
|
||||
}
|
||||
captchaInfo.captchaId = jsonObj.value(QStringLiteral("captcha_id")).toString();
|
||||
captchaInfo.captchaImageBase64 = jsonObj.value(QStringLiteral("captcha_image")).toString();
|
||||
captchaInfo.hint = jsonObj.value(QStringLiteral("hint")).toString();
|
||||
captchaInfo.isRequired = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -173,6 +205,7 @@ ErrorCode SubscriptionController::extractServerConfigJsonFromResponse(const QByt
|
||||
serverProtocolConfig[configKey::specialJunk3] = clientProtocolConfig.value(configKey::specialJunk3);
|
||||
serverProtocolConfig[configKey::specialJunk4] = clientProtocolConfig.value(configKey::specialJunk4);
|
||||
serverProtocolConfig[configKey::specialJunk5] = clientProtocolConfig.value(configKey::specialJunk5);
|
||||
serverProtocolConfig[configKey::headerProtectionKey] = clientProtocolConfig.value(configKey::headerProtectionKey);
|
||||
|
||||
//
|
||||
|
||||
@@ -198,9 +231,6 @@ void SubscriptionController::updateApiConfigInJson(QJsonObject &serverConfigJson
|
||||
|
||||
if (serverConfigJson.value(configKey::configVersion).toInt() == serverConfigUtils::ConfigSource::AmneziaGateway) {
|
||||
QJsonObject responseObj = QJsonDocument::fromJson(apiResponseBody).object();
|
||||
if (responseObj.contains(apiDefs::key::supportedProtocols)) {
|
||||
apiConfig.insert(apiDefs::key::supportedProtocols, responseObj.value(apiDefs::key::supportedProtocols).toArray());
|
||||
}
|
||||
if (responseObj.contains(apiDefs::key::serviceInfo)) {
|
||||
apiConfig.insert(apiDefs::key::serviceInfo, responseObj.value(apiDefs::key::serviceInfo).toObject());
|
||||
}
|
||||
@@ -212,7 +242,7 @@ void SubscriptionController::updateApiConfigInJson(QJsonObject &serverConfigJson
|
||||
ErrorCode SubscriptionController::executeRequest(const QString &endpoint, const QJsonObject &apiPayload, QByteArray &responseBody, bool isTestPurchase)
|
||||
{
|
||||
GatewayController gatewayController(m_appSettingsRepository->getGatewayEndpoint(isTestPurchase), m_appSettingsRepository->isDevGatewayEnv(isTestPurchase), apiDefs::requestTimeoutMsecs,
|
||||
m_appSettingsRepository->isStrictKillSwitchEnabled());
|
||||
m_appSettingsRepository->isStrictKillSwitchEnabled(), m_appSettingsRepository);
|
||||
return gatewayController.post(endpoint, apiPayload, responseBody);
|
||||
}
|
||||
|
||||
@@ -237,14 +267,7 @@ ErrorCode SubscriptionController::importServiceFromGateway(const QString &userCo
|
||||
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody);
|
||||
|
||||
if (errorCode == ErrorCode::ApiCaptchaRequiredError) {
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
|
||||
if (jsonDoc.isObject()) {
|
||||
QJsonObject jsonObj = jsonDoc.object();
|
||||
captchaInfo.captchaId = jsonObj.value("captcha_id").toString();
|
||||
captchaInfo.captchaImageBase64 = jsonObj.value("captcha_image").toString();
|
||||
captchaInfo.hint = jsonObj.value("hint").toString();
|
||||
captchaInfo.isRequired = true;
|
||||
}
|
||||
fillCaptchaInfoFromResponse(responseBody, captchaInfo);
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
@@ -410,7 +433,8 @@ ErrorCode SubscriptionController::importServiceFromAppStore(const QString &userC
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode SubscriptionController::updateServiceFromGateway(const QString &serverId, const QString &newCountryCode, bool isConnectEvent)
|
||||
ErrorCode SubscriptionController::updateServiceFromGateway(const QString &serverId, const QString &newCountryCode, bool isConnectEvent,
|
||||
CaptchaInfo *captchaInfoOut, ProtocolData *usedProtocolDataOut)
|
||||
{
|
||||
auto apiV2 = m_serversRepository->apiV2Config(serverId);
|
||||
if (!apiV2.has_value()) {
|
||||
@@ -418,8 +442,25 @@ ErrorCode SubscriptionController::updateServiceFromGateway(const QString &server
|
||||
}
|
||||
const bool isTestPurchase = apiV2->apiConfig.isTestPurchase;
|
||||
QString serviceProtocol = apiV2->serviceProtocol();
|
||||
|
||||
if (!newCountryCode.isEmpty()) {
|
||||
const auto availableCountries = apiV2->apiConfig.availableCountries;
|
||||
for (const auto &country : availableCountries) {
|
||||
const auto countryObject = country.toObject();
|
||||
if (countryObject.value(apiDefs::key::serverCountryCode).toString() != newCountryCode) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto availableProtocols = countryObject.value(apiDefs::key::availableProtocols).toArray();
|
||||
if (!availableProtocols.isEmpty() && !availableProtocols.contains(serviceProtocol)) {
|
||||
serviceProtocol = availableProtocols.first().toString();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ProtocolData protocolData = generateProtocolData(serviceProtocol);
|
||||
|
||||
|
||||
QJsonObject authDataJson = apiV2->authData.toJson();
|
||||
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
|
||||
QString(APP_VERSION),
|
||||
@@ -441,6 +482,11 @@ ErrorCode SubscriptionController::updateServiceFromGateway(const QString &server
|
||||
QByteArray responseBody;
|
||||
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody, isTestPurchase);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
if (errorCode == ErrorCode::ApiCaptchaRequiredError && captchaInfoOut) {
|
||||
if (fillCaptchaInfoFromResponse(responseBody, *captchaInfoOut) && usedProtocolDataOut) {
|
||||
*usedProtocolDataOut = protocolData;
|
||||
}
|
||||
}
|
||||
if (errorCode == ErrorCode::ApiSubscriptionExpiredError && !apiV2->apiConfig.isInAppPurchase) {
|
||||
ApiV2ServerConfig expiredApiV2 = *apiV2;
|
||||
expiredApiV2.apiConfig.subscriptionExpiredByServer = true;
|
||||
@@ -450,29 +496,40 @@ ErrorCode SubscriptionController::updateServiceFromGateway(const QString &server
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
return applyUpdatedServiceConfig(serverId, serviceProtocol, protocolData, responseBody);
|
||||
}
|
||||
|
||||
ErrorCode SubscriptionController::applyUpdatedServiceConfig(const QString &serverId, const QString &serviceProtocol,
|
||||
const ProtocolData &protocolData, const QByteArray &responseBody)
|
||||
{
|
||||
auto apiV2 = m_serversRepository->apiV2Config(serverId);
|
||||
if (!apiV2.has_value()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
QJsonObject serverConfigJson;
|
||||
errorCode = extractServerConfigJsonFromResponse(responseBody, serviceProtocol, protocolData, serverConfigJson);
|
||||
ErrorCode errorCode = extractServerConfigJsonFromResponse(responseBody, serviceProtocol, protocolData, serverConfigJson);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
|
||||
updateApiConfigInJson(serverConfigJson, apiV2->apiConfig.serviceType, serviceProtocol, apiV2->apiConfig.userCountryCode, responseBody);
|
||||
|
||||
|
||||
if (serverConfigJson.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
ApiV2ServerConfig newApiV2Config = ApiV2ServerConfig::fromJson(serverConfigJson);
|
||||
ApiV2ServerConfig* newApiV2 = &newApiV2Config;
|
||||
|
||||
|
||||
newApiV2->apiConfig.vpnKey = apiV2->apiConfig.vpnKey;
|
||||
newApiV2->apiConfig.isTestPurchase = apiV2->apiConfig.isTestPurchase;
|
||||
newApiV2->apiConfig.isInAppPurchase = apiV2->apiConfig.isInAppPurchase;
|
||||
newApiV2->apiConfig.subscriptionExpiredByServer = false;
|
||||
|
||||
|
||||
newApiV2->authData = apiV2->authData;
|
||||
newApiV2->crc = apiV2->crc;
|
||||
|
||||
|
||||
if (apiV2->nameOverriddenByUser) {
|
||||
newApiV2->name = apiV2->name;
|
||||
newApiV2->displayName = apiV2->displayName;
|
||||
@@ -484,6 +541,59 @@ ErrorCode SubscriptionController::updateServiceFromGateway(const QString &server
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode SubscriptionController::resolveUpdateServiceCaptcha(const QString &serverId, const QString &newCountryCode,
|
||||
bool isConnectEvent, const ProtocolData &protocolData,
|
||||
const QString &captchaId, const QString &captchaSolution,
|
||||
CaptchaInfo *retryCaptchaOut)
|
||||
{
|
||||
auto apiV2 = m_serversRepository->apiV2Config(serverId);
|
||||
if (!apiV2.has_value()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
const bool isTestPurchase = apiV2->apiConfig.isTestPurchase;
|
||||
QString serviceProtocol = apiV2->serviceProtocol();
|
||||
|
||||
QJsonObject authDataJson = apiV2->authData.toJson();
|
||||
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
|
||||
QString(APP_VERSION),
|
||||
m_appSettingsRepository->getAppLanguage().name().split("_").first(),
|
||||
m_appSettingsRepository->getInstallationUuid(true),
|
||||
apiV2->apiConfig.userCountryCode,
|
||||
newCountryCode,
|
||||
apiV2->serviceType(),
|
||||
serviceProtocol,
|
||||
authDataJson };
|
||||
|
||||
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
|
||||
appendProtocolDataToApiPayload(serviceProtocol, protocolData, apiPayload);
|
||||
|
||||
if (isConnectEvent) {
|
||||
apiPayload[apiDefs::key::isConnectEvent] = true;
|
||||
}
|
||||
|
||||
apiPayload["captcha_id"] = captchaId;
|
||||
apiPayload["captcha_solution"] = normalizeCaptchaSolution(captchaSolution);
|
||||
|
||||
QByteArray responseBody;
|
||||
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody, isTestPurchase);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
if (retryCaptchaOut
|
||||
&& (errorCode == ErrorCode::ApiCaptchaInvalidError || errorCode == ErrorCode::ApiCaptchaRefreshError
|
||||
|| errorCode == ErrorCode::ApiCaptchaRequiredError)) {
|
||||
fillCaptchaInfoFromResponse(responseBody, *retryCaptchaOut);
|
||||
}
|
||||
if (errorCode == ErrorCode::ApiSubscriptionExpiredError && !apiV2->apiConfig.isInAppPurchase) {
|
||||
ApiV2ServerConfig expiredApiV2 = *apiV2;
|
||||
expiredApiV2.apiConfig.subscriptionExpiredByServer = true;
|
||||
m_serversRepository->editServer(serverId, expiredApiV2.toJson(),
|
||||
serverConfigUtils::configTypeFromJson(expiredApiV2.toJson()));
|
||||
}
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
return applyUpdatedServiceConfig(serverId, serviceProtocol, protocolData, responseBody);
|
||||
}
|
||||
|
||||
ErrorCode SubscriptionController::deactivateDevice(const QString &serverId)
|
||||
{
|
||||
auto apiV2 = m_serversRepository->apiV2Config(serverId);
|
||||
@@ -648,19 +758,20 @@ ErrorCode SubscriptionController::prepareVpnKeyExport(const QString &serverId, Q
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode SubscriptionController::validateAndUpdateConfig(const QString &serverId, bool hasInstalledContainers)
|
||||
ErrorCode SubscriptionController::validateAndUpdateConfig(const QString &serverId, bool hasInstalledContainers,
|
||||
CaptchaInfo *captchaInfoOut, ProtocolData *usedProtocolDataOut)
|
||||
{
|
||||
if (!m_serversRepository->apiV2Config(serverId).has_value()) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
if (!hasInstalledContainers) {
|
||||
return updateServiceFromGateway(serverId, "", true);
|
||||
return updateServiceFromGateway(serverId, "", true, captchaInfoOut, usedProtocolDataOut);
|
||||
}
|
||||
|
||||
if (isApiKeyExpired(serverId)) {
|
||||
qDebug() << "attempt to update api config by expires_at event";
|
||||
return updateServiceFromGateway(serverId, "", true);
|
||||
return updateServiceFromGateway(serverId, "", true, captchaInfoOut, usedProtocolDataOut);
|
||||
}
|
||||
|
||||
return ErrorCode::NoError;
|
||||
@@ -752,6 +863,36 @@ bool SubscriptionController::isVlessProtocol(const QString &serverId) const
|
||||
return apiV2.has_value() && apiV2->serviceProtocol() == "vless";
|
||||
}
|
||||
|
||||
QString SubscriptionController::currentProtocol(const QString &serverId) const
|
||||
{
|
||||
auto apiV2 = m_serversRepository->apiV2Config(serverId);
|
||||
return apiV2.has_value() ? apiV2->serviceProtocol() : QString();
|
||||
}
|
||||
|
||||
QStringList SubscriptionController::availableProtocols(const QString &serverId) const
|
||||
{
|
||||
auto apiV2 = m_serversRepository->apiV2Config(serverId);
|
||||
if (!apiV2.has_value()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto currentCountryCode = apiV2->apiConfig.serverCountryCode;
|
||||
const auto availableCountries = apiV2->apiConfig.availableCountries;
|
||||
|
||||
QStringList protocols;
|
||||
for (const auto &country : availableCountries) {
|
||||
const auto countryObject = country.toObject();
|
||||
if (countryObject.value(apiDefs::key::serverCountryCode).toString() != currentCountryCode) {
|
||||
continue;
|
||||
}
|
||||
for (const auto &protocol : countryObject.value(apiDefs::key::availableProtocols).toArray()) {
|
||||
protocols.push_back(protocol.toString());
|
||||
}
|
||||
break;
|
||||
}
|
||||
return protocols;
|
||||
}
|
||||
|
||||
ErrorCode SubscriptionController::processAppStorePurchase(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const QString &productId,
|
||||
int *duplicateServerIndex)
|
||||
@@ -949,7 +1090,8 @@ QFuture<QPair<ErrorCode, QString>> SubscriptionController::getRenewalLink(const
|
||||
auto gatewayController = QSharedPointer<GatewayController>::create(m_appSettingsRepository->getGatewayEndpoint(isTestPurchase),
|
||||
m_appSettingsRepository->isDevGatewayEnv(isTestPurchase),
|
||||
apiDefs::requestTimeoutMsecs,
|
||||
m_appSettingsRepository->isStrictKillSwitchEnabled());
|
||||
m_appSettingsRepository->isStrictKillSwitchEnabled(),
|
||||
m_appSettingsRepository);
|
||||
auto postFuture = gatewayController->postAsync(QString("%1v1/renewal_link"), apiPayload);
|
||||
auto *watcher = new QFutureWatcher<QPair<ErrorCode, QByteArray>>();
|
||||
QObject::connect(watcher, &QFutureWatcher<QPair<ErrorCode, QByteArray>>::finished,
|
||||
@@ -993,17 +1135,7 @@ ErrorCode SubscriptionController::resolveImportServiceCaptcha(const QString &use
|
||||
appendProtocolDataToApiPayload(serviceProtocol, protocolData, apiPayload);
|
||||
|
||||
apiPayload["captcha_id"] = captchaId;
|
||||
QString normalizedSolution;
|
||||
normalizedSolution.reserve(captchaSolution.size());
|
||||
for (const QChar &ch : captchaSolution) {
|
||||
const ushort u = ch.unicode();
|
||||
if (u >= '0' && u <= '9') {
|
||||
normalizedSolution += ch;
|
||||
} else if (u >= 0xFF10 && u <= 0xFF19) {
|
||||
normalizedSolution += QChar(static_cast<char16_t>(u - 0xFF10 + '0'));
|
||||
}
|
||||
}
|
||||
apiPayload["captcha_solution"] = normalizedSolution.isEmpty() ? captchaSolution.trimmed() : normalizedSolution;
|
||||
apiPayload["captcha_solution"] = normalizeCaptchaSolution(captchaSolution);
|
||||
|
||||
QByteArray responseBody;
|
||||
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody);
|
||||
@@ -1011,16 +1143,7 @@ ErrorCode SubscriptionController::resolveImportServiceCaptcha(const QString &use
|
||||
if (retryCaptchaOut
|
||||
&& (errorCode == ErrorCode::ApiCaptchaInvalidError || errorCode == ErrorCode::ApiCaptchaRefreshError
|
||||
|| errorCode == ErrorCode::ApiCaptchaRequiredError)) {
|
||||
const QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
|
||||
if (jsonDoc.isObject()) {
|
||||
const QJsonObject jsonObj = jsonDoc.object();
|
||||
if (jsonObj.contains(QStringLiteral("captcha_id")) && jsonObj.contains(QStringLiteral("captcha_image"))) {
|
||||
retryCaptchaOut->captchaId = jsonObj.value(QStringLiteral("captcha_id")).toString();
|
||||
retryCaptchaOut->captchaImageBase64 = jsonObj.value(QStringLiteral("captcha_image")).toString();
|
||||
retryCaptchaOut->hint = jsonObj.value(QStringLiteral("hint")).toString();
|
||||
retryCaptchaOut->isRequired = true;
|
||||
}
|
||||
}
|
||||
fillCaptchaInfoFromResponse(responseBody, *retryCaptchaOut);
|
||||
}
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
@@ -66,7 +66,12 @@ public:
|
||||
const QString &transactionId, bool isTestPurchase,
|
||||
int *duplicateServerIndex = nullptr);
|
||||
|
||||
ErrorCode updateServiceFromGateway(const QString &serverId, const QString &newCountryCode, bool isConnectEvent);
|
||||
ErrorCode updateServiceFromGateway(const QString &serverId, const QString &newCountryCode, bool isConnectEvent,
|
||||
CaptchaInfo *captchaInfoOut = nullptr, ProtocolData *usedProtocolDataOut = nullptr);
|
||||
|
||||
ErrorCode resolveUpdateServiceCaptcha(const QString &serverId, const QString &newCountryCode, bool isConnectEvent,
|
||||
const ProtocolData &protocolData, const QString &captchaId,
|
||||
const QString &captchaSolution, CaptchaInfo *retryCaptchaOut = nullptr);
|
||||
|
||||
ErrorCode deactivateDevice(const QString &serverId);
|
||||
|
||||
@@ -78,7 +83,8 @@ public:
|
||||
|
||||
ErrorCode prepareVpnKeyExport(const QString &serverId, QString &vpnKey);
|
||||
|
||||
ErrorCode validateAndUpdateConfig(const QString &serverId, bool hasInstalledContainers);
|
||||
ErrorCode validateAndUpdateConfig(const QString &serverId, bool hasInstalledContainers,
|
||||
CaptchaInfo *captchaInfoOut = nullptr, ProtocolData *usedProtocolDataOut = nullptr);
|
||||
|
||||
void removeApiConfig(const QString &serverId);
|
||||
|
||||
@@ -86,6 +92,8 @@ public:
|
||||
|
||||
void setCurrentProtocol(const QString &serverId, const QString &protocolName);
|
||||
bool isVlessProtocol(const QString &serverId) const;
|
||||
QString currentProtocol(const QString &serverId) const;
|
||||
QStringList availableProtocols(const QString &serverId) const;
|
||||
|
||||
ErrorCode getAccountInfo(const QString &serverId, QJsonObject &accountInfo);
|
||||
QFuture<QPair<ErrorCode, QString>> getRenewalLink(const QString &serverId);
|
||||
@@ -115,8 +123,10 @@ private:
|
||||
ErrorCode executeRequest(const QString &endpoint, const QJsonObject &apiPayload, QByteArray &responseBody, bool isTestPurchase = false);
|
||||
bool isApiKeyExpired(const QString &serverId) const;
|
||||
|
||||
ErrorCode extractServerConfigJsonFromResponse(const QByteArray &apiResponseBody, const QString &protocol,
|
||||
ErrorCode extractServerConfigJsonFromResponse(const QByteArray &apiResponseBody, const QString &protocol,
|
||||
const ProtocolData &protocolData, QJsonObject &serverConfigJson);
|
||||
ErrorCode applyUpdatedServiceConfig(const QString &serverId, const QString &serviceProtocol,
|
||||
const ProtocolData &protocolData, const QByteArray &responseBody);
|
||||
void updateApiConfigInJson(QJsonObject &serverConfigJson, const QString &serviceType,
|
||||
const QString &serviceProtocol, const QString &userCountryCode,
|
||||
const QByteArray &apiResponseBody);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "core/utils/protocolEnum.h"
|
||||
#include "core/protocols/protocolUtils.h"
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/payloadSender.h"
|
||||
#include "core/utils/utilities.h"
|
||||
#include "core/utils/serverConfigUtils.h"
|
||||
#include "version.h"
|
||||
@@ -30,7 +31,6 @@ ConnectionController::ConnectionController(SecureServersRepository* serversRepos
|
||||
connect(m_vpnConnection, &VpnConnection::connectionStateChanged, this, &ConnectionController::connectionStateChanged);
|
||||
connect(this, &ConnectionController::openConnectionRequested, m_vpnConnection, &VpnConnection::connectToVpn, Qt::QueuedConnection);
|
||||
connect(this, &ConnectionController::closeConnectionRequested, m_vpnConnection, &VpnConnection::disconnectFromVpn, Qt::QueuedConnection);
|
||||
connect(this, &ConnectionController::setConnectionStateRequested, m_vpnConnection, &VpnConnection::setConnectionState, Qt::QueuedConnection);
|
||||
connect(this, &ConnectionController::killSwitchModeChangedRequested, m_vpnConnection, &VpnConnection::onKillSwitchModeChanged, Qt::QueuedConnection);
|
||||
#ifdef Q_OS_ANDROID
|
||||
connect(this, &ConnectionController::restoreConnectionRequested, m_vpnConnection, &VpnConnection::restoreConnection, Qt::QueuedConnection);
|
||||
@@ -44,9 +44,7 @@ bool ConnectionController::isConnected() const
|
||||
|
||||
void ConnectionController::setConnectionState(Vpn::ConnectionState state)
|
||||
{
|
||||
if (m_vpnConnection) {
|
||||
emit setConnectionStateRequested(state);
|
||||
}
|
||||
emit connectionStateChanged(state);
|
||||
}
|
||||
|
||||
ErrorCode ConnectionController::defaultContainerForServer(const QString &serverId, DockerContainer &container) const
|
||||
@@ -218,12 +216,10 @@ ErrorCode ConnectionController::openConnection(const QString &serverId)
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
if (m_appSettingsRepository && m_appSettingsRepository->isLocalProxyHttpEnabled()) {
|
||||
m_appSettingsRepository->setLocalProxyHttpEnabled(false);
|
||||
emit localProxyStoppedBecauseVpnTurnedOn(tr("Local proxy stopped because VPN was turned on"));
|
||||
const auto apiV2 = m_serversRepository->apiV2Config(serverId);
|
||||
if (apiV2.has_value() && !apiV2->sendPayload.isEmpty()) {
|
||||
PayloadSender::sendAll(apiV2->sendPayload);
|
||||
}
|
||||
#endif
|
||||
|
||||
emit openConnectionRequested(serverId, container, vpnConfiguration);
|
||||
return ErrorCode::NoError;
|
||||
|
||||
@@ -67,9 +67,7 @@ signals:
|
||||
void connectionStateChanged(Vpn::ConnectionState state);
|
||||
void openConnectionRequested(const QString &serverId, DockerContainer container, const QJsonObject &vpnConfiguration);
|
||||
void closeConnectionRequested();
|
||||
void setConnectionStateRequested(Vpn::ConnectionState state);
|
||||
void killSwitchModeChangedRequested(bool enabled);
|
||||
void localProxyStoppedBecauseVpnTurnedOn(const QString &message);
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
void restoreConnectionRequested();
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
#include "coreController.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDirIterator>
|
||||
#include <QDebug>
|
||||
#include <QTranslator>
|
||||
#include <QStandardPaths>
|
||||
#include <QTimer>
|
||||
|
||||
#include "core/utils/selfhosted/sshSession.h"
|
||||
@@ -25,7 +22,8 @@
|
||||
#endif
|
||||
|
||||
CoreController::CoreController(const QSharedPointer<VpnConnection> &vpnConnection, SecureQSettings* settings,
|
||||
QQmlApplicationEngine *engine, QObject *parent)
|
||||
QQmlApplicationEngine *engine, QObject *parent,
|
||||
bool skipPlatformControllerInit)
|
||||
: QObject(parent), m_vpnConnection(vpnConnection), m_settings(settings), m_engine(engine)
|
||||
{
|
||||
initRepositories();
|
||||
@@ -34,14 +32,12 @@ CoreController::CoreController(const QSharedPointer<VpnConnection> &vpnConnectio
|
||||
initControllers();
|
||||
initSignalHandlers();
|
||||
|
||||
initAndroidController();
|
||||
initAppleController();
|
||||
if (!skipPlatformControllerInit) {
|
||||
initAndroidController();
|
||||
initAppleController();
|
||||
}
|
||||
initLogging();
|
||||
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
initLocalProxy();
|
||||
#endif
|
||||
|
||||
m_translator = new QTranslator(this);
|
||||
if (m_appSettingsRepository) {
|
||||
updateTranslator(m_appSettingsRepository->getAppLanguage());
|
||||
@@ -55,69 +51,6 @@ void CoreController::setQmlContextProperty(const QString &name, QObject *value)
|
||||
}
|
||||
}
|
||||
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
void CoreController::initLocalProxy()
|
||||
{
|
||||
constexpr quint16 kLocalProxyApiPort = 49490;
|
||||
|
||||
m_proxyServer.reset(new ProxyServer(m_serversRepository, m_appSettingsRepository, this));
|
||||
|
||||
QObject::connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, [this]() {
|
||||
if (m_appSettingsRepository && m_appSettingsRepository->isLocalProxyHttpEnabled()) {
|
||||
m_appSettingsRepository->setLocalProxyHttpEnabled(false);
|
||||
}
|
||||
});
|
||||
|
||||
auto syncLocalProxy = [this]() {
|
||||
if (!m_proxyServer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool httpEnabled = m_appSettingsRepository->isLocalProxyHttpEnabled();
|
||||
|
||||
if (!httpEnabled) {
|
||||
qInfo() << "Local proxy: HTTP API disabled";
|
||||
m_proxyServer->stop();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!m_proxyServer->start(kLocalProxyApiPort)) {
|
||||
qWarning() << "Local proxy: failed to start on port" << kLocalProxyApiPort;
|
||||
m_appSettingsRepository->setLocalProxyHttpEnabled(false);
|
||||
emit m_appSettingsRepository->localProxyStartFailed(tr("Local proxy failed to start. Check if the port is available."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!m_proxyServer->syncSettings()) {
|
||||
qWarning() << "Local proxy: failed to start proxy core (Xray)";
|
||||
m_appSettingsRepository->setLocalProxyHttpEnabled(false);
|
||||
emit m_appSettingsRepository->localProxyStartFailed(tr("Couldn’t start the proxy due to an internal error. Try restarting the app."));
|
||||
return;
|
||||
}
|
||||
|
||||
qInfo() << "Local proxy: running on 127.0.0.1:" << kLocalProxyApiPort;
|
||||
};
|
||||
|
||||
syncLocalProxy();
|
||||
|
||||
connect(m_appSettingsRepository, &SecureAppSettingsRepository::localProxySettingsChanged, this, syncLocalProxy);
|
||||
|
||||
connect(m_serversRepository, &SecureServersRepository::serverEdited, this, [this](const QString &serverId) {
|
||||
if (m_appSettingsRepository && m_appSettingsRepository->isLocalProxyHttpEnabled()
|
||||
&& m_appSettingsRepository->localProxyOwnerId() == serverId) {
|
||||
m_appSettingsRepository->bumpLocalProxyRestartToken();
|
||||
}
|
||||
});
|
||||
|
||||
connect(m_serversRepository, &SecureServersRepository::serverRemoved, this, [this](const QString &serverId, int) {
|
||||
if (m_appSettingsRepository && m_appSettingsRepository->localProxyOwnerId() == serverId) {
|
||||
m_appSettingsRepository->setLocalProxyOwnerId(QString());
|
||||
m_appSettingsRepository->setLocalProxyHttpEnabled(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
#endif
|
||||
|
||||
void CoreController::initModels()
|
||||
{
|
||||
m_containersModel = new ContainersModel(this);
|
||||
@@ -267,11 +200,6 @@ void CoreController::initControllers()
|
||||
m_pageController = new PageController(m_serversController, m_settingsController, this);
|
||||
setQmlContextProperty("PageController", m_pageController);
|
||||
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
connect(m_connectionController, &ConnectionController::localProxyStoppedBecauseVpnTurnedOn, m_pageController,
|
||||
&PageController::showNotificationMessage);
|
||||
#endif
|
||||
|
||||
m_serversUiController = new ServersUiController(m_serversController, m_settingsController, m_serversModel, m_containersModel, m_defaultServerContainersModel, this);
|
||||
setQmlContextProperty("ServersUiController", m_serversUiController);
|
||||
|
||||
@@ -357,6 +285,10 @@ void CoreController::initSignalHandlers()
|
||||
if (m_serversUiController->hasServersFromGatewayApi()) {
|
||||
m_apiNewsUiController->fetchNews(false);
|
||||
}
|
||||
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
m_updateController->checkForUpdates();
|
||||
#endif
|
||||
}
|
||||
|
||||
void CoreController::updateTranslator(const QLocale &locale)
|
||||
|
||||
@@ -78,7 +78,6 @@
|
||||
#include "ui/models/newsModel.h"
|
||||
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
#include "core/local-proxy/proxyserver.h"
|
||||
#include "ui/utils/notificationHandler.h"
|
||||
#endif
|
||||
|
||||
@@ -91,7 +90,8 @@ class CoreController : public QObject
|
||||
|
||||
public:
|
||||
explicit CoreController(const QSharedPointer<VpnConnection> &vpnConnection, SecureQSettings* settings,
|
||||
QQmlApplicationEngine *engine, QObject *parent = nullptr);
|
||||
QQmlApplicationEngine *engine, QObject *parent = nullptr,
|
||||
bool skipPlatformControllerInit = false);
|
||||
|
||||
PageController* pageController() const;
|
||||
void setQmlRoot();
|
||||
@@ -115,7 +115,7 @@ protected:
|
||||
AppSplitTunnelingModel* appSplitTunnelingModelProtected() const { return m_appSplitTunnelingModel; }
|
||||
IpSplitTunnelingModel* ipSplitTunnelingModelProtected() const { return m_ipSplitTunnelingModel; }
|
||||
LanguageModel* languageModelProtected() const { return m_languageModel; }
|
||||
|
||||
ConnectionUiController* connectionUiControllerProtected() const { return m_connectionUiController; }
|
||||
InstallUiController* installUiControllerProtected() const { return m_installUiController; }
|
||||
ImportController* importCoreControllerProtected() const { return m_importCoreController; }
|
||||
ExportController* exportControllerProtected() const { return m_exportController; }
|
||||
@@ -143,9 +143,6 @@ private:
|
||||
void initAppleController();
|
||||
void initLogging();
|
||||
void initSignalHandlers();
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
void initLocalProxy();
|
||||
#endif
|
||||
void setQmlContextProperty(const QString &name, QObject *value);
|
||||
|
||||
QQmlApplicationEngine *m_engine {}; // TODO use parent child system here?
|
||||
@@ -231,10 +228,6 @@ private:
|
||||
TelemtConfigModel* m_telemtConfigModel;
|
||||
|
||||
CoreSignalHandlers* m_signalHandlers;
|
||||
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
QScopedPointer<ProxyServer> m_proxyServer;
|
||||
#endif
|
||||
};
|
||||
|
||||
#endif // CORECONTROLLER_H
|
||||
|
||||
@@ -205,10 +205,8 @@ void CoreSignalHandlers::initAdminConfigRevokedHandler()
|
||||
{
|
||||
connect(m_coreController->m_installController, &InstallController::clientRevocationRequested, this,
|
||||
[this](const QString &serverId, const ContainerConfig &containerConfig, DockerContainer container) {
|
||||
QtConcurrent::run([this, serverId, containerConfig, container]() {
|
||||
m_coreController->m_usersController->revokeClient(serverId, containerConfig, container);
|
||||
});
|
||||
});
|
||||
m_coreController->m_usersController->revokeClient(serverId, containerConfig, container);
|
||||
}, Qt::DirectConnection);
|
||||
|
||||
connect(m_coreController->m_installController, &InstallController::clientAppendRequested, this,
|
||||
[this](const QString &serverId, const QString &clientId, const QString &clientName, DockerContainer container) {
|
||||
@@ -442,9 +440,6 @@ void CoreSignalHandlers::initNotificationHandler()
|
||||
void CoreSignalHandlers::initUpdateFoundHandler()
|
||||
{
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
connect(m_coreController->m_apiNewsUiController, &ApiNewsUiController::fetchNewsFinished, m_coreController->m_updateUiController,
|
||||
&UpdateUiController::checkForUpdates);
|
||||
|
||||
connect(m_coreController->m_updateUiController, &UpdateUiController::updateFound, this, [this]() {
|
||||
const QString version = m_coreController->m_updateUiController->getVersion();
|
||||
const QString updateId = version.isEmpty() ? QStringLiteral("update") : QStringLiteral("update-%1").arg(version);
|
||||
|
||||
@@ -12,14 +12,14 @@
|
||||
#include <QPromise>
|
||||
#include <QUrl>
|
||||
|
||||
#include "QBlockCipher.h"
|
||||
#include "QRsa.h"
|
||||
#include <openssl/rsa.h>
|
||||
|
||||
#include "amneziaApplication.h"
|
||||
#include "core/repositories/secureAppSettingsRepository.h"
|
||||
#include "core/utils/api/apiUtils.h"
|
||||
#include "core/utils/constants/apiKeys.h"
|
||||
#include "core/utils/networkUtilities.h"
|
||||
#include "core/utils/utilities.h"
|
||||
#include "cryptoUtils.h"
|
||||
|
||||
#ifdef AMNEZIA_DESKTOP
|
||||
#include "core/utils/ipcClient.h"
|
||||
@@ -45,15 +45,75 @@ namespace
|
||||
constexpr QLatin1String unprocessableSubscriptionMessage("Failed to retrieve subscription information. Is it activated?");
|
||||
|
||||
constexpr int proxyStorageRequestTimeoutMsecs = 3000;
|
||||
|
||||
QStringList shuffledProxyUrls(const QStringList &proxyUrls)
|
||||
{
|
||||
QStringList shuffled = proxyUrls;
|
||||
std::random_device randomDevice;
|
||||
std::mt19937 generator(randomDevice());
|
||||
std::shuffle(shuffled.begin(), shuffled.end(), generator);
|
||||
return shuffled;
|
||||
}
|
||||
|
||||
QString getProxyUrlsCacheKey(const QString &serviceType, const QString &userCountryCode)
|
||||
{
|
||||
return QStringLiteral("service_%1_country_%2").arg(serviceType, userCountryCode);
|
||||
}
|
||||
|
||||
bool decryptProxyUrlsPayload(const QByteArray &encryptedPayload, bool isDevEnvironment, QByteArray &decryptedPayload)
|
||||
{
|
||||
QByteArray key = isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
|
||||
if (!isDevEnvironment) {
|
||||
QCryptographicHash hash(QCryptographicHash::Sha512);
|
||||
hash.addData(key);
|
||||
QByteArray h = hash.result().toHex();
|
||||
|
||||
QByteArray decKey = QByteArray::fromHex(h.left(64));
|
||||
QByteArray iv = QByteArray::fromHex(h.mid(64, 32));
|
||||
QByteArray ba = QByteArray::fromBase64(encryptedPayload);
|
||||
|
||||
decryptedPayload = CryptoUtils::decryptAes256Cbc(ba, decKey, iv);
|
||||
if (decryptedPayload.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
decryptedPayload = encryptedPayload;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
QStringList readCachedProxyUrls(const QByteArray &cachedProxyUrlsEncrypted, bool isDevEnvironment)
|
||||
{
|
||||
if (cachedProxyUrlsEncrypted.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
QByteArray cachedProxyUrlsDecrypted;
|
||||
if (!decryptProxyUrlsPayload(cachedProxyUrlsEncrypted, isDevEnvironment, cachedProxyUrlsDecrypted)) {
|
||||
qCritical() << "error decrypting cached proxy urls payload";
|
||||
return {};
|
||||
}
|
||||
|
||||
QJsonArray endpointsArray = QJsonDocument::fromJson(cachedProxyUrlsDecrypted).array();
|
||||
QStringList endpoints;
|
||||
endpoints.reserve(endpointsArray.size());
|
||||
for (const QJsonValue &endpoint : endpointsArray) {
|
||||
endpoints.push_back(endpoint.toString());
|
||||
}
|
||||
|
||||
return endpoints;
|
||||
}
|
||||
}
|
||||
|
||||
GatewayController::GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
|
||||
const bool isStrictKillSwitchEnabled, QObject *parent)
|
||||
const bool isStrictKillSwitchEnabled, SecureAppSettingsRepository *appSettingsRepository,
|
||||
QObject *parent)
|
||||
: QObject(parent),
|
||||
m_gatewayEndpoint(gatewayEndpoint),
|
||||
m_isDevEnvironment(isDevEnvironment),
|
||||
m_requestTimeoutMsecs(requestTimeoutMsecs),
|
||||
m_isStrictKillSwitchEnabled(isStrictKillSwitchEnabled)
|
||||
m_isStrictKillSwitchEnabled(isStrictKillSwitchEnabled),
|
||||
m_appSettingsRepository(appSettingsRepository)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -87,40 +147,29 @@ GatewayController::EncryptedRequestData GatewayController::prepareRequest(const
|
||||
}
|
||||
#endif
|
||||
|
||||
QSimpleCrypto::QBlockCipher blockCipher;
|
||||
encRequestData.key = blockCipher.generatePrivateSalt(32);
|
||||
encRequestData.iv = blockCipher.generatePrivateSalt(32);
|
||||
encRequestData.salt = blockCipher.generatePrivateSalt(8);
|
||||
encRequestData.key = CryptoUtils::generateRandomBytes(32);
|
||||
encRequestData.iv = CryptoUtils::generateRandomBytes(32);
|
||||
encRequestData.salt = CryptoUtils::generateRandomBytes(8);
|
||||
|
||||
QJsonObject keyPayload;
|
||||
keyPayload[apiDefs::key::aesKey] = QString(encRequestData.key.toBase64());
|
||||
keyPayload[apiDefs::key::aesIv] = QString(encRequestData.iv.toBase64());
|
||||
keyPayload[apiDefs::key::aesSalt] = QString(encRequestData.salt.toBase64());
|
||||
|
||||
QByteArray encryptedKeyPayload;
|
||||
QByteArray encryptedApiPayload;
|
||||
try {
|
||||
QSimpleCrypto::QRsa rsa;
|
||||
QByteArray rsaKey = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
|
||||
EVP_PKEY *publicKey = CryptoUtils::loadPublicKeyFromPem(rsaKey);
|
||||
if (publicKey == nullptr) {
|
||||
qCritical() << "error loading public key from environment variables";
|
||||
encRequestData.errorCode = ErrorCode::ApiMissingAgwPublicKey;
|
||||
return encRequestData;
|
||||
}
|
||||
|
||||
EVP_PKEY *publicKey = nullptr;
|
||||
try {
|
||||
QByteArray rsaKey = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
|
||||
QSimpleCrypto::QRsa rsa;
|
||||
publicKey = rsa.getPublicKeyFromByteArray(rsaKey);
|
||||
} catch (...) {
|
||||
Utils::logException();
|
||||
qCritical() << "error loading public key from environment variables";
|
||||
encRequestData.errorCode = ErrorCode::ApiMissingAgwPublicKey;
|
||||
return encRequestData;
|
||||
}
|
||||
QByteArray encryptedKeyPayload = CryptoUtils::rsaEncrypt(QJsonDocument(keyPayload).toJson(), publicKey, RSA_PKCS1_PADDING);
|
||||
EVP_PKEY_free(publicKey);
|
||||
|
||||
encryptedKeyPayload = rsa.encrypt(QJsonDocument(keyPayload).toJson(), publicKey, RSA_PKCS1_PADDING);
|
||||
EVP_PKEY_free(publicKey);
|
||||
QByteArray encryptedApiPayload = CryptoUtils::encryptAes256Cbc(QJsonDocument(apiPayload).toJson(), encRequestData.key, encRequestData.iv);
|
||||
|
||||
encryptedApiPayload = blockCipher.encryptAesBlockCipher(QJsonDocument(apiPayload).toJson(), encRequestData.key, encRequestData.iv,
|
||||
"", encRequestData.salt);
|
||||
} catch (...) {
|
||||
Utils::logException();
|
||||
if (encryptedKeyPayload.isEmpty() || encryptedApiPayload.isEmpty()) {
|
||||
qCritical() << "error when encrypting the request body";
|
||||
encRequestData.errorCode = ErrorCode::ApiConfigDecryptionError;
|
||||
return encRequestData;
|
||||
@@ -142,11 +191,11 @@ GatewayController::DecryptionResult GatewayController::tryDecryptResponseBody(co
|
||||
result.decryptedBody = encryptedResponseBody;
|
||||
result.isDecryptionSuccessful = false;
|
||||
|
||||
try {
|
||||
QSimpleCrypto::QBlockCipher blockCipher;
|
||||
result.decryptedBody = blockCipher.decryptAesBlockCipher(encryptedResponseBody, key, iv, "", salt);
|
||||
QByteArray decrypted = CryptoUtils::decryptAes256Cbc(encryptedResponseBody, key, iv);
|
||||
if (!decrypted.isEmpty()) {
|
||||
result.decryptedBody = decrypted;
|
||||
result.isDecryptionSuccessful = true;
|
||||
} catch (...) {
|
||||
} else {
|
||||
result.decryptedBody = encryptedResponseBody;
|
||||
result.isDecryptionSuccessful = false;
|
||||
}
|
||||
@@ -265,7 +314,6 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
|
||||
}
|
||||
|
||||
if (!decryptionResult.isDecryptionSuccessful) {
|
||||
Utils::logException();
|
||||
qCritical() << "error when decrypting the request body";
|
||||
promise->addResult(qMakePair(ErrorCode::ApiConfigDecryptionError, QByteArray()));
|
||||
promise->finish();
|
||||
@@ -308,8 +356,9 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
|
||||
QStringList proxyStorageUrls;
|
||||
appendStorageUrls(primaryBaseUrls, proxyStorageUrls);
|
||||
appendStorageUrls(fallbackBaseUrls, proxyStorageUrls);
|
||||
const QString proxyUrlsCacheKey = getProxyUrlsCacheKey(serviceType, userCountryCode);
|
||||
|
||||
getProxyUrlsAsync(proxyStorageUrls, 0, [this, encRequestData, endpoint, processResponse](const QStringList &proxyUrls) {
|
||||
getProxyUrlsAsync(proxyStorageUrls, 0, proxyUrlsCacheKey, [this, encRequestData, endpoint, processResponse](const QStringList &proxyUrls) {
|
||||
getProxyUrlAsync(proxyUrls, 0, [this, encRequestData, endpoint, processResponse](const QString &proxyUrl) {
|
||||
bypassProxyAsync(endpoint, proxyUrl, encRequestData,
|
||||
[processResponse, this](const QByteArray &decryptedBody, bool isDecryptionSuccessful,
|
||||
@@ -355,8 +404,6 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS
|
||||
std::shuffle(primaryBaseUrls.begin(), primaryBaseUrls.end(), generator);
|
||||
std::shuffle(fallbackBaseUrls.begin(), fallbackBaseUrls.end(), generator);
|
||||
|
||||
QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
|
||||
|
||||
auto appendStorageUrls = [&serviceType, &userCountryCode](const QStringList &baseUrls, QStringList &target) {
|
||||
if (!serviceType.isEmpty()) {
|
||||
for (const auto &baseUrl : baseUrls) {
|
||||
@@ -372,10 +419,12 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS
|
||||
QStringList proxyStorageUrls;
|
||||
appendStorageUrls(primaryBaseUrls, proxyStorageUrls);
|
||||
appendStorageUrls(fallbackBaseUrls, proxyStorageUrls);
|
||||
const QString proxyUrlsCacheKey = getProxyUrlsCacheKey(serviceType, userCountryCode);
|
||||
const QByteArray cachedProxyUrlsEncrypted = m_appSettingsRepository->readGatewayProxyUrls(proxyUrlsCacheKey);
|
||||
|
||||
if (proxyStorageUrls.empty()) {
|
||||
qDebug() << "empty storage endpoint list";
|
||||
return {};
|
||||
return readCachedProxyUrls(cachedProxyUrlsEncrypted, m_isDevEnvironment);
|
||||
}
|
||||
|
||||
for (const auto &proxyStorageUrl : proxyStorageUrls) {
|
||||
@@ -390,26 +439,8 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS
|
||||
auto encryptedResponseBody = reply->readAll();
|
||||
reply->deleteLater();
|
||||
|
||||
EVP_PKEY *privateKey = nullptr;
|
||||
QByteArray responseBody;
|
||||
try {
|
||||
if (!m_isDevEnvironment) {
|
||||
QCryptographicHash hash(QCryptographicHash::Sha512);
|
||||
hash.addData(key);
|
||||
QByteArray hashResult = hash.result().toHex();
|
||||
|
||||
QByteArray key = QByteArray::fromHex(hashResult.left(64));
|
||||
QByteArray iv = QByteArray::fromHex(hashResult.mid(64, 32));
|
||||
|
||||
QByteArray ba = QByteArray::fromBase64(encryptedResponseBody);
|
||||
|
||||
QSimpleCrypto::QBlockCipher blockCipher;
|
||||
responseBody = blockCipher.decryptAesBlockCipher(ba, key, iv);
|
||||
} else {
|
||||
responseBody = encryptedResponseBody;
|
||||
}
|
||||
} catch (...) {
|
||||
Utils::logException();
|
||||
if (!decryptProxyUrlsPayload(encryptedResponseBody, m_isDevEnvironment, responseBody)) {
|
||||
qCritical() << "error loading private key from environment variables or decrypting payload" << encryptedResponseBody;
|
||||
continue;
|
||||
}
|
||||
@@ -420,6 +451,8 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS
|
||||
for (const auto &endpoint : endpointsArray) {
|
||||
endpoints.push_back(endpoint.toString());
|
||||
}
|
||||
m_appSettingsRepository->writeGatewayProxyUrls(proxyUrlsCacheKey, encryptedResponseBody);
|
||||
|
||||
return endpoints;
|
||||
} else {
|
||||
auto replyError = reply->error();
|
||||
@@ -431,7 +464,7 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS
|
||||
reply->deleteLater();
|
||||
}
|
||||
}
|
||||
return {};
|
||||
return readCachedProxyUrls(cachedProxyUrlsEncrypted, m_isDevEnvironment);
|
||||
}
|
||||
|
||||
bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &decryptedResponseBody,
|
||||
@@ -573,10 +606,12 @@ void GatewayController::bypassProxy(const QString &endpoint, const QString &serv
|
||||
}
|
||||
|
||||
void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex,
|
||||
std::function<void(const QStringList &)> onComplete)
|
||||
const QString &proxyUrlsCacheKey, std::function<void(const QStringList &)> onComplete)
|
||||
{
|
||||
const QByteArray cachedProxyUrlsEncrypted = m_appSettingsRepository->readGatewayProxyUrls(proxyUrlsCacheKey);
|
||||
|
||||
if (currentProxyStorageIndex >= proxyStorageUrls.size()) {
|
||||
onComplete({});
|
||||
onComplete(shuffledProxyUrls(readCachedProxyUrls(cachedProxyUrlsEncrypted, m_isDevEnvironment)));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -589,33 +624,17 @@ void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, co
|
||||
|
||||
// connect(reply, &QNetworkReply::sslErrors, this, [state](const QList<QSslError> &e) { *(state->sslErrors) = e; });
|
||||
|
||||
connect(reply, &QNetworkReply::finished, this, [this, proxyStorageUrls, currentProxyStorageIndex, onComplete, reply]() {
|
||||
connect(reply, &QNetworkReply::finished, this,
|
||||
[this, proxyStorageUrls, currentProxyStorageIndex, proxyUrlsCacheKey, onComplete, reply]() {
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
QByteArray encrypted = reply->readAll();
|
||||
reply->deleteLater();
|
||||
|
||||
QByteArray responseBody;
|
||||
try {
|
||||
QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
|
||||
if (!m_isDevEnvironment) {
|
||||
QCryptographicHash hash(QCryptographicHash::Sha512);
|
||||
hash.addData(key);
|
||||
QByteArray h = hash.result().toHex();
|
||||
|
||||
QByteArray decKey = QByteArray::fromHex(h.left(64));
|
||||
QByteArray iv = QByteArray::fromHex(h.mid(64, 32));
|
||||
QByteArray ba = QByteArray::fromBase64(encrypted);
|
||||
|
||||
QSimpleCrypto::QBlockCipher cipher;
|
||||
responseBody = cipher.decryptAesBlockCipher(ba, decKey, iv);
|
||||
} else {
|
||||
responseBody = encrypted;
|
||||
}
|
||||
} catch (...) {
|
||||
Utils::logException();
|
||||
if (!decryptProxyUrlsPayload(encrypted, m_isDevEnvironment, responseBody)) {
|
||||
qCritical() << "error decrypting payload";
|
||||
QMetaObject::invokeMethod(
|
||||
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, onComplete); }, Qt::QueuedConnection);
|
||||
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, proxyUrlsCacheKey, onComplete); }, Qt::QueuedConnection);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -623,13 +642,9 @@ void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, co
|
||||
QStringList endpoints;
|
||||
for (const QJsonValue &endpoint : endpointsArray)
|
||||
endpoints.push_back(endpoint.toString());
|
||||
m_appSettingsRepository->writeGatewayProxyUrls(proxyUrlsCacheKey, encrypted);
|
||||
|
||||
QStringList shuffled = endpoints;
|
||||
std::random_device randomDevice;
|
||||
std::mt19937 generator(randomDevice());
|
||||
std::shuffle(shuffled.begin(), shuffled.end(), generator);
|
||||
|
||||
onComplete(shuffled);
|
||||
onComplete(shuffledProxyUrls(endpoints));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -638,7 +653,7 @@ void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, co
|
||||
qDebug() << "go to the next storage endpoint";
|
||||
reply->deleteLater();
|
||||
QMetaObject::invokeMethod(
|
||||
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, onComplete); }, Qt::QueuedConnection);
|
||||
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, proxyUrlsCacheKey, onComplete); }, Qt::QueuedConnection);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -16,13 +16,16 @@
|
||||
#include "platforms/ios/ios_controller.h"
|
||||
#endif
|
||||
|
||||
class SecureAppSettingsRepository;
|
||||
|
||||
class GatewayController : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
|
||||
const bool isStrictKillSwitchEnabled, QObject *parent = nullptr);
|
||||
const bool isStrictKillSwitchEnabled, SecureAppSettingsRepository *appSettingsRepository,
|
||||
QObject *parent = nullptr);
|
||||
|
||||
amnezia::ErrorCode post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody);
|
||||
QFuture<QPair<amnezia::ErrorCode, QByteArray>> postAsync(const QString &endpoint, const QJsonObject apiPayload);
|
||||
@@ -55,7 +58,7 @@ private:
|
||||
std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction);
|
||||
|
||||
void getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex,
|
||||
std::function<void(const QStringList &)> onComplete);
|
||||
const QString &proxyUrlsCacheKey, std::function<void(const QStringList &)> onComplete);
|
||||
void getProxyUrlAsync(const QStringList proxyUrls, const int currentProxyIndex, std::function<void(const QString &)> onComplete);
|
||||
void bypassProxyAsync(
|
||||
const QString &endpoint, const QString &proxyUrl, EncryptedRequestData encRequestData,
|
||||
@@ -65,6 +68,7 @@ private:
|
||||
QString m_gatewayEndpoint;
|
||||
bool m_isDevEnvironment = false;
|
||||
bool m_isStrictKillSwitchEnabled = false;
|
||||
SecureAppSettingsRepository *m_appSettingsRepository = nullptr;
|
||||
|
||||
inline static QString m_proxyUrl;
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "core/models/protocolConfig.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDebug>
|
||||
#include <QEventLoop>
|
||||
#include <QFutureWatcher>
|
||||
@@ -20,7 +21,6 @@
|
||||
#include "core/installers/sftpInstaller.h"
|
||||
#include "core/installers/socks5Installer.h"
|
||||
#include "core/installers/mtProxyInstaller.h"
|
||||
#include "core/configurators/xrayConfigurator.h"
|
||||
#include "core/installers/telemtInstaller.h"
|
||||
#include "core/installers/torInstaller.h"
|
||||
#include "core/installers/wireguardInstaller.h"
|
||||
@@ -103,7 +103,7 @@ ErrorCode InstallController::setupContainer(const ServerCredentials &credentials
|
||||
bool isUpdate)
|
||||
{
|
||||
qDebug().noquote() << "InstallController::setupContainer" << ContainerUtils::containerToString(container);
|
||||
SshSession sshSession(this);
|
||||
SshSession sshSession;
|
||||
ErrorCode e = ErrorCode::NoError;
|
||||
|
||||
e = isUserInSudo(credentials, sshSession);
|
||||
@@ -168,11 +168,11 @@ ErrorCode InstallController::updateServerConfig(const QString &serverId, DockerC
|
||||
}
|
||||
if (container == DockerContainer::MtProxy) {
|
||||
ServerCredentials credentials = adminConfig->credentials();
|
||||
SshSession sshSession(this);
|
||||
SshSession sshSession;
|
||||
MtProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
|
||||
} else if (container == DockerContainer::Telemt) {
|
||||
ServerCredentials credentials = adminConfig->credentials();
|
||||
SshSession sshSession(this);
|
||||
SshSession sshSession;
|
||||
TelemtInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
|
||||
}
|
||||
adminConfig->updateContainerConfig(container, newConfig);
|
||||
@@ -188,21 +188,11 @@ ErrorCode InstallController::updateServerConfig(const QString &serverId, DockerC
|
||||
if (!credentials.isValid()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
SshSession sshSession(this);
|
||||
SshSession sshSession;
|
||||
|
||||
bool reinstallRequired = isReinstallContainerRequired(container, oldConfig, newConfig);
|
||||
qDebug() << "InstallController::updateServerConfig for container" << container << "reinstall required is" << reinstallRequired;
|
||||
|
||||
bool xrayServerSettingsChanged = false;
|
||||
if (container == DockerContainer::Xray || container == DockerContainer::SSXray) {
|
||||
const auto *oldXrayConfig = oldConfig.getXrayProtocolConfig();
|
||||
const auto *newXrayConfig = newConfig.getXrayProtocolConfig();
|
||||
if (oldXrayConfig && newXrayConfig) {
|
||||
xrayServerSettingsChanged =
|
||||
!oldXrayConfig->serverConfig.hasEqualServerSettings(newXrayConfig->serverConfig);
|
||||
}
|
||||
}
|
||||
|
||||
ErrorCode errorCode = ErrorCode::NoError;
|
||||
if (reinstallRequired) {
|
||||
errorCode = setupContainer(credentials, container, newConfig, true);
|
||||
@@ -211,20 +201,11 @@ ErrorCode InstallController::updateServerConfig(const QString &serverId, DockerC
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
errorCode = startupContainerWorker(credentials, container, newConfig, sshSession);
|
||||
}
|
||||
}
|
||||
|
||||
const bool skipXrayInboundSync =
|
||||
newConfig.getXrayProtocolConfig() && newConfig.getXrayProtocolConfig()->serverConfig.isThirdPartyConfig;
|
||||
|
||||
if (errorCode == ErrorCode::NoError && xrayServerSettingsChanged && !skipXrayInboundSync) {
|
||||
DnsSettings dnsSettings = { m_appSettingsRepository->primaryDns(), m_appSettingsRepository->secondaryDns() };
|
||||
XrayConfigurator xrayConfigurator(&sshSession);
|
||||
qDebug() << "InstallController::updateServerConfig applying Xray server inbound sync, reinstall="
|
||||
<< reinstallRequired;
|
||||
errorCode = xrayConfigurator.applyServerSettingsToRemote(credentials, container, newConfig, dnsSettings, false);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
qDebug() << "InstallController::updateServerConfig Xray inbound sync failed, error="
|
||||
<< static_cast<int>(errorCode);
|
||||
if (errorCode == ErrorCode::NoError
|
||||
&& (container == DockerContainer::MtProxy || container == DockerContainer::Telemt)) {
|
||||
const QString containerName = ContainerUtils::containerToString(container);
|
||||
errorCode = sshSession.runScript(credentials, "sudo docker restart " + containerName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -716,13 +697,7 @@ bool InstallController::isReinstallContainerRequired(DockerContainer container,
|
||||
const auto *newXrayConfig = newConfig.getXrayProtocolConfig();
|
||||
|
||||
if (oldXrayConfig && newXrayConfig) {
|
||||
const QString oldPort = oldXrayConfig->serverConfig.port.isEmpty()
|
||||
? QString(protocols::xray::defaultPort)
|
||||
: oldXrayConfig->serverConfig.port;
|
||||
const QString newPort = newXrayConfig->serverConfig.port.isEmpty()
|
||||
? QString(protocols::xray::defaultPort)
|
||||
: newXrayConfig->serverConfig.port;
|
||||
if (oldPort != newPort) {
|
||||
if (!oldXrayConfig->serverConfig.hasEqualServerSettings(newXrayConfig->serverConfig)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -739,18 +714,6 @@ bool InstallController::isReinstallContainerRequired(DockerContainer container,
|
||||
if (oldPort != newPort) {
|
||||
return true;
|
||||
}
|
||||
const QString oldTransport = oldMt->transportMode.isEmpty() ? QString(
|
||||
protocols::mtProxy::transportModeStandard)
|
||||
: oldMt->transportMode;
|
||||
const QString newTransport = newMt->transportMode.isEmpty() ? QString(
|
||||
protocols::mtProxy::transportModeStandard)
|
||||
: newMt->transportMode;
|
||||
if (oldTransport != newTransport) {
|
||||
return true;
|
||||
}
|
||||
if (oldMt->tlsDomain != newMt->tlsDomain) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -765,39 +728,6 @@ bool InstallController::isReinstallContainerRequired(DockerContainer container,
|
||||
if (oldPort != newPort) {
|
||||
return true;
|
||||
}
|
||||
const QString oldTransport = oldT->transportMode.isEmpty()
|
||||
? QString(protocols::telemt::transportModeStandard)
|
||||
: oldT->transportMode;
|
||||
const QString newTransport = newT->transportMode.isEmpty()
|
||||
? QString(protocols::telemt::transportModeStandard)
|
||||
: newT->transportMode;
|
||||
if (oldTransport != newTransport) {
|
||||
return true;
|
||||
}
|
||||
if (oldT->tlsDomain != newT->tlsDomain) {
|
||||
return true;
|
||||
}
|
||||
if (oldT->maskEnabled != newT->maskEnabled) {
|
||||
return true;
|
||||
}
|
||||
if (oldT->tlsEmulation != newT->tlsEmulation) {
|
||||
return true;
|
||||
}
|
||||
if (oldT->useMiddleProxy != newT->useMiddleProxy) {
|
||||
return true;
|
||||
}
|
||||
if (oldT->tag != newT->tag) {
|
||||
return true;
|
||||
}
|
||||
const QString oldUser = oldT->userName.isEmpty()
|
||||
? QString::fromUtf8(protocols::telemt::defaultUserName)
|
||||
: oldT->userName;
|
||||
const QString newUser = newT->userName.isEmpty()
|
||||
? QString::fromUtf8(protocols::telemt::defaultUserName)
|
||||
: newT->userName;
|
||||
if (oldUser != newUser) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -837,6 +767,20 @@ ErrorCode InstallController::installDockerWorker(const ServerCredentials &creden
|
||||
|
||||
qDebug().noquote() << "InstallController::installDockerWorker" << stdOut;
|
||||
|
||||
if (container == DockerContainer::MtProxy || container == DockerContainer::Telemt) {
|
||||
QString conntrackOut;
|
||||
auto cbConntrack = [&](const QString &data, libssh::Client &) {
|
||||
conntrackOut += data + "\n";
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
sshSession.runScript(
|
||||
credentials,
|
||||
sshSession.replaceVars(amnezia::scriptData(SharedScriptType::install_conntrack),
|
||||
amnezia::genBaseVars(credentials, DockerContainer::None, QString(), QString())),
|
||||
cbConntrack, cbConntrack);
|
||||
qDebug().noquote() << "InstallController::installDockerWorker install_conntrack:" << conntrackOut;
|
||||
}
|
||||
|
||||
if (container == DockerContainer::Awg2) {
|
||||
QRegularExpression kernelVersionRegex(R"(Linux\s+(\d+)\.(\d+)[^\d]*)");
|
||||
QRegularExpressionMatch match = kernelVersionRegex.match(stdOut);
|
||||
@@ -983,7 +927,7 @@ ErrorCode InstallController::rebootServer(const QString &serverId)
|
||||
if (!credentials.isValid()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
SshSession sshSession(this);
|
||||
SshSession sshSession;
|
||||
|
||||
QString script = QString("sudo reboot");
|
||||
|
||||
@@ -1011,7 +955,7 @@ ErrorCode InstallController::removeAllContainers(const QString &serverId)
|
||||
if (!credentials.isValid()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
SshSession sshSession(this);
|
||||
SshSession sshSession;
|
||||
ErrorCode errorCode = sshSession.runScript(credentials, amnezia::scriptData(SharedScriptType::remove_all_containers));
|
||||
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
@@ -1033,7 +977,7 @@ ErrorCode InstallController::removeContainer(const QString &serverId, DockerCont
|
||||
if (!credentials.isValid()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
SshSession sshSession(this);
|
||||
SshSession sshSession;
|
||||
const amnezia::ScriptVars removeContainerVars =
|
||||
amnezia::genBaseVars(credentials, container, QString(), QString());
|
||||
const bool removeDataVolume = (container == DockerContainer::MtProxy || container == DockerContainer::Telemt);
|
||||
@@ -1142,7 +1086,7 @@ ErrorCode InstallController::scanServerForInstalledContainers(const QString &ser
|
||||
if (!credentials.isValid()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
SshSession sshSession(this);
|
||||
SshSession sshSession;
|
||||
|
||||
QMap<DockerContainer, ContainerConfig> installedContainers;
|
||||
ErrorCode errorCode = getAlreadyInstalledContainers(credentials, installedContainers, sshSession);
|
||||
@@ -1185,7 +1129,7 @@ ErrorCode InstallController::scanServerForInstalledContainers(const QString &ser
|
||||
ErrorCode InstallController::installServer(const ServerCredentials &credentials, DockerContainer container, int port,
|
||||
TransportProto transportProto, bool &wasContainerInstalled)
|
||||
{
|
||||
SshSession sshSession(this);
|
||||
SshSession sshSession;
|
||||
QMap<DockerContainer, ContainerConfig> installedContainers;
|
||||
ErrorCode errorCode = getAlreadyInstalledContainers(credentials, installedContainers, sshSession);
|
||||
if (errorCode) {
|
||||
@@ -1254,7 +1198,7 @@ ErrorCode InstallController::installContainer(const QString &serverId, DockerCon
|
||||
if (!credentials.isValid()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
SshSession sshSession(this);
|
||||
SshSession sshSession;
|
||||
|
||||
QMap<DockerContainer, ContainerConfig> installedContainers;
|
||||
ErrorCode errorCode = getAlreadyInstalledContainers(credentials, installedContainers, sshSession);
|
||||
@@ -1296,7 +1240,7 @@ ErrorCode InstallController::installContainer(const QString &serverId, DockerCon
|
||||
ErrorCode InstallController::checkSshConnection(ServerCredentials &credentials, QString &output,
|
||||
std::function<QString()> passphraseCallback)
|
||||
{
|
||||
SshSession sshSession(this);
|
||||
SshSession sshSession;
|
||||
ErrorCode errorCode = ErrorCode::NoError;
|
||||
|
||||
if (credentials.secretData.contains("BEGIN") && credentials.secretData.contains("PRIVATE KEY")) {
|
||||
@@ -1577,7 +1521,7 @@ ErrorCode InstallController::setDockerContainerEnabledState(const QString &serve
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
const QString containerName = ContainerUtils::containerToString(container);
|
||||
SshSession sshSession(this);
|
||||
SshSession sshSession;
|
||||
const QString script = enabled ? QStringLiteral("sudo docker start %1").arg(containerName)
|
||||
: QStringLiteral("sudo docker stop %1").arg(containerName);
|
||||
const ErrorCode runError = sshSession.runScript(credentials, script);
|
||||
@@ -1617,7 +1561,7 @@ ErrorCode InstallController::queryDockerContainerStatus(const QString &serverId,
|
||||
stdOut += data;
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
SshSession sshSession(this);
|
||||
SshSession sshSession;
|
||||
const QString script = QStringLiteral(
|
||||
"sudo docker inspect --format '{{.State.Status}}' %1 2>/dev/null || echo 'not_found'")
|
||||
.arg(containerName);
|
||||
@@ -1651,7 +1595,7 @@ ErrorCode InstallController::queryMtProxyDiagnostics(const QString &serverId, Do
|
||||
if (!credentials.isValid()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
SshSession sshSession(this);
|
||||
SshSession sshSession;
|
||||
return MtProxyInstaller::queryDiagnostics(sshSession, credentials, container, listenPort, out);
|
||||
}
|
||||
|
||||
@@ -1674,7 +1618,7 @@ QString InstallController::fetchDockerContainerSecret(const QString &serverId, D
|
||||
stdOut += data;
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
SshSession sshSession(this);
|
||||
SshSession sshSession;
|
||||
const QString path = QStringLiteral("/data/secret");
|
||||
const QString cmd = QStringLiteral("sudo docker exec %1 cat %2").arg(containerName, path);
|
||||
const ErrorCode errorCode = sshSession.runScript(credentials, cmd, cbReadStdOut);
|
||||
|
||||
@@ -8,19 +8,10 @@
|
||||
#include "version.h"
|
||||
#include "ui/utils/qAutoStart.h"
|
||||
#include "logger.h"
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
#include "core/local-proxy/portavailabilityhelper.h"
|
||||
#endif
|
||||
#ifdef Q_OS_ANDROID
|
||||
#include "platforms/android/android_controller.h"
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
constexpr int kDefaultProxyPort = 10808;
|
||||
constexpr int kLocalProxyPortMin = 1024;
|
||||
constexpr int kLocalProxyPortMax = 65535;
|
||||
}
|
||||
|
||||
QString getPlatformName()
|
||||
{
|
||||
#if defined(Q_OS_WINDOWS)
|
||||
@@ -47,11 +38,6 @@ SettingsController::SettingsController(SecureServersRepository* serversRepositor
|
||||
{
|
||||
m_appVersion = QString("%1 (%2, %3)").arg(QString(APP_VERSION), __DATE__, GIT_COMMIT_HASH);
|
||||
m_isDevModeEnabled = m_appSettingsRepository->isDevGatewayEnv();
|
||||
|
||||
connect(m_appSettingsRepository, &SecureAppSettingsRepository::localProxySettingsChanged, this,
|
||||
&SettingsController::localProxySettingsUpdated);
|
||||
connect(m_appSettingsRepository, &SecureAppSettingsRepository::localProxyStartFailed, this,
|
||||
&SettingsController::localProxyStartFailed);
|
||||
}
|
||||
|
||||
void SettingsController::toggleAmneziaDns(bool enable)
|
||||
@@ -380,114 +366,3 @@ QString SettingsController::nextAvailableServerName() const
|
||||
return m_serversRepository->nextAvailableServerName();
|
||||
}
|
||||
|
||||
bool SettingsController::isLocalProxySupported() const
|
||||
{
|
||||
#ifdef AMNEZIA_DESKTOP
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool SettingsController::isLocalProxyHttpEnabled() const
|
||||
{
|
||||
return m_appSettingsRepository->isLocalProxyHttpEnabled();
|
||||
}
|
||||
|
||||
int SettingsController::localProxyPort() const
|
||||
{
|
||||
return static_cast<int>(m_appSettingsRepository->localProxyPort());
|
||||
}
|
||||
|
||||
QString SettingsController::localProxyOwnerId() const
|
||||
{
|
||||
return m_appSettingsRepository->localProxyOwnerId();
|
||||
}
|
||||
|
||||
bool SettingsController::isLocalProxyPortUserDefined() const
|
||||
{
|
||||
return m_appSettingsRepository->isLocalProxyPortUserDefined();
|
||||
}
|
||||
|
||||
bool SettingsController::setLocalProxyPort(int port)
|
||||
{
|
||||
if (port < kLocalProxyPortMin || port > kLocalProxyPortMax) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_appSettingsRepository->localProxyPort() == static_cast<quint16>(port)) {
|
||||
m_appSettingsRepository->setLocalProxyPortUserDefined(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
m_appSettingsRepository->setLocalProxyPort(static_cast<quint16>(port));
|
||||
m_appSettingsRepository->setLocalProxyPortUserDefined(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SettingsController::isLocalProxyPortBusy(int port) const
|
||||
{
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
return !PortAvailabilityHelper::isPortAvailable(port);
|
||||
#else
|
||||
Q_UNUSED(port);
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
int SettingsController::findFirstAvailableLocalProxyPort(int startPort) const
|
||||
{
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
const auto port = PortAvailabilityHelper::findFirstAvailablePort(startPort, kLocalProxyPortMax);
|
||||
return port ? *port : -1;
|
||||
#else
|
||||
Q_UNUSED(startPort);
|
||||
return -1;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool SettingsController::enableLocalProxy(const QString &ownerId, int port)
|
||||
{
|
||||
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
|
||||
Q_UNUSED(ownerId);
|
||||
Q_UNUSED(port);
|
||||
return false;
|
||||
#else
|
||||
if (port < kLocalProxyPortMin || port > kLocalProxyPortMax || ownerId.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_appSettingsRepository->isLocalProxyHttpEnabled() && m_appSettingsRepository->localProxyOwnerId() != ownerId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int selectedPort = port;
|
||||
|
||||
const bool isUserDefinedPort = m_appSettingsRepository->isLocalProxyPortUserDefined();
|
||||
if (isUserDefinedPort) {
|
||||
if (!PortAvailabilityHelper::isPortAvailable(selectedPort)) {
|
||||
return false;
|
||||
}
|
||||
} else if (selectedPort != kDefaultProxyPort && !PortAvailabilityHelper::isPortAvailable(selectedPort)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_appSettingsRepository->localProxyPort() != static_cast<quint16>(selectedPort)) {
|
||||
m_appSettingsRepository->setLocalProxyPort(static_cast<quint16>(selectedPort));
|
||||
}
|
||||
m_appSettingsRepository->setLocalProxyPortUserDefined(isUserDefinedPort);
|
||||
|
||||
m_appSettingsRepository->setLocalProxyOwnerId(ownerId);
|
||||
m_appSettingsRepository->setLocalProxyHttpEnabled(true);
|
||||
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
void SettingsController::disableLocalProxy()
|
||||
{
|
||||
if (m_appSettingsRepository->isLocalProxyHttpEnabled()) {
|
||||
m_appSettingsRepository->setLocalProxyHttpEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -89,18 +89,6 @@ public:
|
||||
QLocale getAppLanguage() const;
|
||||
void setAppLanguage(const QLocale &locale);
|
||||
|
||||
// Local proxy
|
||||
bool isLocalProxySupported() const;
|
||||
bool isLocalProxyHttpEnabled() const;
|
||||
int localProxyPort() const;
|
||||
QString localProxyOwnerId() const;
|
||||
bool isLocalProxyPortUserDefined() const;
|
||||
bool setLocalProxyPort(int port);
|
||||
bool isLocalProxyPortBusy(int port) const;
|
||||
int findFirstAvailableLocalProxyPort(int startPort) const;
|
||||
bool enableLocalProxy(const QString &ownerId, int port);
|
||||
void disableLocalProxy();
|
||||
|
||||
signals:
|
||||
void siteSplitTunnelingRouteModeChanged(RouteMode mode);
|
||||
void siteSplitTunnelingToggled(bool enabled);
|
||||
@@ -108,9 +96,6 @@ signals:
|
||||
void appSplitTunnelingToggled(bool enabled);
|
||||
void appSplitTunnelingClearAppsList();
|
||||
|
||||
void localProxySettingsUpdated();
|
||||
void localProxyStartFailed(const QString &message);
|
||||
|
||||
private:
|
||||
QString getPlatform() const;
|
||||
|
||||
|
||||
@@ -95,7 +95,8 @@ void UpdateController::fetchGatewayUrl()
|
||||
auto gatewayController = QSharedPointer<GatewayController>::create(m_appSettingsRepository->getGatewayEndpoint(),
|
||||
m_appSettingsRepository->isDevGatewayEnv(),
|
||||
7000,
|
||||
m_appSettingsRepository->isStrictKillSwitchEnabled());
|
||||
m_appSettingsRepository->isStrictKillSwitchEnabled(),
|
||||
m_appSettingsRepository);
|
||||
|
||||
QJsonObject apiPayload;
|
||||
apiPayload[apiDefs::key::cliVersion] = QString(APP_VERSION);
|
||||
|
||||
@@ -71,48 +71,62 @@ ErrorCode MtProxyInstaller::queryDiagnostics(SshSession &sshSession, const Serve
|
||||
DockerContainer container, int listenPort,
|
||||
MtProxyContainerDiagnostics &out)
|
||||
{
|
||||
out = {};
|
||||
if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
const QString containerName = ContainerUtils::containerToString(container);
|
||||
const QString script =
|
||||
QStringLiteral(
|
||||
"PORT_OK=$(sudo docker exec %1 sh -c 'ss -tlnp 2>/dev/null | grep -q :%2 && echo yes || echo no' 2>/dev/null || echo no); "
|
||||
"TG_OK=$(curl -s --max-time 5 -o /dev/null -w '%%{http_code}' https://core.telegram.org/getProxySecret 2>/dev/null | grep -q '200' && echo yes || echo no); "
|
||||
"CLIENTS=$(sudo docker exec amnezia-mtproxy sh -c 'curl -s --max-time 3 http://localhost:2398/stats 2>/dev/null | grep -o \"total_special_connections:[0-9]*\" | cut -d: -f2' 2>/dev/null); "
|
||||
"CONF_TIME=$(sudo docker exec amnezia-mtproxy sh -c 'stat -c \"%%y\" /data/proxy-multi.conf 2>/dev/null | cut -d. -f1' 2>/dev/null || echo unknown); "
|
||||
"echo \"PORT_OK=${PORT_OK}\"; "
|
||||
"echo \"TG_OK=${TG_OK}\"; "
|
||||
"echo \"CLIENTS=${CLIENTS:-0}\"; "
|
||||
"echo \"CONF_TIME=${CONF_TIME}\"; "
|
||||
"echo \"STATS=http://localhost:2398/stats\";")
|
||||
.arg(containerName)
|
||||
.arg(listenPort);
|
||||
out = { };
|
||||
if (container == DockerContainer::MtProxy || container == DockerContainer::Telemt) {
|
||||
const QString containerName = ContainerUtils::containerToString(container);
|
||||
const bool isTelemt = container == DockerContainer::Telemt;
|
||||
|
||||
QString stdOut;
|
||||
auto cbReadStdOut = [&](const QString &data, libssh::Client &) {
|
||||
stdOut += data;
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
const ErrorCode errorCode = sshSession.runScript(credentials, script, cbReadStdOut);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
for (const QString &line : stdOut.split('\n', Qt::SkipEmptyParts)) {
|
||||
if (line.startsWith(QLatin1String("PORT_OK="))) {
|
||||
out.portReachable = line.mid(8).trimmed() == QLatin1String("yes");
|
||||
} else if (line.startsWith(QLatin1String("TG_OK="))) {
|
||||
out.upstreamReachable = line.mid(6).trimmed() == QLatin1String("yes");
|
||||
} else if (line.startsWith(QLatin1String("CLIENTS="))) {
|
||||
out.clientsConnected = line.mid(8).trimmed().toInt();
|
||||
} else if (line.startsWith(QLatin1String("CONF_TIME="))) {
|
||||
out.lastConfigRefresh = line.mid(10).trimmed();
|
||||
} else if (line.startsWith(QLatin1String("STATS="))) {
|
||||
out.statsEndpoint = line.mid(6).trimmed();
|
||||
const QString sportFilter = QString::number(listenPort);
|
||||
const QString peersCmd = QStringLiteral("sudo conntrack -L -p tcp --dport ") + sportFilter
|
||||
+ QStringLiteral(" 2>/dev/null | grep ESTABLISHED | awk '{for(i=1;i<=NF;i++) if($i ~ /^src=/){print "
|
||||
"substr($i,5); break}}'");
|
||||
const QString publicFilter = QStringLiteral(" | grep -vE "
|
||||
"'^(10\\.|127\\.|169\\.254\\.|192\\.168\\.|172\\.(1[6-9]|2[0-9]|3["
|
||||
"01])\\.|::1$|fe80:|f[cd][0-9a-f][0-9a-f]:)'");
|
||||
const QString clientsCmd =
|
||||
QStringLiteral("CLIENTS=$(") + peersCmd + publicFilter + QStringLiteral(" | sort -u | grep -c .); ");
|
||||
const QString confFile =
|
||||
isTelemt ? QStringLiteral("/data/config.toml") : QStringLiteral("/data/proxy-multi.conf");
|
||||
const QString statsUrl = QString();
|
||||
|
||||
const QString script = QStringLiteral("CN=") + containerName + QStringLiteral("; ")
|
||||
+ QStringLiteral("PORT_OK=$(sudo ss -tlnp 2>/dev/null | grep -q :") + QString::number(listenPort)
|
||||
+ QStringLiteral(" && echo yes || echo no); ")
|
||||
+ QStringLiteral("TG_OK=$(curl -s --max-time 5 -o /dev/null -w '%{http_code}' "
|
||||
"https://core.telegram.org/getProxySecret 2>/dev/null | grep -q '200' && echo yes || "
|
||||
"echo no); ")
|
||||
+ clientsCmd + QStringLiteral("CONF_TIME=$(sudo docker exec \"$CN\" sh -c 'stat -c \"%y\" ") + confFile
|
||||
+ QStringLiteral(" 2>/dev/null | cut -d. -f1' 2>/dev/null || echo unknown); ")
|
||||
+ QStringLiteral("echo \"PORT_OK=${PORT_OK}\"; ") + QStringLiteral("echo \"TG_OK=${TG_OK}\"; ")
|
||||
+ QStringLiteral("echo \"CLIENTS=${CLIENTS:-0}\"; ") + QStringLiteral("echo \"CONF_TIME=${CONF_TIME}\"; ")
|
||||
+ QStringLiteral("echo \"STATS=") + statsUrl + QStringLiteral("\";");
|
||||
|
||||
QString stdOut;
|
||||
auto cbReadStdOut = [&](const QString &data, libssh::Client &) {
|
||||
stdOut += data;
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
const ErrorCode errorCode = sshSession.runScript(credentials, script, cbReadStdOut);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
for (const QString &line : stdOut.split('\n', Qt::SkipEmptyParts)) {
|
||||
if (line.startsWith(QLatin1String("PORT_OK="))) {
|
||||
out.portReachable = line.mid(8).trimmed() == QLatin1String("yes");
|
||||
} else if (line.startsWith(QLatin1String("TG_OK="))) {
|
||||
out.upstreamReachable = line.mid(6).trimmed() == QLatin1String("yes");
|
||||
} else if (line.startsWith(QLatin1String("CLIENTS="))) {
|
||||
out.clientsConnected = line.mid(8).trimmed().toInt();
|
||||
} else if (line.startsWith(QLatin1String("CONF_TIME="))) {
|
||||
out.lastConfigRefresh = line.mid(10).trimmed();
|
||||
} else if (line.startsWith(QLatin1String("STATS="))) {
|
||||
out.statsEndpoint = line.mid(6).trimmed();
|
||||
}
|
||||
}
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
return ErrorCode::NoError;
|
||||
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
void MtProxyInstaller::uploadClientSettingsSnapshot(SshSession &sshSession, const ServerCredentials &credentials,
|
||||
|
||||
@@ -1,444 +0,0 @@
|
||||
#include "configmanager.h"
|
||||
|
||||
#include "core/controllers/gatewayController.h"
|
||||
#include "core/repositories/secureAppSettingsRepository.h"
|
||||
#include "core/repositories/secureServersRepository.h"
|
||||
#include "core/utils/api/apiUtils.h"
|
||||
#include "core/utils/constants/apiConstants.h"
|
||||
#include "core/utils/constants/apiKeys.h"
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
#include "portavailabilityhelper.h"
|
||||
#include "proxylogger.h"
|
||||
#include "version.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonParseError>
|
||||
#include <QSaveFile>
|
||||
#include <QSysInfo>
|
||||
#include <QStandardPaths>
|
||||
#include <QUuid>
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
ConfigManager::ConfigManager(SecureServersRepository *serversRepository, SecureAppSettingsRepository *appSettingsRepository)
|
||||
: m_serversRepository(serversRepository), m_appSettingsRepository(appSettingsRepository)
|
||||
{
|
||||
ProxyLogger::getInstance().debug("ConfigManager initialized (repository-backed)");
|
||||
}
|
||||
|
||||
namespace {
|
||||
namespace gateway_key {
|
||||
constexpr char vless[] = "vless";
|
||||
} // namespace gateway_key
|
||||
|
||||
constexpr quint16 kDefaultProxyPort = 10808;
|
||||
constexpr int kProxyPortMin = 1024;
|
||||
constexpr int kProxyPortMax = 65535;
|
||||
|
||||
int resolveProxyPort(SecureAppSettingsRepository *appSettings)
|
||||
{
|
||||
if (!appSettings) {
|
||||
return kDefaultProxyPort;
|
||||
}
|
||||
|
||||
const quint16 port = appSettings->localProxyPort();
|
||||
if (port < kProxyPortMin || port > kProxyPortMax) {
|
||||
return kDefaultProxyPort;
|
||||
}
|
||||
|
||||
return static_cast<int>(port);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool ConfigManager::applyProxyPortToConfig(QJsonObject &config, int port) const
|
||||
{
|
||||
if (!config.contains("inbounds") || !config.value("inbounds").isArray()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonArray inbounds = config.value("inbounds").toArray();
|
||||
if (inbounds.isEmpty() || !inbounds.at(0).isObject()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonObject firstInbound = inbounds.at(0).toObject();
|
||||
firstInbound.insert("port", port);
|
||||
inbounds[0] = firstInbound;
|
||||
config.insert("inbounds", inbounds);
|
||||
return true;
|
||||
}
|
||||
|
||||
QString ConfigManager::serializeConfig(const QJsonObject &config) const
|
||||
{
|
||||
return QString::fromUtf8(QJsonDocument(config).toJson(QJsonDocument::Compact));
|
||||
}
|
||||
|
||||
std::optional<ConfigManager::ConfigData> ConfigManager::buildConfig(QString &errorDescription) const
|
||||
{
|
||||
errorDescription.clear();
|
||||
|
||||
if (!m_serversRepository || !m_appSettingsRepository) {
|
||||
const QString message = QStringLiteral("Local proxy repositories are not available");
|
||||
ProxyLogger::getInstance().error(message);
|
||||
errorDescription = message;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const QString ownerId = m_appSettingsRepository->localProxyOwnerId();
|
||||
if (ownerId.isEmpty()) {
|
||||
const QString message = QStringLiteral("Local proxy owner server id is not configured");
|
||||
ProxyLogger::getInstance().warning(message);
|
||||
errorDescription = message;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const auto ownerServer = m_serversRepository->serverJsonById(ownerId);
|
||||
if (!ownerServer) {
|
||||
const QString message = QStringLiteral("Owner server with id %1 not found").arg(ownerId);
|
||||
ProxyLogger::getInstance().error(message);
|
||||
errorDescription = message;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (!apiUtils::isPremiumServer(*ownerServer)) {
|
||||
const QString message = QStringLiteral("Server %1 is not premium, local proxy is unavailable")
|
||||
.arg(ownerServer->value(configKey::name).toString());
|
||||
ProxyLogger::getInstance().warning(message);
|
||||
errorDescription = message;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const auto serializedConfig = extractSerializedXrayConfig(*ownerServer);
|
||||
if (!serializedConfig || serializedConfig->isEmpty()) {
|
||||
const QString message = QStringLiteral("Server %1 lacks Xray last_config payload")
|
||||
.arg(ownerServer->value(configKey::name).toString());
|
||||
ProxyLogger::getInstance().error(message);
|
||||
errorDescription = message;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
QJsonParseError parseError;
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(serializedConfig->toUtf8(), &parseError);
|
||||
if (parseError.error != QJsonParseError::NoError || !doc.isObject()) {
|
||||
const QString message = QStringLiteral("Failed to parse Xray config JSON: %1").arg(parseError.errorString());
|
||||
ProxyLogger::getInstance().error(message);
|
||||
errorDescription = message;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
ConfigData data;
|
||||
data.ownerId = ownerId;
|
||||
data.serverName = ownerServer->value(configKey::name).toString();
|
||||
data.parsedConfig = doc.object();
|
||||
const int proxyPort = resolveProxyPort(m_appSettingsRepository);
|
||||
if (applyProxyPortToConfig(data.parsedConfig, proxyPort)) {
|
||||
data.serializedConfig = serializeConfig(data.parsedConfig);
|
||||
} else {
|
||||
ProxyLogger::getInstance().warning(QStringLiteral("Failed to override local proxy inbound port; using original config"));
|
||||
data.serializedConfig = *serializedConfig;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
std::optional<ConfigManager::ConfigData> ConfigManager::buildConfigWithFetch(QString &errorDescription) const
|
||||
{
|
||||
errorDescription.clear();
|
||||
|
||||
if (!m_serversRepository || !m_appSettingsRepository) {
|
||||
const QString message = QStringLiteral("Local proxy repositories are not available");
|
||||
ProxyLogger::getInstance().error(message);
|
||||
errorDescription = message;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const QString ownerId = m_appSettingsRepository->localProxyOwnerId();
|
||||
if (ownerId.isEmpty()) {
|
||||
const QString message = QStringLiteral("Local proxy owner server id is not configured");
|
||||
ProxyLogger::getInstance().warning(message);
|
||||
errorDescription = message;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const auto ownerServer = m_serversRepository->serverJsonById(ownerId);
|
||||
if (!ownerServer) {
|
||||
const QString message = QStringLiteral("Owner server with id %1 not found").arg(ownerId);
|
||||
ProxyLogger::getInstance().error(message);
|
||||
errorDescription = message;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (!apiUtils::isPremiumServer(*ownerServer)) {
|
||||
const QString message = QStringLiteral("Server %1 is not premium, local proxy is unavailable")
|
||||
.arg(ownerServer->value(configKey::name).toString());
|
||||
ProxyLogger::getInstance().warning(message);
|
||||
errorDescription = message;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto serializedConfig = extractSerializedXrayConfig(*ownerServer);
|
||||
if (!serializedConfig || serializedConfig->isEmpty()) {
|
||||
auto fetchedConfig = fetchSerializedXrayConfigFromGateway(*ownerServer, errorDescription);
|
||||
if (!fetchedConfig || fetchedConfig->isEmpty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
serializedConfig = fetchedConfig;
|
||||
}
|
||||
|
||||
QJsonParseError parseError;
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(serializedConfig->toUtf8(), &parseError);
|
||||
if (parseError.error != QJsonParseError::NoError || !doc.isObject()) {
|
||||
const QString message = QStringLiteral("Failed to parse Xray config JSON: %1").arg(parseError.errorString());
|
||||
ProxyLogger::getInstance().error(message);
|
||||
errorDescription = message;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
ConfigData data;
|
||||
data.ownerId = ownerId;
|
||||
data.serverName = ownerServer->value(configKey::name).toString();
|
||||
data.parsedConfig = doc.object();
|
||||
|
||||
int selectedPort = resolveProxyPort(m_appSettingsRepository);
|
||||
const bool isUserDefinedPort = m_appSettingsRepository->isLocalProxyPortUserDefined();
|
||||
|
||||
if (!PortAvailabilityHelper::isPortAvailable(selectedPort)) {
|
||||
const bool canAutoSelect = !isUserDefinedPort && selectedPort == kDefaultProxyPort;
|
||||
if (canAutoSelect) {
|
||||
const auto freePort = PortAvailabilityHelper::findFirstAvailablePort(kDefaultProxyPort + 1, kProxyPortMax);
|
||||
if (!freePort) {
|
||||
errorDescription = QStringLiteral("No available local proxy port in range %1-%2")
|
||||
.arg(kDefaultProxyPort + 1)
|
||||
.arg(kProxyPortMax);
|
||||
ProxyLogger::getInstance().error(errorDescription);
|
||||
return std::nullopt;
|
||||
}
|
||||
selectedPort = *freePort;
|
||||
} else {
|
||||
errorDescription = QStringLiteral("Local proxy port %1 is already in use")
|
||||
.arg(selectedPort);
|
||||
ProxyLogger::getInstance().error(errorDescription);
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
if (applyProxyPortToConfig(data.parsedConfig, selectedPort)) {
|
||||
data.serializedConfig = serializeConfig(data.parsedConfig);
|
||||
} else {
|
||||
ProxyLogger::getInstance().warning(QStringLiteral("Failed to override local proxy inbound port; using original config"));
|
||||
data.serializedConfig = *serializedConfig;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
bool ConfigManager::writeTempConfig(const QString &serializedConfig, QString &configPath, QString &errorDescription) const
|
||||
{
|
||||
errorDescription.clear();
|
||||
configPath.clear();
|
||||
|
||||
const QString directory = tempDirectory();
|
||||
if (!QDir().mkpath(directory)) {
|
||||
const QString message = QStringLiteral("Failed to create temp config directory: %1").arg(directory);
|
||||
ProxyLogger::getInstance().error(message);
|
||||
errorDescription = message;
|
||||
return false;
|
||||
}
|
||||
|
||||
const QString path = tempConfigPath();
|
||||
QSaveFile file(path);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||
const QString message = QStringLiteral("Failed to open temp config file %1: %2").arg(path, file.errorString());
|
||||
ProxyLogger::getInstance().error(message);
|
||||
errorDescription = message;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (file.write(serializedConfig.toUtf8()) == -1) {
|
||||
const QString message = QStringLiteral("Failed to write temp config file %1: %2").arg(path, file.errorString());
|
||||
ProxyLogger::getInstance().error(message);
|
||||
errorDescription = message;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!file.commit()) {
|
||||
const QString message = QStringLiteral("Failed to commit temp config file %1").arg(path);
|
||||
ProxyLogger::getInstance().error(message);
|
||||
errorDescription = message;
|
||||
return false;
|
||||
}
|
||||
|
||||
ProxyLogger::getInstance().info(QStringLiteral("Xray config saved to %1").arg(path));
|
||||
configPath = path;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ConfigManager::removeTempConfig() const
|
||||
{
|
||||
const QString path = tempConfigPath();
|
||||
QFile file(path);
|
||||
if (!file.exists()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!file.remove()) {
|
||||
ProxyLogger::getInstance().warning(QStringLiteral("Failed to remove temp config file %1: %2").arg(path, file.errorString()));
|
||||
return false;
|
||||
}
|
||||
|
||||
ProxyLogger::getInstance().debug(QStringLiteral("Removed temp config file %1").arg(path));
|
||||
return true;
|
||||
}
|
||||
|
||||
QString ConfigManager::tempConfigPath() const
|
||||
{
|
||||
return QDir(tempDirectory()).filePath(QStringLiteral("xray_active.json"));
|
||||
}
|
||||
|
||||
std::optional<QString> ConfigManager::extractSerializedXrayConfig(const QJsonObject &server) const
|
||||
{
|
||||
const QJsonArray containers = server.value(configKey::containers).toArray();
|
||||
const QString targetContainer = ContainerUtils::containerToString(DockerContainer::Xray);
|
||||
const QString protoKey = QString(configKey::xray);
|
||||
|
||||
for (const QJsonValue &value : containers) {
|
||||
const QJsonObject container = value.toObject();
|
||||
if (container.value(configKey::container).toString() != targetContainer) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const QJsonObject proto = container.value(protoKey).toObject();
|
||||
const QString serialized = proto.value(configKey::lastConfig).toString();
|
||||
if (!serialized.isEmpty()) {
|
||||
return serialized;
|
||||
}
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<QString> ConfigManager::fetchSerializedXrayConfigFromGateway(const QJsonObject &server, QString &errorDescription) const
|
||||
{
|
||||
errorDescription.clear();
|
||||
|
||||
if (!m_appSettingsRepository) {
|
||||
const QString message = QStringLiteral("App settings repository is not available");
|
||||
ProxyLogger::getInstance().error(message);
|
||||
errorDescription = message;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const QJsonObject apiConfig = server.value(apiDefs::key::apiConfig).toObject();
|
||||
if (apiConfig.isEmpty()) {
|
||||
const QString message = QStringLiteral("Server API config is missing");
|
||||
ProxyLogger::getInstance().warning(message);
|
||||
errorDescription = message;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const QString userCountryCode = apiConfig.value(apiDefs::key::userCountryCode).toString();
|
||||
const QString serviceType = apiConfig.value(apiDefs::key::serviceType).toString();
|
||||
if (userCountryCode.isEmpty() || serviceType.isEmpty()) {
|
||||
const QString message = QStringLiteral("Server API config lacks service identifiers");
|
||||
ProxyLogger::getInstance().warning(message);
|
||||
errorDescription = message;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
QJsonObject apiPayload;
|
||||
apiPayload[apiDefs::key::osVersion] = QSysInfo::productType();
|
||||
apiPayload[apiDefs::key::appVersion] = QString(APP_VERSION);
|
||||
|
||||
const QString appLanguage = m_appSettingsRepository->getAppLanguage().name().split("_").first();
|
||||
if (!appLanguage.isEmpty()) {
|
||||
apiPayload[apiDefs::key::appLanguage] = appLanguage;
|
||||
}
|
||||
|
||||
apiPayload[apiDefs::key::uuid] = m_appSettingsRepository->getInstallationUuid(true);
|
||||
apiPayload[apiDefs::key::userCountryCode] = userCountryCode;
|
||||
apiPayload[apiDefs::key::serviceType] = serviceType;
|
||||
apiPayload[apiDefs::key::serviceProtocol] = QString(gateway_key::vless);
|
||||
apiPayload[apiDefs::key::publicKey] = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
|
||||
const QJsonObject authData = server.value(apiDefs::key::authData).toObject();
|
||||
if (!authData.isEmpty()) {
|
||||
apiPayload[apiDefs::key::authData] = authData;
|
||||
}
|
||||
|
||||
GatewayController gatewayController(m_appSettingsRepository->getGatewayEndpoint(), m_appSettingsRepository->isDevGatewayEnv(),
|
||||
apiDefs::requestTimeoutMsecs, m_appSettingsRepository->isStrictKillSwitchEnabled());
|
||||
|
||||
QByteArray responseBody;
|
||||
const amnezia::ErrorCode errorCode = gatewayController.post(QString("%1v1/config"), apiPayload, responseBody);
|
||||
if (errorCode != amnezia::ErrorCode::NoError) {
|
||||
const QString message = QStringLiteral("Gateway request failed with error code %1").arg(static_cast<int>(errorCode));
|
||||
ProxyLogger::getInstance().error(message);
|
||||
errorDescription = message;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
QJsonParseError responseError;
|
||||
const QJsonDocument responseDoc = QJsonDocument::fromJson(responseBody, &responseError);
|
||||
if (responseError.error != QJsonParseError::NoError || !responseDoc.isObject()) {
|
||||
const QString message = QStringLiteral("Failed to parse gateway response: %1").arg(responseError.errorString());
|
||||
ProxyLogger::getInstance().error(message);
|
||||
errorDescription = message;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
QString data = responseDoc.object().value(configKey::config).toString();
|
||||
if (data.isEmpty()) {
|
||||
const QString message = QStringLiteral("Gateway response lacks config payload");
|
||||
ProxyLogger::getInstance().error(message);
|
||||
errorDescription = message;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
data.replace("vpn://", "");
|
||||
QByteArray decoded = QByteArray::fromBase64(data.toUtf8(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
|
||||
if (decoded.isEmpty()) {
|
||||
const QString message = QStringLiteral("Gateway config payload is empty");
|
||||
ProxyLogger::getInstance().error(message);
|
||||
errorDescription = message;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const QByteArray uncompressed = qUncompress(decoded);
|
||||
if (!uncompressed.isEmpty()) {
|
||||
decoded = uncompressed;
|
||||
}
|
||||
|
||||
QJsonParseError configError;
|
||||
const QJsonDocument configDoc = QJsonDocument::fromJson(decoded, &configError);
|
||||
if (configError.error != QJsonParseError::NoError || !configDoc.isObject()) {
|
||||
const QString message = QStringLiteral("Failed to parse gateway config JSON: %1").arg(configError.errorString());
|
||||
ProxyLogger::getInstance().error(message);
|
||||
errorDescription = message;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const auto serializedConfig = extractSerializedXrayConfig(configDoc.object());
|
||||
if (!serializedConfig || serializedConfig->isEmpty()) {
|
||||
const QString message = QStringLiteral("Gateway response lacks Xray last_config payload");
|
||||
ProxyLogger::getInstance().error(message);
|
||||
errorDescription = message;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
ProxyLogger::getInstance().info("Fetched Xray config from gateway");
|
||||
return serializedConfig;
|
||||
}
|
||||
|
||||
QString ConfigManager::tempDirectory() const
|
||||
{
|
||||
const QString baseDir = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation);
|
||||
if (baseDir.isEmpty()) {
|
||||
return QDir::temp().filePath(QStringLiteral("amnezia_local_proxy"));
|
||||
}
|
||||
return QDir(baseDir).filePath(QStringLiteral("local_proxy"));
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
|
||||
class SecureServersRepository;
|
||||
class SecureAppSettingsRepository;
|
||||
|
||||
class ConfigManager {
|
||||
public:
|
||||
struct ConfigData {
|
||||
QString ownerId;
|
||||
QString serverName;
|
||||
QString serializedConfig;
|
||||
QJsonObject parsedConfig;
|
||||
};
|
||||
|
||||
ConfigManager(SecureServersRepository *serversRepository, SecureAppSettingsRepository *appSettingsRepository);
|
||||
|
||||
std::optional<ConfigData> buildConfig(QString &errorDescription) const;
|
||||
std::optional<ConfigData> buildConfigWithFetch(QString &errorDescription) const;
|
||||
bool writeTempConfig(const QString &serializedConfig, QString &configPath, QString &errorDescription) const;
|
||||
bool removeTempConfig() const;
|
||||
QString tempConfigPath() const;
|
||||
|
||||
private:
|
||||
std::optional<QString> extractSerializedXrayConfig(const QJsonObject &server) const;
|
||||
std::optional<QString> fetchSerializedXrayConfigFromGateway(const QJsonObject &server, QString &errorDescription) const;
|
||||
QString tempDirectory() const;
|
||||
bool applyProxyPortToConfig(QJsonObject &config, int port) const;
|
||||
QString serializeConfig(const QJsonObject &config) const;
|
||||
|
||||
SecureServersRepository *m_serversRepository;
|
||||
SecureAppSettingsRepository *m_appSettingsRepository;
|
||||
};
|
||||
@@ -1,190 +0,0 @@
|
||||
#include "httpapi.h"
|
||||
#include "proxylogger.h"
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QHostAddress>
|
||||
#include <optional>
|
||||
|
||||
namespace {
|
||||
|
||||
std::optional<int> extractInboundPort(const QJsonObject &config)
|
||||
{
|
||||
if (!config.contains("inbounds") || !config["inbounds"].isArray()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const QJsonArray inbounds = config["inbounds"].toArray();
|
||||
if (inbounds.isEmpty() || !inbounds.at(0).isObject()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const QJsonObject firstInbound = inbounds.at(0).toObject();
|
||||
if (!firstInbound.contains("port")) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return firstInbound.value("port").toInt();
|
||||
}
|
||||
|
||||
QJsonValue proxyPortValue(const std::optional<int> &port)
|
||||
{
|
||||
if (port.has_value()) {
|
||||
return QJsonValue(*port);
|
||||
}
|
||||
return QJsonValue::Null;
|
||||
}
|
||||
|
||||
QHttpServerResponse makeServiceUnavailablePingResponse()
|
||||
{
|
||||
QJsonObject payload{
|
||||
{"status", "error"},
|
||||
{"proxyPort", QJsonValue::Null}
|
||||
};
|
||||
return QHttpServerResponse(payload, QHttpServerResponse::StatusCode::ServiceUnavailable);
|
||||
}
|
||||
|
||||
QHttpServerResponse makeServiceUnavailableStatusResponse()
|
||||
{
|
||||
QJsonObject payload{
|
||||
{"status", "error"}
|
||||
};
|
||||
return QHttpServerResponse(payload, QHttpServerResponse::StatusCode::ServiceUnavailable);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
HttpApi::HttpApi(QWeakPointer<IProxyService> service, QObject* parent)
|
||||
: QObject(parent)
|
||||
, m_tcpServer(new QTcpServer(this))
|
||||
, m_service(service)
|
||||
{
|
||||
ProxyLogger::getInstance().debug("HttpApi initialized");
|
||||
}
|
||||
|
||||
HttpApi::~HttpApi()
|
||||
{
|
||||
stop();
|
||||
}
|
||||
|
||||
bool HttpApi::start(quint16 port)
|
||||
{
|
||||
ProxyLogger::getInstance().info(QString("Starting HTTP API server on port %1").arg(port));
|
||||
|
||||
if (!m_tcpServer->listen(QHostAddress::LocalHost, port)) {
|
||||
ProxyLogger::getInstance().error(QString("Failed to start HTTP API server on port %1").arg(port));
|
||||
return false;
|
||||
}
|
||||
|
||||
setupRoutes();
|
||||
m_server.bind(m_tcpServer.data());
|
||||
|
||||
ProxyLogger::getInstance().info(QString("HTTP API server is running on localhost:%1").arg(m_tcpServer->serverPort()));
|
||||
ProxyLogger::getInstance().debug("Available endpoints:\n"
|
||||
" POST /api/v1/up\n"
|
||||
" POST /api/v1/down\n"
|
||||
" GET /api/v1/ping");
|
||||
return true;
|
||||
}
|
||||
|
||||
void HttpApi::stop()
|
||||
{
|
||||
ProxyLogger::getInstance().info("Stopping HTTP API server");
|
||||
if (m_tcpServer) {
|
||||
m_tcpServer->close();
|
||||
}
|
||||
}
|
||||
|
||||
void HttpApi::setupRoutes()
|
||||
{
|
||||
ProxyLogger::getInstance().debug("Setting up HTTP API routes");
|
||||
|
||||
m_server.route("/api/v1/up", QHttpServerRequest::Method::Post,
|
||||
[this] {
|
||||
ProxyLogger::getInstance().debug("Handling POST /api/v1/up request");
|
||||
return handlePostUp();
|
||||
});
|
||||
|
||||
m_server.route("/api/v1/down", QHttpServerRequest::Method::Post,
|
||||
[this] {
|
||||
ProxyLogger::getInstance().debug("Handling POST /api/v1/down request");
|
||||
return handlePostDown();
|
||||
});
|
||||
|
||||
m_server.route("/api/v1/ping", QHttpServerRequest::Method::Get,
|
||||
[this] {
|
||||
ProxyLogger::getInstance().debug("Handling GET /api/v1/ping request");
|
||||
return handleGetPing();
|
||||
});
|
||||
}
|
||||
|
||||
QHttpServerResponse HttpApi::handlePostUp()
|
||||
{
|
||||
if (auto service = m_service.lock()) {
|
||||
const bool started = service->startXray();
|
||||
QJsonObject response;
|
||||
response["status"] = started ? "ok" : "error";
|
||||
|
||||
const auto port = started ? extractInboundPort(service->getConfig()) : std::optional<int>{};
|
||||
response["proxyPort"] = proxyPortValue(port);
|
||||
|
||||
if (started) {
|
||||
if (port.has_value()) {
|
||||
ProxyLogger::getInstance().info(QString("Xray process started on port %1").arg(*port));
|
||||
} else {
|
||||
ProxyLogger::getInstance().warning("Xray started but inbound port is unknown (local proxy owner may be missing)");
|
||||
}
|
||||
} else {
|
||||
ProxyLogger::getInstance().warning("Failed to start Xray process via HTTP API");
|
||||
}
|
||||
|
||||
return QHttpServerResponse(response);
|
||||
}
|
||||
|
||||
ProxyLogger::getInstance().error("Service unavailable: proxy backend is not initialized");
|
||||
return makeServiceUnavailablePingResponse();
|
||||
}
|
||||
|
||||
QHttpServerResponse HttpApi::handlePostDown()
|
||||
{
|
||||
if (auto service = m_service.lock()) {
|
||||
const bool stopped = service->stopXray();
|
||||
QJsonObject response;
|
||||
response["status"] = stopped ? "ok" : "error";
|
||||
if (!stopped) {
|
||||
ProxyLogger::getInstance().warning("Failed to stop Xray process via HTTP API");
|
||||
} else {
|
||||
ProxyLogger::getInstance().info("Xray process stopped via HTTP API");
|
||||
}
|
||||
|
||||
return QHttpServerResponse(response);
|
||||
}
|
||||
|
||||
ProxyLogger::getInstance().error("Service unavailable: proxy backend is not initialized");
|
||||
return makeServiceUnavailableStatusResponse();
|
||||
}
|
||||
|
||||
QHttpServerResponse HttpApi::handleGetPing() const
|
||||
{
|
||||
if (auto service = m_service.lock()) {
|
||||
QJsonObject response;
|
||||
response["status"] = "ok";
|
||||
const bool isRunning = service->isXrayRunning();
|
||||
if (isRunning) {
|
||||
const auto port = extractInboundPort(service->getConfig());
|
||||
response["proxyPort"] = proxyPortValue(port);
|
||||
if (port.has_value()) {
|
||||
ProxyLogger::getInstance().debug(QString("Xray port: %1").arg(*port));
|
||||
} else {
|
||||
ProxyLogger::getInstance().warning("Unable to detect inbound port while Xray is running");
|
||||
}
|
||||
} else {
|
||||
response["proxyPort"] = QJsonValue::Null;
|
||||
ProxyLogger::getInstance().debug("Xray is not running");
|
||||
}
|
||||
|
||||
return QHttpServerResponse(response);
|
||||
}
|
||||
|
||||
ProxyLogger::getInstance().error("Service unavailable: proxy backend is not initialized");
|
||||
return makeServiceUnavailablePingResponse();
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QScopedPointer>
|
||||
#include <QHttpServer>
|
||||
#include <QHttpServerRequest>
|
||||
#include <QHttpServerResponse>
|
||||
#include <QTcpServer>
|
||||
#include <QWeakPointer>
|
||||
#include "iproxyservice.h"
|
||||
|
||||
class HttpApi : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit HttpApi(QWeakPointer<IProxyService> service, QObject* parent = nullptr);
|
||||
~HttpApi();
|
||||
|
||||
bool start(quint16 port);
|
||||
void stop();
|
||||
|
||||
private:
|
||||
void setupRoutes();
|
||||
|
||||
QHttpServerResponse handlePostUp();
|
||||
QHttpServerResponse handlePostDown();
|
||||
QHttpServerResponse handleGetPing() const;
|
||||
|
||||
QHttpServer m_server;
|
||||
QScopedPointer<QTcpServer> m_tcpServer;
|
||||
QWeakPointer<IProxyService> m_service;
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
|
||||
class IProxyService {
|
||||
public:
|
||||
virtual ~IProxyService() = default;
|
||||
|
||||
virtual QJsonObject getConfig() = 0;
|
||||
|
||||
virtual bool startXray() = 0;
|
||||
virtual bool stopXray() = 0;
|
||||
virtual bool isXrayRunning() const = 0;
|
||||
virtual qint64 getXrayProcessId() const = 0;
|
||||
virtual QString getXrayError() const = 0;
|
||||
};
|
||||
@@ -1,43 +0,0 @@
|
||||
#include "portavailabilityhelper.h"
|
||||
|
||||
#include <QHostAddress>
|
||||
#include <QTcpServer>
|
||||
|
||||
namespace {
|
||||
constexpr int kProxyPortMin = 1024;
|
||||
constexpr int kProxyPortMax = 65535;
|
||||
}
|
||||
|
||||
bool PortAvailabilityHelper::isPortAvailable(int port)
|
||||
{
|
||||
if (port < kProxyPortMin || port > kProxyPortMax) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QTcpServer server;
|
||||
const bool success = server.listen(QHostAddress::LocalHost, static_cast<quint16>(port));
|
||||
server.close();
|
||||
return success;
|
||||
}
|
||||
|
||||
std::optional<int> PortAvailabilityHelper::findFirstAvailablePort(int startPort, int endPort)
|
||||
{
|
||||
if (startPort < kProxyPortMin) {
|
||||
startPort = kProxyPortMin;
|
||||
}
|
||||
if (endPort > kProxyPortMax) {
|
||||
endPort = kProxyPortMax;
|
||||
}
|
||||
if (startPort > endPort) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
for (int port = startPort; port <= endPort; ++port) {
|
||||
if (isPortAvailable(port)) {
|
||||
return port;
|
||||
}
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
|
||||
class PortAvailabilityHelper
|
||||
{
|
||||
public:
|
||||
static bool isPortAvailable(int port);
|
||||
static std::optional<int> findFirstAvailablePort(int startPort, int endPort);
|
||||
};
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
#include "proxylogger.h"
|
||||
#include <QDir>
|
||||
#include <QTextStream>
|
||||
|
||||
ProxyLogger::ProxyLogger() : m_maxFileSize(0), m_currentLevel(LogLevel::Info)
|
||||
{
|
||||
}
|
||||
|
||||
ProxyLogger::~ProxyLogger()
|
||||
{
|
||||
}
|
||||
|
||||
ProxyLogger& ProxyLogger::getInstance()
|
||||
{
|
||||
static ProxyLogger instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
void ProxyLogger::init(const QString& logPath, qint64 maxFileSize)
|
||||
{
|
||||
QMutexLocker locker(&m_mutex);
|
||||
m_logPath = logPath;
|
||||
m_maxFileSize = maxFileSize;
|
||||
|
||||
// Create logs directory if it doesn't exist
|
||||
QDir dir = QFileInfo(m_logPath).dir();
|
||||
if (!dir.exists()) {
|
||||
dir.mkpath(".");
|
||||
}
|
||||
}
|
||||
|
||||
void ProxyLogger::setLogLevel(LogLevel level)
|
||||
{
|
||||
m_currentLevel = level;
|
||||
}
|
||||
|
||||
void ProxyLogger::log(LogLevel level, const QString& message)
|
||||
{
|
||||
logInternal(level, message);
|
||||
}
|
||||
|
||||
void ProxyLogger::debug(const QString& message)
|
||||
{
|
||||
logInternal(LogLevel::Debug, message);
|
||||
}
|
||||
|
||||
void ProxyLogger::info(const QString& message)
|
||||
{
|
||||
logInternal(LogLevel::Info, message);
|
||||
}
|
||||
|
||||
void ProxyLogger::warning(const QString& message)
|
||||
{
|
||||
logInternal(LogLevel::Warning, message);
|
||||
}
|
||||
|
||||
void ProxyLogger::error(const QString& message)
|
||||
{
|
||||
logInternal(LogLevel::Error, message);
|
||||
}
|
||||
|
||||
void ProxyLogger::logInternal(LogLevel level, const QString& message)
|
||||
{
|
||||
if (m_logPath.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (level < m_currentLevel) {
|
||||
return;
|
||||
}
|
||||
|
||||
QMutexLocker locker(&m_mutex);
|
||||
|
||||
checkRotation();
|
||||
|
||||
QFile file(m_logPath);
|
||||
if (!openLogFile(file)) {
|
||||
return;
|
||||
}
|
||||
|
||||
QTextStream stream(&file);
|
||||
QString timestamp = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss.zzz");
|
||||
stream << QString("[%1] [%2] %3\n").arg(timestamp, levelToString(level), message);
|
||||
stream.flush();
|
||||
file.close();
|
||||
}
|
||||
|
||||
QString ProxyLogger::levelToString(LogLevel level)
|
||||
{
|
||||
switch (level) {
|
||||
case LogLevel::Debug: return "DEBUG";
|
||||
case LogLevel::Info: return "INFO";
|
||||
case LogLevel::Warning: return "WARNING";
|
||||
case LogLevel::Error: return "ERROR";
|
||||
default: return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
qint64 ProxyLogger::getCurrentFileSize() const
|
||||
{
|
||||
QFile file(m_logPath);
|
||||
if (file.exists()) {
|
||||
return file.size();
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void ProxyLogger::checkRotation()
|
||||
{
|
||||
if (m_maxFileSize > 0 && getCurrentFileSize() >= m_maxFileSize) {
|
||||
// Delete the oldest file
|
||||
QFile::remove(QString("%1.%2").arg(m_logPath).arg(MAX_BACKUP_FILES));
|
||||
|
||||
// Shift existing files
|
||||
for (int i = MAX_BACKUP_FILES - 1; i >= 1; --i) {
|
||||
QString oldName = QString("%1.%2").arg(m_logPath).arg(i);
|
||||
QString newName = QString("%1.%2").arg(m_logPath).arg(i + 1);
|
||||
QFile::rename(oldName, newName);
|
||||
}
|
||||
|
||||
// Rename current file
|
||||
QFile::rename(m_logPath, m_logPath + ".1");
|
||||
}
|
||||
}
|
||||
|
||||
bool ProxyLogger::openLogFile(QFile& file)
|
||||
{
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text)) {
|
||||
qDebug() << "Failed to open log file:" << m_logPath;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
#ifndef LOCAL_PROXY_LOGGER_H
|
||||
#define LOCAL_PROXY_LOGGER_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QFile>
|
||||
#include <QDateTime>
|
||||
#include <QMutex>
|
||||
#include <QString>
|
||||
|
||||
class ProxyLogger
|
||||
{
|
||||
|
||||
public:
|
||||
enum class LogLevel {
|
||||
Debug,
|
||||
Info,
|
||||
Warning,
|
||||
Error
|
||||
};
|
||||
|
||||
static ProxyLogger& getInstance();
|
||||
|
||||
void init(const QString& logPath, qint64 maxFileSize = 1024 * 1024 * 10); // 10MB by default
|
||||
void setLogLevel(LogLevel level);
|
||||
|
||||
// Main logging method
|
||||
void log(LogLevel level, const QString& message);
|
||||
|
||||
// Helper methods for convenience
|
||||
void debug(const QString& message);
|
||||
void info(const QString& message);
|
||||
void warning(const QString& message);
|
||||
void error(const QString& message);
|
||||
|
||||
private:
|
||||
ProxyLogger();
|
||||
~ProxyLogger();
|
||||
ProxyLogger(const ProxyLogger&) = delete;
|
||||
ProxyLogger& operator=(const ProxyLogger&) = delete;
|
||||
|
||||
void logInternal(LogLevel level, const QString& message);
|
||||
void checkRotation();
|
||||
QString levelToString(LogLevel level);
|
||||
bool openLogFile(QFile& file);
|
||||
qint64 getCurrentFileSize() const;
|
||||
|
||||
QString m_logPath;
|
||||
qint64 m_maxFileSize;
|
||||
LogLevel m_currentLevel;
|
||||
QMutex m_mutex;
|
||||
static const int MAX_BACKUP_FILES = 3;
|
||||
};
|
||||
|
||||
#endif // LOCAL_PROXY_LOGGER_H
|
||||
@@ -1,114 +0,0 @@
|
||||
#include "proxyserver.h"
|
||||
|
||||
#include "core/repositories/secureAppSettingsRepository.h"
|
||||
#include "core/repositories/secureServersRepository.h"
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
ProxyServer::ProxyServer(SecureServersRepository *serversRepository, SecureAppSettingsRepository *appSettingsRepository,
|
||||
QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_appSettingsRepository(appSettingsRepository)
|
||||
, m_service(new ProxyService(serversRepository, appSettingsRepository, this))
|
||||
{
|
||||
m_lastRestartToken = m_appSettingsRepository ? m_appSettingsRepository->localProxyRestartToken() : 0;
|
||||
}
|
||||
|
||||
ProxyServer::~ProxyServer()
|
||||
{
|
||||
stop();
|
||||
}
|
||||
|
||||
bool ProxyServer::start(quint16 port)
|
||||
{
|
||||
if (m_isRunning) {
|
||||
if (m_currentApiPort == port) {
|
||||
qInfo() << "Local proxy: already running on port" << port;
|
||||
return true;
|
||||
}
|
||||
|
||||
qInfo() << "Local proxy: restarting on new port" << port;
|
||||
stop();
|
||||
}
|
||||
|
||||
m_api.reset(new HttpApi(m_service.toWeakRef()));
|
||||
const bool apiStarted = m_api->start(port);
|
||||
if (!apiStarted) {
|
||||
qWarning() << "Local proxy: port is busy:" << port;
|
||||
m_api.reset();
|
||||
m_isRunning = false;
|
||||
m_currentApiPort = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
m_isRunning = true;
|
||||
m_currentApiPort = port;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ProxyServer::stop()
|
||||
{
|
||||
stopXrayProcess();
|
||||
if (m_api) {
|
||||
m_api->stop();
|
||||
m_api.reset();
|
||||
}
|
||||
m_isRunning = false;
|
||||
m_currentApiPort = 0;
|
||||
m_currentProxyPort = 0;
|
||||
}
|
||||
|
||||
bool ProxyServer::startXrayProcess()
|
||||
{
|
||||
return m_service->startXray();
|
||||
}
|
||||
|
||||
void ProxyServer::stopXrayProcess()
|
||||
{
|
||||
m_service->stopXray();
|
||||
}
|
||||
|
||||
bool ProxyServer::syncSettings()
|
||||
{
|
||||
if (!m_isRunning) {
|
||||
qDebug() << "Local proxy: syncSettings called but server is not running";
|
||||
return false;
|
||||
}
|
||||
|
||||
const quint16 newProxyPort = m_appSettingsRepository ? m_appSettingsRepository->localProxyPort() : 0;
|
||||
const int restartToken = m_appSettingsRepository ? m_appSettingsRepository->localProxyRestartToken() : 0;
|
||||
const bool xrayRunning = m_service->isXrayRunning();
|
||||
|
||||
if (!xrayRunning) {
|
||||
qInfo() << "Local proxy: starting Xray on port" << newProxyPort;
|
||||
const bool started = startXrayProcess();
|
||||
if (started) {
|
||||
m_currentProxyPort = newProxyPort;
|
||||
m_lastRestartToken = restartToken;
|
||||
}
|
||||
return started;
|
||||
}
|
||||
|
||||
if (m_lastRestartToken != restartToken) {
|
||||
qInfo() << "Local proxy: restarting Xray due to config change token";
|
||||
const bool restarted = m_service->restartXray();
|
||||
if (restarted) {
|
||||
m_currentProxyPort = newProxyPort;
|
||||
m_lastRestartToken = restartToken;
|
||||
}
|
||||
return restarted;
|
||||
}
|
||||
|
||||
if (m_currentProxyPort != newProxyPort) {
|
||||
qInfo() << "Local proxy: proxy port changed from" << m_currentProxyPort << "to" << newProxyPort;
|
||||
const bool restarted = m_service->restartXray();
|
||||
if (restarted) {
|
||||
m_currentProxyPort = newProxyPort;
|
||||
m_lastRestartToken = restartToken;
|
||||
}
|
||||
return restarted;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QScopedPointer>
|
||||
#include <QSharedPointer>
|
||||
|
||||
#include "httpapi.h"
|
||||
#include "proxyservice.h"
|
||||
|
||||
class SecureServersRepository;
|
||||
class SecureAppSettingsRepository;
|
||||
|
||||
class ProxyServer : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
ProxyServer(SecureServersRepository *serversRepository, SecureAppSettingsRepository *appSettingsRepository,
|
||||
QObject *parent = nullptr);
|
||||
~ProxyServer();
|
||||
|
||||
bool start(quint16 port = 49490);
|
||||
void stop();
|
||||
bool syncSettings();
|
||||
|
||||
private:
|
||||
bool startXrayProcess();
|
||||
void stopXrayProcess();
|
||||
|
||||
SecureAppSettingsRepository *m_appSettingsRepository;
|
||||
QScopedPointer<HttpApi> m_api;
|
||||
QSharedPointer<ProxyService> m_service;
|
||||
bool m_isRunning {false};
|
||||
quint16 m_currentApiPort {0};
|
||||
quint16 m_currentProxyPort {0};
|
||||
int m_lastRestartToken {0};
|
||||
};
|
||||
@@ -1,118 +0,0 @@
|
||||
#include "proxyservice.h"
|
||||
|
||||
#include "proxylogger.h"
|
||||
|
||||
namespace {
|
||||
|
||||
void logConfigError(const QString &errorMessage)
|
||||
{
|
||||
if (!errorMessage.isEmpty()) {
|
||||
ProxyLogger::getInstance().error(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ProxyService::ProxyService(SecureServersRepository *serversRepository, SecureAppSettingsRepository *appSettingsRepository,
|
||||
QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_configManager(new ConfigManager(serversRepository, appSettingsRepository))
|
||||
, m_xrayController(new XrayController())
|
||||
{
|
||||
ProxyLogger::getInstance().debug("ProxyService initialized");
|
||||
}
|
||||
|
||||
QJsonObject ProxyService::getConfig()
|
||||
{
|
||||
if (!m_cachedConfig.isEmpty()) {
|
||||
return m_cachedConfig;
|
||||
}
|
||||
|
||||
QString error;
|
||||
const auto configData = m_configManager->buildConfigWithFetch(error);
|
||||
if (!configData) {
|
||||
logConfigError(error);
|
||||
return {};
|
||||
}
|
||||
|
||||
m_cachedConfig = configData->parsedConfig;
|
||||
return m_cachedConfig;
|
||||
}
|
||||
|
||||
bool ProxyService::startXray()
|
||||
{
|
||||
ProxyLogger::getInstance().info("Starting Xray");
|
||||
|
||||
if (m_xrayController->isXrayRunning()) {
|
||||
ProxyLogger::getInstance().info("Xray is already running");
|
||||
return true;
|
||||
}
|
||||
|
||||
QString error;
|
||||
const auto configData = m_configManager->buildConfigWithFetch(error);
|
||||
if (!configData) {
|
||||
logConfigError(error);
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool success = m_xrayController->start(configData->serializedConfig);
|
||||
if (success) {
|
||||
m_cachedConfig = configData->parsedConfig;
|
||||
ProxyLogger::getInstance().info("Xray started successfully");
|
||||
emit xrayStatusChanged(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
ProxyLogger::getInstance().error(QStringLiteral("Failed to start Xray: %1").arg(m_xrayController->getError()));
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ProxyService::stopXray()
|
||||
{
|
||||
ProxyLogger::getInstance().info("Stopping Xray");
|
||||
const bool stopped = m_xrayController->stop();
|
||||
if (stopped) {
|
||||
ProxyLogger::getInstance().info("Xray stopped");
|
||||
emit xrayStatusChanged(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
ProxyLogger::getInstance().warning(QStringLiteral("Failed to stop Xray: %1").arg(m_xrayController->getError()));
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ProxyService::isXrayRunning() const
|
||||
{
|
||||
return m_xrayController->isXrayRunning();
|
||||
}
|
||||
|
||||
qint64 ProxyService::getXrayProcessId() const
|
||||
{
|
||||
return m_xrayController->getProcessId();
|
||||
}
|
||||
|
||||
QString ProxyService::getXrayError() const
|
||||
{
|
||||
return m_xrayController->getError();
|
||||
}
|
||||
|
||||
void ProxyService::clearCache()
|
||||
{
|
||||
m_cachedConfig = QJsonObject();
|
||||
ProxyLogger::getInstance().debug("ProxyService cache cleared");
|
||||
}
|
||||
|
||||
bool ProxyService::restartXray()
|
||||
{
|
||||
ProxyLogger::getInstance().info("Restarting Xray with updated config");
|
||||
clearCache();
|
||||
|
||||
if (m_xrayController->isXrayRunning()) {
|
||||
if (!stopXray()) {
|
||||
ProxyLogger::getInstance().error("Failed to stop Xray during restart, aborting");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return startXray();
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "configmanager.h"
|
||||
#include "iproxyservice.h"
|
||||
#include "xraycontroller.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QScopedPointer>
|
||||
#include <QJsonObject>
|
||||
|
||||
class SecureServersRepository;
|
||||
class SecureAppSettingsRepository;
|
||||
|
||||
class ProxyService : public QObject, public IProxyService {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
ProxyService(SecureServersRepository *serversRepository, SecureAppSettingsRepository *appSettingsRepository,
|
||||
QObject *parent = nullptr);
|
||||
~ProxyService() = default;
|
||||
|
||||
QJsonObject getConfig() override;
|
||||
bool startXray() override;
|
||||
bool stopXray() override;
|
||||
bool isXrayRunning() const override;
|
||||
qint64 getXrayProcessId() const override;
|
||||
QString getXrayError() const override;
|
||||
|
||||
void clearCache();
|
||||
bool restartXray();
|
||||
|
||||
signals:
|
||||
void xrayStatusChanged(bool running);
|
||||
|
||||
private:
|
||||
QScopedPointer<ConfigManager> m_configManager;
|
||||
QScopedPointer<XrayController> m_xrayController;
|
||||
QJsonObject m_cachedConfig;
|
||||
};
|
||||
@@ -1,105 +0,0 @@
|
||||
#include "xraycontroller.h"
|
||||
|
||||
#include "proxylogger.h"
|
||||
#include "core/utils/ipcClient.h"
|
||||
|
||||
namespace {
|
||||
const QString kIpcUnavailableError = QStringLiteral("Failed to communicate with IPC service");
|
||||
}
|
||||
|
||||
XrayController::XrayController(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
ProxyLogger::getInstance().debug("XrayController initialized");
|
||||
}
|
||||
|
||||
XrayController::~XrayController()
|
||||
{
|
||||
stop();
|
||||
}
|
||||
|
||||
bool XrayController::start(const QString &configJson)
|
||||
{
|
||||
if (m_isRunning) {
|
||||
ProxyLogger::getInstance().info("Xray is already running");
|
||||
return true;
|
||||
}
|
||||
|
||||
ProxyLogger::getInstance().info("Request to start Xray via IPC");
|
||||
|
||||
m_lastError.clear();
|
||||
|
||||
if (configJson.trimmed().isEmpty()) {
|
||||
m_lastError = QStringLiteral("Config content is empty");
|
||||
ProxyLogger::getInstance().error(m_lastError);
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool ipcResult = IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
auto xrayStart = iface->xrayStart(configJson);
|
||||
if (!xrayStart.waitForFinished() || !xrayStart.returnValue()) {
|
||||
ProxyLogger::getInstance().warning("Failed to start Xray via IPC");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}, []() {
|
||||
return false;
|
||||
});
|
||||
|
||||
if (!ipcResult) {
|
||||
m_lastError = kIpcUnavailableError;
|
||||
ProxyLogger::getInstance().error(m_lastError);
|
||||
return false;
|
||||
}
|
||||
|
||||
ProxyLogger::getInstance().info("Xray start command sent to IPC service");
|
||||
m_isRunning = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool XrayController::stop()
|
||||
{
|
||||
if (!m_isRunning) {
|
||||
ProxyLogger::getInstance().debug("Skipping Xray stop via IPC: local proxy Xray is not running");
|
||||
return true;
|
||||
}
|
||||
|
||||
ProxyLogger::getInstance().info("Stopping Xray via IPC");
|
||||
|
||||
const bool ipcResult = IpcClient::withInterface([](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
auto xrayStop = iface->xrayStop();
|
||||
if (!xrayStop.waitForFinished() || !xrayStop.returnValue()) {
|
||||
ProxyLogger::getInstance().warning("Failed to stop Xray via IPC");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}, []() {
|
||||
return false;
|
||||
});
|
||||
|
||||
if (!ipcResult) {
|
||||
m_lastError = kIpcUnavailableError;
|
||||
ProxyLogger::getInstance().warning(m_lastError);
|
||||
return false;
|
||||
}
|
||||
|
||||
m_isRunning = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool XrayController::isXrayRunning() const
|
||||
{
|
||||
return m_isRunning;
|
||||
}
|
||||
|
||||
qint64 XrayController::getProcessId() const
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
QString XrayController::getError() const
|
||||
{
|
||||
return m_lastError;
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
class XrayController : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit XrayController(QObject* parent = nullptr);
|
||||
~XrayController();
|
||||
|
||||
bool start(const QString& configJson);
|
||||
bool stop();
|
||||
bool isXrayRunning() const;
|
||||
qint64 getProcessId() const;
|
||||
QString getError() const;
|
||||
|
||||
private:
|
||||
bool m_isRunning {false};
|
||||
QString m_lastError;
|
||||
};
|
||||
@@ -144,10 +144,6 @@ QJsonObject ApiConfig::toJson() const
|
||||
obj[apiDefs::key::availableCountries] = availableCountries;
|
||||
}
|
||||
|
||||
if (!supportedProtocols.isEmpty()) {
|
||||
obj[apiDefs::key::supportedProtocols] = supportedProtocols;
|
||||
}
|
||||
|
||||
QJsonObject serviceInfoObj = serviceInfo.toJson();
|
||||
if (!serviceInfoObj.isEmpty()) {
|
||||
obj[apiDefs::key::serviceInfo] = serviceInfoObj;
|
||||
@@ -198,7 +194,6 @@ ApiConfig ApiConfig::fromJson(const QJsonObject& json)
|
||||
config.issuedConfigs = json.value(apiDefs::key::issuedConfigs).toInt(0);
|
||||
|
||||
config.availableCountries = json.value(apiDefs::key::availableCountries).toArray();
|
||||
config.supportedProtocols = json.value(apiDefs::key::supportedProtocols).toArray();
|
||||
|
||||
QJsonObject serviceInfoObj = json.value(apiDefs::key::serviceInfo).toObject();
|
||||
if (!serviceInfoObj.isEmpty()) {
|
||||
|
||||
@@ -34,7 +34,6 @@ struct ApiConfig
|
||||
int maxDeviceCount;
|
||||
int issuedConfigs;
|
||||
QJsonArray availableCountries;
|
||||
QJsonArray supportedProtocols;
|
||||
|
||||
struct ServiceInfo {
|
||||
bool isAdVisible = false;
|
||||
|
||||
@@ -121,7 +121,11 @@ QJsonObject ApiV2ServerConfig::toJson() const
|
||||
if (!dns2.isEmpty()) {
|
||||
obj[configKey::dns2] = dns2;
|
||||
}
|
||||
|
||||
|
||||
if (!sendPayload.isEmpty()) {
|
||||
obj[configKey::sendPayload] = sendPayload;
|
||||
}
|
||||
|
||||
if (crc > 0) {
|
||||
obj[configKey::crc] = crc;
|
||||
}
|
||||
@@ -165,6 +169,7 @@ ApiV2ServerConfig ApiV2ServerConfig::fromJson(const QJsonObject& json)
|
||||
|
||||
config.dns1 = json.value(configKey::dns1).toString();
|
||||
config.dns2 = json.value(configKey::dns2).toString();
|
||||
config.sendPayload = json.value(configKey::sendPayload).toArray();
|
||||
|
||||
config.crc = json.value(configKey::crc).toInt(0);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef APIV2SERVERCONFIG_H
|
||||
#define APIV2SERVERCONFIG_H
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QMap>
|
||||
#include <QPair>
|
||||
@@ -28,6 +29,7 @@ struct ApiV2ServerConfig {
|
||||
DockerContainer defaultContainer;
|
||||
QString dns1;
|
||||
QString dns2;
|
||||
QJsonArray sendPayload;
|
||||
|
||||
QString name;
|
||||
bool nameOverriddenByUser = false;
|
||||
|
||||
@@ -74,6 +74,28 @@ QJsonObject AwgServerConfig::toJson() const
|
||||
obj[configKey::specialJunk4] = specialJunk4;
|
||||
obj[configKey::specialJunk5] = specialJunk5;
|
||||
|
||||
if (!headerProtectionKey.isEmpty()) {
|
||||
obj[configKey::headerProtectionKey] = headerProtectionKey;
|
||||
}
|
||||
if (!contentPaddingAddition.isEmpty()) {
|
||||
obj[configKey::contentPaddingAddition] = contentPaddingAddition;
|
||||
}
|
||||
if (!rekeyAfterTime.isEmpty()) {
|
||||
obj[configKey::rekeyAfterTime] = rekeyAfterTime;
|
||||
}
|
||||
if (!rekeyTimeout.isEmpty()) {
|
||||
obj[configKey::rekeyTimeout] = rekeyTimeout;
|
||||
}
|
||||
if (!rejectAfterTime.isEmpty()) {
|
||||
obj[configKey::rejectAfterTime] = rejectAfterTime;
|
||||
}
|
||||
if (!keepaliveTimeout.isEmpty()) {
|
||||
obj[configKey::keepaliveTimeout] = keepaliveTimeout;
|
||||
}
|
||||
if (!maxHandshakeAttempts.isEmpty()) {
|
||||
obj[configKey::maxHandshakeAttempts] = maxHandshakeAttempts;
|
||||
}
|
||||
|
||||
if (isThirdPartyConfig) {
|
||||
obj[configKey::isThirdPartyConfig] = isThirdPartyConfig;
|
||||
}
|
||||
@@ -109,6 +131,14 @@ AwgServerConfig AwgServerConfig::fromJson(const QJsonObject& json)
|
||||
config.specialJunk3 = json.value(configKey::specialJunk3).toString();
|
||||
config.specialJunk4 = json.value(configKey::specialJunk4).toString();
|
||||
config.specialJunk5 = json.value(configKey::specialJunk5).toString();
|
||||
|
||||
config.headerProtectionKey = json.value(configKey::headerProtectionKey).toString();
|
||||
config.contentPaddingAddition = json.value(configKey::contentPaddingAddition).toString();
|
||||
config.rekeyAfterTime = json.value(configKey::rekeyAfterTime).toString();
|
||||
config.rekeyTimeout = json.value(configKey::rekeyTimeout).toString();
|
||||
config.rejectAfterTime = json.value(configKey::rejectAfterTime).toString();
|
||||
config.keepaliveTimeout = json.value(configKey::keepaliveTimeout).toString();
|
||||
config.maxHandshakeAttempts = json.value(configKey::maxHandshakeAttempts).toString();
|
||||
|
||||
config.isThirdPartyConfig = json.value(configKey::isThirdPartyConfig).toBool(false);
|
||||
|
||||
@@ -196,11 +226,43 @@ QJsonObject AwgClientConfig::toJson() const
|
||||
obj[configKey::transportPacketMagicHeader] = transportPacketMagicHeader;
|
||||
}
|
||||
|
||||
obj[configKey::specialJunk1] = specialJunk1;
|
||||
obj[configKey::specialJunk2] = specialJunk2;
|
||||
obj[configKey::specialJunk3] = specialJunk3;
|
||||
obj[configKey::specialJunk4] = specialJunk4;
|
||||
obj[configKey::specialJunk5] = specialJunk5;
|
||||
if (!specialJunk1.isEmpty()) {
|
||||
obj[configKey::specialJunk1] = specialJunk1;
|
||||
}
|
||||
if (!specialJunk2.isEmpty()) {
|
||||
obj[configKey::specialJunk2] = specialJunk2;
|
||||
}
|
||||
if (!specialJunk3.isEmpty()) {
|
||||
obj[configKey::specialJunk3] = specialJunk3;
|
||||
}
|
||||
if (!specialJunk4.isEmpty()) {
|
||||
obj[configKey::specialJunk4] = specialJunk4;
|
||||
}
|
||||
if (!specialJunk5.isEmpty()) {
|
||||
obj[configKey::specialJunk5] = specialJunk5;
|
||||
}
|
||||
|
||||
if (!headerProtectionKey.isEmpty()) {
|
||||
obj[configKey::headerProtectionKey] = headerProtectionKey;
|
||||
}
|
||||
if (!contentPaddingAddition.isEmpty()) {
|
||||
obj[configKey::contentPaddingAddition] = contentPaddingAddition;
|
||||
}
|
||||
if (!rekeyAfterTime.isEmpty()) {
|
||||
obj[configKey::rekeyAfterTime] = rekeyAfterTime;
|
||||
}
|
||||
if (!rekeyTimeout.isEmpty()) {
|
||||
obj[configKey::rekeyTimeout] = rekeyTimeout;
|
||||
}
|
||||
if (!rejectAfterTime.isEmpty()) {
|
||||
obj[configKey::rejectAfterTime] = rejectAfterTime;
|
||||
}
|
||||
if (!keepaliveTimeout.isEmpty()) {
|
||||
obj[configKey::keepaliveTimeout] = keepaliveTimeout;
|
||||
}
|
||||
if (!maxHandshakeAttempts.isEmpty()) {
|
||||
obj[configKey::maxHandshakeAttempts] = maxHandshakeAttempts;
|
||||
}
|
||||
|
||||
if (isObfuscationEnabled) {
|
||||
obj[configKey::isObfuscationEnabled] = isObfuscationEnabled;
|
||||
@@ -248,6 +310,14 @@ AwgClientConfig AwgClientConfig::fromJson(const QJsonObject& json)
|
||||
config.specialJunk3 = json.value(configKey::specialJunk3).toString();
|
||||
config.specialJunk4 = json.value(configKey::specialJunk4).toString();
|
||||
config.specialJunk5 = json.value(configKey::specialJunk5).toString();
|
||||
|
||||
config.headerProtectionKey = json.value(configKey::headerProtectionKey).toString();
|
||||
config.contentPaddingAddition = json.value(configKey::contentPaddingAddition).toString();
|
||||
config.rekeyAfterTime = json.value(configKey::rekeyAfterTime).toString();
|
||||
config.rekeyTimeout = json.value(configKey::rekeyTimeout).toString();
|
||||
config.rejectAfterTime = json.value(configKey::rejectAfterTime).toString();
|
||||
config.keepaliveTimeout = json.value(configKey::keepaliveTimeout).toString();
|
||||
config.maxHandshakeAttempts = json.value(configKey::maxHandshakeAttempts).toString();
|
||||
|
||||
config.isObfuscationEnabled = json.value(configKey::isObfuscationEnabled).toBool(false);
|
||||
|
||||
@@ -310,7 +380,8 @@ bool AwgServerConfig::hasEqualServerSettings(const AwgServerConfig& other) const
|
||||
transportPacketMagicHeader != other.transportPacketMagicHeader ||
|
||||
specialJunk1 != other.specialJunk1 || specialJunk2 != other.specialJunk2 ||
|
||||
specialJunk3 != other.specialJunk3 || specialJunk4 != other.specialJunk4 ||
|
||||
specialJunk5 != other.specialJunk5) {
|
||||
specialJunk5 != other.specialJunk5 ||
|
||||
headerProtectionKey != other.headerProtectionKey) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,13 @@ struct AwgServerConfig {
|
||||
QString specialJunk3;
|
||||
QString specialJunk4;
|
||||
QString specialJunk5;
|
||||
QString headerProtectionKey;
|
||||
QString contentPaddingAddition;
|
||||
QString rekeyAfterTime;
|
||||
QString rekeyTimeout;
|
||||
QString rejectAfterTime;
|
||||
QString keepaliveTimeout;
|
||||
QString maxHandshakeAttempts;
|
||||
bool isThirdPartyConfig = false;
|
||||
|
||||
QJsonObject toJson() const;
|
||||
@@ -76,6 +83,13 @@ struct AwgClientConfig {
|
||||
QString specialJunk3;
|
||||
QString specialJunk4;
|
||||
QString specialJunk5;
|
||||
QString headerProtectionKey;
|
||||
QString contentPaddingAddition;
|
||||
QString rekeyAfterTime;
|
||||
QString rekeyTimeout;
|
||||
QString rejectAfterTime;
|
||||
QString keepaliveTimeout;
|
||||
QString maxHandshakeAttempts;
|
||||
bool isObfuscationEnabled = false;
|
||||
|
||||
QJsonObject toJson() const;
|
||||
|
||||
@@ -68,8 +68,6 @@ ErrorCode XrayProtocol::start()
|
||||
{
|
||||
qDebug() << "XrayProtocol::start()";
|
||||
|
||||
m_teardownDone = false;
|
||||
|
||||
// Inject SOCKS5 auth into the inbound before starting xray.
|
||||
// Re-uses existing credentials if the config already has them (e.g. imported config).
|
||||
amnezia::serialization::inbounds::InboundCredentials creds;
|
||||
@@ -122,17 +120,6 @@ void XrayProtocol::stop()
|
||||
{
|
||||
qDebug() << "XrayProtocol::stop()";
|
||||
|
||||
// The destructor calls stop() as a safety net, but stop() is also called
|
||||
// explicitly on disconnect. Because stop() spins the event loop via
|
||||
// waitForFinished(), the local proxy may start its own xray on the shared
|
||||
// global engine in between. Running the teardown (xrayStop) twice would then
|
||||
// kill the proxy's just-started xray. Run the engine teardown only once.
|
||||
if (m_teardownDone) {
|
||||
qDebug() << "XrayProtocol::stop() teardown already done, skipping";
|
||||
return;
|
||||
}
|
||||
m_teardownDone = true;
|
||||
|
||||
IpcClient::withInterface([](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
auto disableKillSwitch = iface->disableKillSwitch();
|
||||
if (!disableKillSwitch.waitForFinished() || !disableKillSwitch.returnValue())
|
||||
|
||||
@@ -38,11 +38,6 @@ private:
|
||||
int m_tun2socksRetryCount = 0;
|
||||
static constexpr int maxTun2SocksRetries = 5;
|
||||
static constexpr int tun2socksRetryDelayMs = 400;
|
||||
|
||||
// Guards against tearing down the shared xray engine more than once.
|
||||
// The destructor also calls stop(); without this the second stop() would
|
||||
// call xrayStop() on the global engine that the local proxy may already own.
|
||||
bool m_teardownDone = false;
|
||||
};
|
||||
|
||||
#endif // XRAYPROTOCOL_H
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
#include <QJsonObject>
|
||||
#include <QUuid>
|
||||
|
||||
#include <limits>
|
||||
|
||||
#include "core/utils/errorCodes.h"
|
||||
#include "core/utils/routeModes.h"
|
||||
#include "core/utils/commonStructs.h"
|
||||
@@ -282,6 +280,24 @@ void SecureAppSettingsRepository::toggleDevGatewayEnv(bool enabled)
|
||||
setValue("Conf/devGatewayEnv", enabled);
|
||||
}
|
||||
|
||||
QByteArray SecureAppSettingsRepository::readGatewayProxyUrls(const QString &cacheKey) const
|
||||
{
|
||||
if (cacheKey.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return value(QStringLiteral("Conf/proxyUrls/") + cacheKey).toByteArray();
|
||||
}
|
||||
|
||||
void SecureAppSettingsRepository::writeGatewayProxyUrls(const QString &cacheKey, const QByteArray &proxyUrlsEncrypted)
|
||||
{
|
||||
if (cacheKey.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setValue(QStringLiteral("Conf/proxyUrls/") + cacheKey, proxyUrlsEncrypted);
|
||||
}
|
||||
|
||||
bool SecureAppSettingsRepository::isKillSwitchEnabled() const
|
||||
{
|
||||
return value("Conf/killSwitchEnabled", true).toBool();
|
||||
@@ -442,59 +458,3 @@ void SecureAppSettingsRepository::setXraySavedConfigs(const QByteArray &data)
|
||||
{
|
||||
setValue("Xray/savedConfigs", data);
|
||||
}
|
||||
|
||||
QString SecureAppSettingsRepository::localProxyOwnerId() const
|
||||
{
|
||||
return value("Conf/localProxyOwnerId", "").toString();
|
||||
}
|
||||
|
||||
void SecureAppSettingsRepository::setLocalProxyOwnerId(const QString &serverId)
|
||||
{
|
||||
setValue("Conf/localProxyOwnerId", serverId);
|
||||
emit localProxySettingsChanged();
|
||||
}
|
||||
|
||||
quint16 SecureAppSettingsRepository::localProxyPort() const
|
||||
{
|
||||
return static_cast<quint16>(value("Conf/localProxyPort", 10808).toUInt());
|
||||
}
|
||||
|
||||
void SecureAppSettingsRepository::setLocalProxyPort(quint16 port)
|
||||
{
|
||||
setValue("Conf/localProxyPort", port);
|
||||
emit localProxySettingsChanged();
|
||||
}
|
||||
|
||||
bool SecureAppSettingsRepository::isLocalProxyPortUserDefined() const
|
||||
{
|
||||
return value("Conf/localProxyPortUserDefined", false).toBool();
|
||||
}
|
||||
|
||||
void SecureAppSettingsRepository::setLocalProxyPortUserDefined(bool userDefined)
|
||||
{
|
||||
setValue("Conf/localProxyPortUserDefined", userDefined);
|
||||
}
|
||||
|
||||
bool SecureAppSettingsRepository::isLocalProxyHttpEnabled() const
|
||||
{
|
||||
return value("Conf/localProxyHttpEnabled", false).toBool();
|
||||
}
|
||||
|
||||
void SecureAppSettingsRepository::setLocalProxyHttpEnabled(bool enabled)
|
||||
{
|
||||
setValue("Conf/localProxyHttpEnabled", enabled);
|
||||
emit localProxySettingsChanged();
|
||||
}
|
||||
|
||||
int SecureAppSettingsRepository::localProxyRestartToken() const
|
||||
{
|
||||
return value("Conf/localProxyRestartToken", 0).toInt();
|
||||
}
|
||||
|
||||
void SecureAppSettingsRepository::bumpLocalProxyRestartToken()
|
||||
{
|
||||
const int current = localProxyRestartToken();
|
||||
const int next = (current == std::numeric_limits<int>::max()) ? 0 : (current + 1);
|
||||
setValue("Conf/localProxyRestartToken", next);
|
||||
emit localProxySettingsChanged();
|
||||
}
|
||||
|
||||
@@ -59,7 +59,9 @@ public:
|
||||
void setDevGatewayEndpoint();
|
||||
bool isDevGatewayEnv(bool isTestPurchase = false) const;
|
||||
void toggleDevGatewayEnv(bool enabled);
|
||||
|
||||
QByteArray readGatewayProxyUrls(const QString &cacheKey) const;
|
||||
void writeGatewayProxyUrls(const QString &cacheKey, const QByteArray &proxyUrlsEncrypted);
|
||||
|
||||
bool isKillSwitchEnabled() const;
|
||||
void setKillSwitchEnabled(bool enabled);
|
||||
bool isStrictKillSwitchEnabled() const;
|
||||
@@ -93,18 +95,6 @@ public:
|
||||
QByteArray xraySavedConfigs() const;
|
||||
void setXraySavedConfigs(const QByteArray &data);
|
||||
|
||||
// Local proxy
|
||||
QString localProxyOwnerId() const;
|
||||
void setLocalProxyOwnerId(const QString &serverId);
|
||||
quint16 localProxyPort() const;
|
||||
void setLocalProxyPort(quint16 port);
|
||||
bool isLocalProxyPortUserDefined() const;
|
||||
void setLocalProxyPortUserDefined(bool userDefined);
|
||||
bool isLocalProxyHttpEnabled() const;
|
||||
void setLocalProxyHttpEnabled(bool enabled);
|
||||
int localProxyRestartToken() const;
|
||||
void bumpLocalProxyRestartToken();
|
||||
|
||||
signals:
|
||||
void appLanguageChanged(QLocale locale);
|
||||
void allowedDnsServersChanged(const QStringList &servers);
|
||||
@@ -118,8 +108,6 @@ signals:
|
||||
void saveLogsChanged(bool enabled);
|
||||
void screenshotsEnabledChanged(bool enabled);
|
||||
void settingsCleared();
|
||||
void localProxySettingsChanged();
|
||||
void localProxyStartFailed(const QString &message);
|
||||
|
||||
private:
|
||||
void setVpnSites(RouteMode mode, const QVariantMap &sites);
|
||||
|
||||
@@ -309,15 +309,6 @@ serverConfigUtils::ConfigType SecureServersRepository::serverKind(const QString
|
||||
return serverConfigUtils::configTypeFromJson(withoutStorageServerId(it.value()));
|
||||
}
|
||||
|
||||
std::optional<QJsonObject> SecureServersRepository::serverJsonById(const QString &serverId) const
|
||||
{
|
||||
const auto it = m_serverJsonById.constFind(serverId);
|
||||
if (it == m_serverJsonById.constEnd()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return it.value();
|
||||
}
|
||||
|
||||
std::optional<SelfHostedAdminServerConfig> SecureServersRepository::selfHostedAdminConfig(const QString &serverId) const
|
||||
{
|
||||
const auto it = m_serverJsonById.constFind(serverId);
|
||||
|
||||
@@ -31,8 +31,6 @@ public:
|
||||
void removeServer(const QString &serverId);
|
||||
serverConfigUtils::ConfigType serverKind(const QString &serverId) const;
|
||||
|
||||
std::optional<QJsonObject> serverJsonById(const QString &serverId) const;
|
||||
|
||||
std::optional<SelfHostedAdminServerConfig> selfHostedAdminConfig(const QString &serverId) const;
|
||||
std::optional<SelfHostedUserServerConfig> selfHostedUserConfig(const QString &serverId) const;
|
||||
std::optional<NativeServerConfig> nativeConfig(const QString &serverId) const;
|
||||
|
||||
@@ -18,8 +18,8 @@ namespace apiDefs
|
||||
constexpr QLatin1String stackType("stack_type");
|
||||
constexpr QLatin1String cliVersion("cli_version");
|
||||
constexpr QLatin1String cliName("cli_name");
|
||||
constexpr QLatin1String supportedProtocols("supported_protocols");
|
||||
constexpr QLatin1String availableCountries("available_countries");
|
||||
constexpr QLatin1String availableProtocols("available_protocols");
|
||||
constexpr QLatin1String installationUuid("installation_uuid");
|
||||
constexpr QLatin1String uuid("installation_uuid");
|
||||
constexpr QLatin1String osVersion("os_version");
|
||||
|
||||
@@ -25,6 +25,13 @@ namespace amnezia
|
||||
constexpr QLatin1String config("config");
|
||||
constexpr QLatin1String configVersion("config_version");
|
||||
|
||||
constexpr QLatin1String sendPayload("send_payload");
|
||||
constexpr QLatin1String sendPayloadEndpoint("endpoint");
|
||||
constexpr QLatin1String sendPayloadTimeoutMs("timeout_ms");
|
||||
constexpr QLatin1String sendPayloadProtocol("protocol");
|
||||
constexpr QLatin1String sendPayloadData("payload");
|
||||
constexpr QLatin1String sendPayloadExpectedResponse("expected_response");
|
||||
|
||||
constexpr QLatin1String containers("containers");
|
||||
constexpr QLatin1String container("container");
|
||||
constexpr QLatin1String defaultContainer("defaultContainer");
|
||||
@@ -83,6 +90,14 @@ namespace amnezia
|
||||
constexpr QLatin1String specialJunk4("I4");
|
||||
constexpr QLatin1String specialJunk5("I5");
|
||||
|
||||
constexpr QLatin1String headerProtectionKey("HeaderProtectionKey");
|
||||
constexpr QLatin1String contentPaddingAddition("ContentPaddingAddition");
|
||||
constexpr QLatin1String rekeyAfterTime("RekeyAfterTime");
|
||||
constexpr QLatin1String rekeyTimeout("RekeyTimeout");
|
||||
constexpr QLatin1String rejectAfterTime("RejectAfterTime");
|
||||
constexpr QLatin1String keepaliveTimeout("KeepaliveTimeout");
|
||||
constexpr QLatin1String maxHandshakeAttempts("MaxHandshakeAttempts");
|
||||
|
||||
constexpr QLatin1String protocolVersion("protocol_version");
|
||||
|
||||
constexpr QLatin1String openvpn("openvpn");
|
||||
|
||||
@@ -64,7 +64,7 @@ namespace amnezia
|
||||
constexpr char defaultFlow[] = "xtls-rprx-vision";
|
||||
constexpr char defaultTransport[] = "raw";
|
||||
constexpr char defaultFingerprint[] = "chrome";
|
||||
constexpr char defaultSni[] = "cdn.example.com";
|
||||
constexpr char defaultSni[] = "www.googletagmanager.com";
|
||||
constexpr char defaultAlpn[] = "HTTP/2";
|
||||
|
||||
constexpr char defaultXhttpMode[] = "Auto";
|
||||
@@ -147,6 +147,7 @@ namespace amnezia
|
||||
constexpr char defaultSubnetCidr[] = "24";
|
||||
|
||||
constexpr char defaultPort[] = "51820";
|
||||
constexpr char defaultPersistentKeepAlive[] = "25";
|
||||
|
||||
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
constexpr char defaultMtu[] = "1280";
|
||||
@@ -271,6 +272,7 @@ namespace amnezia
|
||||
constexpr char workersModeAuto[] = "auto";
|
||||
constexpr char workersModeManual[] = "manual";
|
||||
constexpr int maxWorkers = 32;
|
||||
constexpr int botTagHexLength = 32;
|
||||
}
|
||||
|
||||
} // namespace protocols
|
||||
|
||||
@@ -286,7 +286,7 @@ QPair<QString, QNetworkInterface> NetworkUtilities::getGatewayAndIface()
|
||||
return { resGateway, QNetworkInterface::interfaceFromIndex(resIndex) };
|
||||
#endif
|
||||
#ifdef Q_OS_LINUX
|
||||
constexpr int BUFFER_SIZE = 100;
|
||||
constexpr int BUFFER_SIZE = 8192;
|
||||
int received_bytes = 0, msg_len = 0, route_attribute_len = 0;
|
||||
int sock = -1, msgseq = 0;
|
||||
struct nlmsghdr *nlh, *nlmsg;
|
||||
@@ -294,7 +294,7 @@ QPair<QString, QNetworkInterface> NetworkUtilities::getGatewayAndIface()
|
||||
// This struct contain route attributes (route type)
|
||||
struct rtattr *route_attribute;
|
||||
char gateway_address[INET_ADDRSTRLEN], interface[IF_NAMESIZE];
|
||||
char msgbuf[BUFFER_SIZE], buffer[BUFFER_SIZE];
|
||||
char msgbuf[100], buffer[BUFFER_SIZE];
|
||||
char *ptr = buffer;
|
||||
struct timeval tv;
|
||||
|
||||
@@ -339,8 +339,8 @@ QPair<QString, QNetworkInterface> NetworkUtilities::getGatewayAndIface()
|
||||
nlh = (struct nlmsghdr *) ptr;
|
||||
|
||||
/* Check if the header is valid */
|
||||
if((NLMSG_OK(nlmsg, received_bytes) == 0) ||
|
||||
(nlmsg->nlmsg_type == NLMSG_ERROR))
|
||||
if((NLMSG_OK(nlh, received_bytes) == 0) ||
|
||||
(nlh->nlmsg_type == NLMSG_ERROR))
|
||||
{
|
||||
perror("Error in received packet");
|
||||
return {};
|
||||
@@ -355,13 +355,15 @@ QPair<QString, QNetworkInterface> NetworkUtilities::getGatewayAndIface()
|
||||
}
|
||||
|
||||
/* Break if its not a multi part message */
|
||||
if ((nlmsg->nlmsg_flags & NLM_F_MULTI) == 0)
|
||||
if ((nlh->nlmsg_flags & NLM_F_MULTI) == 0)
|
||||
break;
|
||||
}
|
||||
while ((nlmsg->nlmsg_seq != msgseq) || (nlmsg->nlmsg_pid != getpid()));
|
||||
while ((nlh->nlmsg_seq != msgseq) || (nlh->nlmsg_pid != getpid()));
|
||||
|
||||
/* parse response */
|
||||
for ( ; NLMSG_OK(nlh, received_bytes); nlh = NLMSG_NEXT(nlh, received_bytes))
|
||||
int remaining = msg_len + received_bytes;
|
||||
nlh = (struct nlmsghdr *) buffer;
|
||||
for ( ; NLMSG_OK(nlh, remaining); nlh = NLMSG_NEXT(nlh, remaining))
|
||||
{
|
||||
/* Get the route data */
|
||||
route_entry = (struct rtmsg *) NLMSG_DATA(nlh);
|
||||
@@ -370,6 +372,10 @@ QPair<QString, QNetworkInterface> NetworkUtilities::getGatewayAndIface()
|
||||
if (route_entry->rtm_table != RT_TABLE_MAIN)
|
||||
continue;
|
||||
|
||||
/* Reset per-route to avoid cross-route state pollution */
|
||||
memset(gateway_address, 0, sizeof(gateway_address));
|
||||
memset(interface, 0, sizeof(interface));
|
||||
|
||||
route_attribute = (struct rtattr *) RTM_RTA(route_entry);
|
||||
route_attribute_len = RTM_PAYLOAD(nlh);
|
||||
|
||||
@@ -395,6 +401,8 @@ QPair<QString, QNetworkInterface> NetworkUtilities::getGatewayAndIface()
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!(*gateway_address) || !(*interface))
|
||||
qDebug() << "getGatewayAndIface: no gateway found";
|
||||
close(sock);
|
||||
return { gateway_address, QNetworkInterface::interfaceFromName(interface) };
|
||||
#endif
|
||||
|
||||
193
client/core/utils/payloadSender.cpp
Normal file
193
client/core/utils/payloadSender.cpp
Normal file
@@ -0,0 +1,193 @@
|
||||
#include "payloadSender.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QEventLoop>
|
||||
#include <QHostAddress>
|
||||
#include <QJsonObject>
|
||||
#include <QRandomGenerator>
|
||||
#include <QRegularExpression>
|
||||
#include <QTcpSocket>
|
||||
#include <QTimer>
|
||||
#include <QUdpSocket>
|
||||
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/networkUtilities.h"
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr char protoUdp[] = "udp";
|
||||
constexpr char protoTcp[] = "tcp";
|
||||
|
||||
QByteArray randomBytes(int count)
|
||||
{
|
||||
if (count <= 0) {
|
||||
return {};
|
||||
}
|
||||
QByteArray bytes(count, Qt::Uninitialized);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
bytes[i] = static_cast<char>(QRandomGenerator::global()->bounded(256));
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
bool parseEndpoint(const QString &endpoint, QString &host, quint16 &port)
|
||||
{
|
||||
const int separatorIndex = endpoint.lastIndexOf(QLatin1Char(':'));
|
||||
if (separatorIndex <= 0 || separatorIndex == endpoint.size() - 1) {
|
||||
return false;
|
||||
}
|
||||
host = endpoint.left(separatorIndex);
|
||||
bool ok = false;
|
||||
const uint parsedPort = endpoint.mid(separatorIndex + 1).toUInt(&ok);
|
||||
if (!ok || parsedPort == 0 || parsedPort > 65535) {
|
||||
return false;
|
||||
}
|
||||
port = static_cast<quint16>(parsedPort);
|
||||
return true;
|
||||
}
|
||||
|
||||
enum class ExchangeResult { Sent, Matched, Mismatch, Timeout };
|
||||
|
||||
ExchangeResult waitForResponse(QAbstractSocket &socket, const QByteArray &expectedResponse, int timeoutMs)
|
||||
{
|
||||
if (expectedResponse.isEmpty()) {
|
||||
return ExchangeResult::Sent;
|
||||
}
|
||||
|
||||
QByteArray response;
|
||||
QEventLoop loop;
|
||||
QTimer timer;
|
||||
timer.setSingleShot(true);
|
||||
QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);
|
||||
QObject::connect(&socket, &QIODevice::readyRead, &loop, [&]() {
|
||||
response.append(socket.readAll());
|
||||
if (response.size() >= expectedResponse.size()) {
|
||||
loop.quit();
|
||||
}
|
||||
});
|
||||
|
||||
timer.start(timeoutMs);
|
||||
loop.exec();
|
||||
|
||||
if (response.isEmpty()) {
|
||||
return ExchangeResult::Timeout;
|
||||
}
|
||||
return response == expectedResponse ? ExchangeResult::Matched : ExchangeResult::Mismatch;
|
||||
}
|
||||
}
|
||||
|
||||
QByteArray PayloadSender::buildPayload(const QString &tagString)
|
||||
{
|
||||
QByteArray result;
|
||||
if (tagString.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
static const QRegularExpression tagRegExp(QStringLiteral("<([^>]*)>"));
|
||||
QRegularExpressionMatchIterator it = tagRegExp.globalMatch(tagString);
|
||||
while (it.hasNext()) {
|
||||
const QRegularExpressionMatch match = it.next();
|
||||
const QStringList parts = match.captured(1).simplified().split(QLatin1Char(' '), Qt::SkipEmptyParts);
|
||||
if (parts.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const QString tag = parts.at(0).toLower();
|
||||
if (tag == QLatin1String("b")) {
|
||||
if (parts.size() < 2) {
|
||||
continue;
|
||||
}
|
||||
QString hex = parts.at(1);
|
||||
if (hex.startsWith(QLatin1String("0x"), Qt::CaseInsensitive)) {
|
||||
hex = hex.mid(2);
|
||||
}
|
||||
result.append(QByteArray::fromHex(hex.toLatin1()));
|
||||
} else if (tag == QLatin1String("r")) {
|
||||
if (parts.size() < 2) {
|
||||
continue;
|
||||
}
|
||||
bool ok = false;
|
||||
const int count = parts.at(1).toInt(&ok);
|
||||
if (ok) {
|
||||
result.append(randomBytes(count));
|
||||
}
|
||||
} else {
|
||||
qWarning() << "PayloadSender: unknown payload tag" << tag;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void PayloadSender::sendAll(const QJsonArray &sendPayload)
|
||||
{
|
||||
for (int i = 0; i < sendPayload.size(); ++i) {
|
||||
const QJsonValue value = sendPayload.at(i);
|
||||
if (!value.isObject()) {
|
||||
continue;
|
||||
}
|
||||
sendEntry(value.toObject(), i);
|
||||
}
|
||||
}
|
||||
|
||||
void PayloadSender::sendEntry(const QJsonObject &entry, int index)
|
||||
{
|
||||
const QString endpoint = entry.value(configKey::sendPayloadEndpoint).toString();
|
||||
QString host;
|
||||
quint16 port = 0;
|
||||
if (!parseEndpoint(endpoint, host, port)) {
|
||||
qWarning() << "PayloadSender: skipping entry" << index << "- invalid endpoint" << endpoint;
|
||||
return;
|
||||
}
|
||||
|
||||
const QString protocol = entry.value(configKey::sendPayloadProtocol).toString().toLower();
|
||||
const int timeoutMs = entry.value(configKey::sendPayloadTimeoutMs).toInt();
|
||||
const QByteArray payload = buildPayload(entry.value(configKey::sendPayloadData).toString());
|
||||
const QByteArray expectedResponse = buildPayload(entry.value(configKey::sendPayloadExpectedResponse).toString());
|
||||
|
||||
const QString resolvedHost = NetworkUtilities::getIPAddress(host);
|
||||
const QHostAddress hostAddress(resolvedHost.isEmpty() ? host : resolvedHost);
|
||||
|
||||
QUdpSocket udpSocket;
|
||||
QTcpSocket tcpSocket;
|
||||
QAbstractSocket *socket = nullptr;
|
||||
if (protocol == protoUdp) {
|
||||
socket = &udpSocket;
|
||||
if (udpSocket.writeDatagram(payload, hostAddress, port) < 0) {
|
||||
qWarning() << "PayloadSender: entry" << index << "- udp write failed to" << endpoint << udpSocket.errorString();
|
||||
return;
|
||||
}
|
||||
} else if (protocol == protoTcp) {
|
||||
socket = &tcpSocket;
|
||||
tcpSocket.connectToHost(hostAddress, port);
|
||||
if (!tcpSocket.waitForConnected(timeoutMs)) {
|
||||
qWarning() << "PayloadSender: entry" << index << "- tcp connect failed to" << endpoint << tcpSocket.errorString();
|
||||
return;
|
||||
}
|
||||
if (tcpSocket.write(payload) != payload.size()) {
|
||||
qWarning() << "PayloadSender: entry" << index << "- tcp write failed to" << endpoint << tcpSocket.errorString();
|
||||
return;
|
||||
}
|
||||
tcpSocket.flush();
|
||||
} else {
|
||||
qWarning() << "PayloadSender: skipping entry" << index << "- unsupported protocol" << protocol;
|
||||
return;
|
||||
}
|
||||
|
||||
switch (waitForResponse(*socket, expectedResponse, timeoutMs)) {
|
||||
case ExchangeResult::Sent:
|
||||
qInfo() << "PayloadSender: entry" << index << "-" << protocol << "payload sent to" << endpoint;
|
||||
break;
|
||||
case ExchangeResult::Matched:
|
||||
qInfo() << "PayloadSender: entry" << index << "-" << protocol << "expected response received from" << endpoint;
|
||||
break;
|
||||
case ExchangeResult::Mismatch:
|
||||
qWarning() << "PayloadSender: entry" << index << "-" << protocol << "response mismatch from" << endpoint;
|
||||
break;
|
||||
case ExchangeResult::Timeout:
|
||||
qWarning() << "PayloadSender: entry" << index << "-" << protocol << "response timeout from" << endpoint;
|
||||
break;
|
||||
}
|
||||
}
|
||||
18
client/core/utils/payloadSender.h
Normal file
18
client/core/utils/payloadSender.h
Normal file
@@ -0,0 +1,18 @@
|
||||
#ifndef PAYLOADSENDER_H
|
||||
#define PAYLOADSENDER_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QJsonArray>
|
||||
#include <QString>
|
||||
|
||||
class PayloadSender
|
||||
{
|
||||
public:
|
||||
static void sendAll(const QJsonArray &sendPayload);
|
||||
static QByteArray buildPayload(const QString &tagString);
|
||||
|
||||
private:
|
||||
static void sendEntry(const QJsonObject &entry, int index);
|
||||
};
|
||||
|
||||
#endif // PAYLOADSENDER_H
|
||||
@@ -50,6 +50,7 @@ QString amnezia::scriptName(SharedScriptType type)
|
||||
switch (type) {
|
||||
case SharedScriptType::prepare_host: return QLatin1String("prepare_host.sh");
|
||||
case SharedScriptType::install_docker: return QLatin1String("install_docker.sh");
|
||||
case SharedScriptType::install_conntrack: return QLatin1String("install_conntrack.sh");
|
||||
case SharedScriptType::build_container: return QLatin1String("build_container.sh");
|
||||
case SharedScriptType::remove_container: return QLatin1String("remove_container.sh");
|
||||
case SharedScriptType::remove_all_containers: return QLatin1String("remove_all_containers.sh");
|
||||
@@ -366,6 +367,14 @@ amnezia::ScriptVars amnezia::genTelemtVars(const ContainerConfig &containerConfi
|
||||
vars.append({ { "$TELEMT_USE_MIDDLE_PROXY", c.useMiddleProxy ? QLatin1String("true") : QLatin1String("false") } });
|
||||
vars.append({ { "$TELEMT_MASK", c.maskEnabled ? QLatin1String("true") : QLatin1String("false") } });
|
||||
vars.append({ { "$TELEMT_TLS_EMULATION", c.tlsEmulation ? QLatin1String("true") : QLatin1String("false") } });
|
||||
|
||||
QStringList additionalList;
|
||||
for (const QString &s : c.additionalSecrets) {
|
||||
if (!s.isEmpty()) {
|
||||
additionalList << s;
|
||||
}
|
||||
}
|
||||
vars.append({ { "$TELEMT_ADDITIONAL_SECRETS", additionalList.join(QLatin1Char(',')) } });
|
||||
}
|
||||
|
||||
return vars;
|
||||
|
||||
@@ -21,6 +21,7 @@ enum SharedScriptType {
|
||||
// General scripts
|
||||
prepare_host,
|
||||
install_docker,
|
||||
install_conntrack,
|
||||
build_container,
|
||||
remove_container,
|
||||
remove_all_containers,
|
||||
|
||||
@@ -265,6 +265,8 @@ bool Daemon::parseConfig(const QJsonObject& obj, InterfaceConfig& config) {
|
||||
#endif
|
||||
}
|
||||
|
||||
config.m_persistentKeepalive = obj.value("persistentKeepalive").toString();
|
||||
|
||||
config.m_deviceIpv4Address = obj.value("deviceIpv4Address").toString();
|
||||
config.m_deviceIpv6Address = obj.value("deviceIpv6Address").toString();
|
||||
if (config.m_deviceIpv4Address.isNull() &&
|
||||
@@ -390,55 +392,77 @@ bool Daemon::parseConfig(const QJsonObject& obj, InterfaceConfig& config) {
|
||||
|
||||
config.m_killSwitchEnabled = QVariant(obj.value("killSwitchOption").toString()).toBool();
|
||||
|
||||
if (!obj.value("Jc").isNull()) {
|
||||
config.m_junkPacketCount = obj.value("Jc").toString();
|
||||
if (const auto jc = obj.value("Jc"); !jc.isUndefined()) {
|
||||
config.m_junkPacketCount = jc.toString();
|
||||
}
|
||||
if (!obj.value("Jmin").isNull()) {
|
||||
config.m_junkPacketMinSize = obj.value("Jmin").toString();
|
||||
if (const auto jmin = obj.value("Jmin"); !jmin.isUndefined()) {
|
||||
config.m_junkPacketMinSize = jmin.toString();
|
||||
}
|
||||
if (!obj.value("Jmax").isNull()) {
|
||||
config.m_junkPacketMaxSize = obj.value("Jmax").toString();
|
||||
if (const auto jmax = obj.value("Jmax"); !jmax.isUndefined()) {
|
||||
config.m_junkPacketMaxSize = jmax.toString();
|
||||
}
|
||||
if (!obj.value("S1").isNull()) {
|
||||
config.m_initPacketJunkSize = obj.value("S1").toString();
|
||||
if (const auto s1 = obj.value("S1"); !s1.isUndefined()) {
|
||||
config.m_initPacketJunkSize = s1.toString();
|
||||
}
|
||||
if (!obj.value("S2").isNull()) {
|
||||
config.m_responsePacketJunkSize = obj.value("S2").toString();
|
||||
if (const auto s2 = obj.value("S2"); !s2.isUndefined()) {
|
||||
config.m_responsePacketJunkSize = s2.toString();
|
||||
}
|
||||
if (!obj.value("S3").isNull()) {
|
||||
config.m_cookieReplyPacketJunkSize = obj.value("S3").toString();
|
||||
if (const auto s3 = obj.value("S3"); !s3.isUndefined()) {
|
||||
config.m_cookieReplyPacketJunkSize = s3.toString();
|
||||
}
|
||||
if (!obj.value("S4").isNull()) {
|
||||
config.m_transportPacketJunkSize = obj.value("S4").toString();
|
||||
if (const auto s4 = obj.value("S4"); !s4.isUndefined()) {
|
||||
config.m_transportPacketJunkSize = s4.toString();
|
||||
}
|
||||
|
||||
if (!obj.value("H1").isNull()) {
|
||||
config.m_initPacketMagicHeader = obj.value("H1").toString();
|
||||
if (const auto h1 = obj.value("H1"); !h1.isUndefined()) {
|
||||
config.m_initPacketMagicHeader = h1.toString();
|
||||
}
|
||||
if (!obj.value("H2").isNull()) {
|
||||
config.m_responsePacketMagicHeader = obj.value("H2").toString();
|
||||
if (const auto h2 = obj.value("H2"); !h2.isUndefined()) {
|
||||
config.m_responsePacketMagicHeader = h2.toString();
|
||||
}
|
||||
if (!obj.value("H3").isNull()) {
|
||||
config.m_underloadPacketMagicHeader = obj.value("H3").toString();
|
||||
if (const auto h3 = obj.value("H3"); !h3.isUndefined()) {
|
||||
config.m_underloadPacketMagicHeader = h3.toString();
|
||||
}
|
||||
if (!obj.value("H4").isNull()) {
|
||||
config.m_transportPacketMagicHeader = obj.value("H4").toString();
|
||||
if (const auto h4 = obj.value("H4"); !h4.isUndefined()) {
|
||||
config.m_transportPacketMagicHeader = h4.toString();
|
||||
}
|
||||
|
||||
if (!obj.value("I1").isNull()) {
|
||||
config.m_specialJunk["I1"] = obj.value("I1").toString();
|
||||
if (const auto i1 = obj.value("I1"); !i1.isUndefined()) {
|
||||
config.m_specialJunk["I1"] = i1.toString();
|
||||
}
|
||||
if (!obj.value("I2").isNull()) {
|
||||
config.m_specialJunk["I2"] = obj.value("I2").toString();
|
||||
if (const auto i2 = obj.value("I2"); !i2.isUndefined()) {
|
||||
config.m_specialJunk["I2"] = i2.toString();
|
||||
}
|
||||
if (!obj.value("I3").isNull()) {
|
||||
config.m_specialJunk["I3"] = obj.value("I3").toString();
|
||||
if (const auto i3 = obj.value("I3"); !i3.isUndefined()) {
|
||||
config.m_specialJunk["I3"] = i3.toString();
|
||||
}
|
||||
if (!obj.value("I4").isNull()) {
|
||||
config.m_specialJunk["I4"] = obj.value("I4").toString();
|
||||
if (const auto i4 = obj.value("I4"); !i4.isUndefined()) {
|
||||
config.m_specialJunk["I4"] = i4.toString();
|
||||
}
|
||||
if (!obj.value("I5").isNull()) {
|
||||
config.m_specialJunk["I5"] = obj.value("I5").toString();
|
||||
if (const auto i5 = obj.value("I5"); !i5.isUndefined()) {
|
||||
config.m_specialJunk["I5"] = i5.toString();
|
||||
}
|
||||
|
||||
if (const auto headerProtectionKey = obj.value("HeaderProtectionKey"); !headerProtectionKey.isUndefined()) {
|
||||
config.m_headerProtectionKey = headerProtectionKey.toString();
|
||||
}
|
||||
if (const auto contentPaddingAddition = obj.value("ContentPaddingAddition"); !contentPaddingAddition.isUndefined()) {
|
||||
config.m_contentPaddingAddition = contentPaddingAddition.toString();
|
||||
}
|
||||
if (const auto rekeyAfterTime = obj.value("RekeyAfterTime"); !rekeyAfterTime.isUndefined()) {
|
||||
config.m_rekeyAfterTime = rekeyAfterTime.toString();
|
||||
}
|
||||
if (const auto rekeyTimeout = obj.value("RekeyTimeout"); !rekeyTimeout.isUndefined()) {
|
||||
config.m_rekeyTimeout = rekeyTimeout.toString();
|
||||
}
|
||||
if (const auto rejectAfterTime = obj.value("RejectAfterTime"); !rejectAfterTime.isUndefined()) {
|
||||
config.m_rejectAfterTime = rejectAfterTime.toString();
|
||||
}
|
||||
if (const auto keepaliveTimeout = obj.value("KeepaliveTimeout"); !keepaliveTimeout.isUndefined()) {
|
||||
config.m_keepaliveTimeout = keepaliveTimeout.toString();
|
||||
}
|
||||
if (const auto maxHandshakeAttempts = obj.value("MaxHandshakeAttempts"); !maxHandshakeAttempts.isUndefined()) {
|
||||
config.m_maxHandshakeAttempts = maxHandshakeAttempts.toString();
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -24,6 +24,9 @@ QJsonObject InterfaceConfig::toJson() const {
|
||||
json.insert("serverIpv6AddrIn", QJsonValue(m_serverIpv6AddrIn));
|
||||
json.insert("serverPort", QJsonValue((double)m_serverPort));
|
||||
json.insert("deviceMTU", QJsonValue(m_deviceMTU));
|
||||
if (!m_persistentKeepalive.isEmpty()) {
|
||||
json.insert("persistentKeepalive", QJsonValue(m_persistentKeepalive));
|
||||
}
|
||||
if ((m_hopType == InterfaceConfig::MultiHopExit) ||
|
||||
(m_hopType == InterfaceConfig::SingleHop)) {
|
||||
json.insert("serverIpv4Gateway", QJsonValue(m_serverIpv4Gateway));
|
||||
@@ -153,6 +156,28 @@ QString InterfaceConfig::toWgConf(const QMap<QString, QString>& extra) const {
|
||||
out << key << " = " << m_specialJunk[key] << "\n";
|
||||
}
|
||||
|
||||
if (!m_headerProtectionKey.isEmpty()) {
|
||||
out << "HeaderProtectionKey = " << m_headerProtectionKey << "\n";
|
||||
}
|
||||
if (!m_contentPaddingAddition.isEmpty()) {
|
||||
out << "ContentPaddingAddition = " << m_contentPaddingAddition << "\n";
|
||||
}
|
||||
if (!m_rekeyAfterTime.isEmpty()) {
|
||||
out << "RekeyAfterTime = " << m_rekeyAfterTime << "\n";
|
||||
}
|
||||
if (!m_rekeyTimeout.isEmpty()) {
|
||||
out << "RekeyTimeout = " << m_rekeyTimeout << "\n";
|
||||
}
|
||||
if (!m_rejectAfterTime.isEmpty()) {
|
||||
out << "RejectAfterTime = " << m_rejectAfterTime << "\n";
|
||||
}
|
||||
if (!m_keepaliveTimeout.isEmpty()) {
|
||||
out << "KeepaliveTimeout = " << m_keepaliveTimeout << "\n";
|
||||
}
|
||||
if (!m_maxHandshakeAttempts.isEmpty()) {
|
||||
out << "MaxHandshakeAttempts = " << m_maxHandshakeAttempts << "\n";
|
||||
}
|
||||
|
||||
// If any extra config was provided, append it now.
|
||||
for (const QString& key : extra.keys()) {
|
||||
out << key << " = " << extra[key] << "\n";
|
||||
@@ -173,6 +198,9 @@ QString InterfaceConfig::toWgConf(const QMap<QString, QString>& extra) const {
|
||||
ranges.append(ip.toString());
|
||||
}
|
||||
out << "AllowedIPs = " << ranges.join(", ") << "\n";
|
||||
if (!m_persistentKeepalive.isEmpty()) {
|
||||
out << "PersistentKeepalive = " << m_persistentKeepalive << "\n";
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ class InterfaceConfig {
|
||||
QString m_secondaryDnsServer;
|
||||
int m_serverPort = 0;
|
||||
int m_deviceMTU = 1420;
|
||||
QString m_persistentKeepalive;
|
||||
QList<IPAddress> m_allowedIPAddressRanges;
|
||||
QStringList m_excludedAddresses;
|
||||
QStringList m_vpnDisabledApps;
|
||||
@@ -58,6 +59,14 @@ class InterfaceConfig {
|
||||
QString m_transportPacketMagicHeader;
|
||||
QMap<QString, QString> m_specialJunk;
|
||||
|
||||
QString m_headerProtectionKey;
|
||||
QString m_contentPaddingAddition;
|
||||
QString m_rekeyAfterTime;
|
||||
QString m_rekeyTimeout;
|
||||
QString m_rejectAfterTime;
|
||||
QString m_keepaliveTimeout;
|
||||
QString m_maxHandshakeAttempts;
|
||||
|
||||
QJsonObject toJson() const;
|
||||
QString toWgConf(
|
||||
const QMap<QString, QString>& extra = QMap<QString, QString>()) const;
|
||||
|
||||
@@ -16,8 +16,6 @@
|
||||
|
||||
constexpr const char* WG_INTERFACE = "amn0";
|
||||
|
||||
constexpr uint16_t WG_KEEPALIVE_PERIOD = 60;
|
||||
|
||||
class WireguardUtils : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
|
||||
6
client/images/controls/arrow-left-right.svg
Normal file
6
client/images/controls/arrow-left-right.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17 11L21 7L17 3" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M21 7H9" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7 21L3 17L7 13" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M15 17H3" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 525 B |
@@ -10,6 +10,7 @@
|
||||
<file>controls/app.svg</file>
|
||||
<file>controls/archive-restore.svg</file>
|
||||
<file>controls/arrow-left.svg</file>
|
||||
<file>controls/arrow-left-right.svg</file>
|
||||
<file>controls/arrow-right.svg</file>
|
||||
<file>controls/bug.svg</file>
|
||||
<file>controls/check.svg</file>
|
||||
|
||||
@@ -82,10 +82,32 @@
|
||||
<key>NSAllowsLocalNetworking</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>CFBundleIconName</key>
|
||||
<string>AppIcon</string>
|
||||
<key>CFBundleIcons</key>
|
||||
<dict/>
|
||||
<key>CFBundleIcons~ipad</key>
|
||||
<dict/>
|
||||
<dict>
|
||||
<key>CFBundlePrimaryIcon</key>
|
||||
<dict>
|
||||
<key>CFBundleIconFiles</key>
|
||||
<array>
|
||||
<string>AppIcon</string>
|
||||
</array>
|
||||
<key>CFBundleIconName</key>
|
||||
<string>AppIcon</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>CFBundleIcons~ipad</key>
|
||||
<dict>
|
||||
<key>CFBundlePrimaryIcon</key>
|
||||
<dict>
|
||||
<key>CFBundleIconFiles</key>
|
||||
<array>
|
||||
<string>AppIcon</string>
|
||||
</array>
|
||||
<key>CFBundleIconName</key>
|
||||
<string>AppIcon</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>UTImportedTypeDeclarations</key>
|
||||
<array>
|
||||
<dict>
|
||||
|
||||
@@ -44,8 +44,20 @@
|
||||
<key>NSAllowsLocalNetworking</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>CFBundleIconName</key>
|
||||
<string>AppIcon</string>
|
||||
<key>CFBundleIcons</key>
|
||||
<dict/>
|
||||
<dict>
|
||||
<key>CFBundlePrimaryIcon</key>
|
||||
<dict>
|
||||
<key>CFBundleIconFiles</key>
|
||||
<array>
|
||||
<string>AppIcon</string>
|
||||
</array>
|
||||
<key>CFBundleIconName</key>
|
||||
<string>AppIcon</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>UTImportedTypeDeclarations</key>
|
||||
<array>
|
||||
<dict>
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
#include <QDebug>
|
||||
#include <QTimer>
|
||||
#include <libssh/libssh.h>
|
||||
#include <openssl/ssl.h>
|
||||
|
||||
#include "amneziaApplication.h"
|
||||
#include "core/utils/osSignalHandler.h"
|
||||
#include "core/utils/migrations.h"
|
||||
#include "version.h"
|
||||
|
||||
|
||||
// use openssl symbols to prevent linker throwing-off the OpenSSL dependency
|
||||
void anchorOpenSSL() {
|
||||
SSL_CTX_free(SSL_CTX_new(TLS_method()));
|
||||
}
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
#include "Windows.h"
|
||||
#endif
|
||||
@@ -46,6 +53,8 @@ int main(int argc, char *argv[])
|
||||
AmneziaApplication app(argc, argv);
|
||||
OsSignalHandler::setup();
|
||||
|
||||
anchorOpenSSL();
|
||||
|
||||
ssh_init();
|
||||
QObject::connect(&app, &QCoreApplication::aboutToQuit, []() {
|
||||
ssh_finalize();
|
||||
|
||||
@@ -159,6 +159,11 @@ void LocalSocketController::activate(const QJsonObject &rawConfig) {
|
||||
json.insert("serverIpv4Gateway", wgConfig.value(amnezia::configKey::hostName));
|
||||
// json.insert("serverIpv6Gateway", QJsonValue(hop.m_server.ipv6Gateway()));
|
||||
|
||||
if (wgConfig.contains(amnezia::configKey::persistentKeepAlive)) {
|
||||
json.insert("persistentKeepalive",
|
||||
wgConfig.value(amnezia::configKey::persistentKeepAlive).toString());
|
||||
}
|
||||
|
||||
json.insert("primaryDnsServer", rawConfig.value(amnezia::configKey::dns1));
|
||||
|
||||
// We don't use secondary DNS if primary DNS is AmneziaDNS
|
||||
@@ -263,6 +268,13 @@ void LocalSocketController::activate(const QJsonObject &rawConfig) {
|
||||
json.insert(amnezia::configKey::specialJunk3, wgConfig.value(amnezia::configKey::specialJunk3));
|
||||
json.insert(amnezia::configKey::specialJunk4, wgConfig.value(amnezia::configKey::specialJunk4));
|
||||
json.insert(amnezia::configKey::specialJunk5, wgConfig.value(amnezia::configKey::specialJunk5));
|
||||
json.insert(amnezia::configKey::headerProtectionKey, wgConfig.value(amnezia::configKey::headerProtectionKey));
|
||||
json.insert(amnezia::configKey::contentPaddingAddition, wgConfig.value(amnezia::configKey::contentPaddingAddition));
|
||||
json.insert(amnezia::configKey::rekeyAfterTime, wgConfig.value(amnezia::configKey::rekeyAfterTime));
|
||||
json.insert(amnezia::configKey::rekeyTimeout, wgConfig.value(amnezia::configKey::rekeyTimeout));
|
||||
json.insert(amnezia::configKey::rejectAfterTime, wgConfig.value(amnezia::configKey::rejectAfterTime));
|
||||
json.insert(amnezia::configKey::keepaliveTimeout, wgConfig.value(amnezia::configKey::keepaliveTimeout));
|
||||
json.insert(amnezia::configKey::maxHandshakeAttempts, wgConfig.value(amnezia::configKey::maxHandshakeAttempts));
|
||||
} else if (!wgConfig.value(amnezia::configKey::junkPacketCount).isUndefined()
|
||||
&& !wgConfig.value(amnezia::configKey::junkPacketMinSize).isUndefined()
|
||||
&& !wgConfig.value(amnezia::configKey::junkPacketMaxSize).isUndefined()
|
||||
@@ -290,6 +302,13 @@ void LocalSocketController::activate(const QJsonObject &rawConfig) {
|
||||
json.insert(amnezia::configKey::specialJunk3, wgConfig.value(amnezia::configKey::specialJunk3));
|
||||
json.insert(amnezia::configKey::specialJunk4, wgConfig.value(amnezia::configKey::specialJunk4));
|
||||
json.insert(amnezia::configKey::specialJunk5, wgConfig.value(amnezia::configKey::specialJunk5));
|
||||
json.insert(amnezia::configKey::headerProtectionKey, wgConfig.value(amnezia::configKey::headerProtectionKey));
|
||||
json.insert(amnezia::configKey::contentPaddingAddition, wgConfig.value(amnezia::configKey::contentPaddingAddition));
|
||||
json.insert(amnezia::configKey::rekeyAfterTime, wgConfig.value(amnezia::configKey::rekeyAfterTime));
|
||||
json.insert(amnezia::configKey::rekeyTimeout, wgConfig.value(amnezia::configKey::rekeyTimeout));
|
||||
json.insert(amnezia::configKey::rejectAfterTime, wgConfig.value(amnezia::configKey::rejectAfterTime));
|
||||
json.insert(amnezia::configKey::keepaliveTimeout, wgConfig.value(amnezia::configKey::keepaliveTimeout));
|
||||
json.insert(amnezia::configKey::maxHandshakeAttempts, wgConfig.value(amnezia::configKey::maxHandshakeAttempts));
|
||||
}
|
||||
|
||||
write(json);
|
||||
|
||||
@@ -154,6 +154,28 @@ void AndroidController::resetLastServer(int serverIndex)
|
||||
callActivityMethod("resetLastServer", "(I)V", serverIndex);
|
||||
}
|
||||
|
||||
void AndroidController::showUpdateCover()
|
||||
{
|
||||
callActivityMethod("showUpdateCover", "()V");
|
||||
}
|
||||
|
||||
void AndroidController::hideUpdateCover()
|
||||
{
|
||||
callActivityMethod("hideUpdateCover", "()V");
|
||||
}
|
||||
|
||||
void AndroidController::showUpdatePrompt(const QString &title, const QString &message, const QString &updateTitle,
|
||||
const QString &skipTitle, const QString &storeUrl)
|
||||
{
|
||||
callActivityMethod("showUpdatePrompt",
|
||||
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V",
|
||||
QJniObject::fromString(title).object<jstring>(),
|
||||
QJniObject::fromString(message).object<jstring>(),
|
||||
QJniObject::fromString(updateTitle).object<jstring>(),
|
||||
QJniObject::fromString(skipTitle).object<jstring>(),
|
||||
QJniObject::fromString(storeUrl).object<jstring>());
|
||||
}
|
||||
|
||||
void AndroidController::saveFile(const QString &fileName, const QString &data)
|
||||
{
|
||||
callActivityMethod("saveFile", "(Ljava/lang/String;Ljava/lang/String;)V",
|
||||
|
||||
@@ -56,6 +56,11 @@ public:
|
||||
bool requestAuthentication();
|
||||
void sendTouch(float x, float y);
|
||||
|
||||
void showUpdateCover();
|
||||
void hideUpdateCover();
|
||||
void showUpdatePrompt(const QString &title, const QString &message, const QString &updateTitle,
|
||||
const QString &skipTitle, const QString &storeUrl);
|
||||
|
||||
static bool initLogging();
|
||||
static void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &message);
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ struct WGConfig: Decodable {
|
||||
let junkPacketCount, junkPacketMinSize, junkPacketMaxSize: String?
|
||||
let initPacketJunkSize, responsePacketJunkSize, cookieReplyPacketJunkSize, transportPacketJunkSize: String?
|
||||
let specialJunk1, specialJunk2, specialJunk3, specialJunk4, specialJunk5: String?
|
||||
let headerProtectionKey: String?
|
||||
let contentPaddingAddition, rekeyAfterTime, rekeyTimeout, rejectAfterTime, keepaliveTimeout, maxHandshakeAttempts: String?
|
||||
let dns1: String
|
||||
let dns2: String
|
||||
let mtu: String
|
||||
@@ -16,7 +18,7 @@ struct WGConfig: Decodable {
|
||||
let serverPublicKey: String
|
||||
let presharedKey: String?
|
||||
var allowedIPs: [String]
|
||||
var persistentKeepAlive: String
|
||||
var persistentKeepAlive: String?
|
||||
let splitTunnelType: Int
|
||||
let splitTunnelSites: [String]
|
||||
|
||||
@@ -26,6 +28,11 @@ struct WGConfig: Decodable {
|
||||
case junkPacketCount = "Jc", junkPacketMinSize = "Jmin", junkPacketMaxSize = "Jmax"
|
||||
case initPacketJunkSize = "S1", responsePacketJunkSize = "S2", cookieReplyPacketJunkSize = "S3", transportPacketJunkSize = "S4"
|
||||
case specialJunk1 = "I1", specialJunk2 = "I2", specialJunk3 = "I3", specialJunk4 = "I4", specialJunk5 = "I5"
|
||||
case headerProtectionKey = "HeaderProtectionKey"
|
||||
case contentPaddingAddition = "ContentPaddingAddition"
|
||||
case rekeyAfterTime = "RekeyAfterTime", rekeyTimeout = "RekeyTimeout"
|
||||
case rejectAfterTime = "RejectAfterTime", keepaliveTimeout = "KeepaliveTimeout"
|
||||
case maxHandshakeAttempts = "MaxHandshakeAttempts"
|
||||
case dns1
|
||||
case dns2
|
||||
case mtu
|
||||
@@ -100,6 +107,28 @@ struct WGConfig: Decodable {
|
||||
settingsLines.append("I5 = \(i5)")
|
||||
}
|
||||
|
||||
if let headerProtectionKey = trimmed(headerProtectionKey) {
|
||||
settingsLines.append("HeaderProtectionKey = \(headerProtectionKey)")
|
||||
}
|
||||
if let contentPaddingAddition = trimmed(contentPaddingAddition) {
|
||||
settingsLines.append("ContentPaddingAddition = \(contentPaddingAddition)")
|
||||
}
|
||||
if let rekeyAfterTime = trimmed(rekeyAfterTime) {
|
||||
settingsLines.append("RekeyAfterTime = \(rekeyAfterTime)")
|
||||
}
|
||||
if let rekeyTimeout = trimmed(rekeyTimeout) {
|
||||
settingsLines.append("RekeyTimeout = \(rekeyTimeout)")
|
||||
}
|
||||
if let rejectAfterTime = trimmed(rejectAfterTime) {
|
||||
settingsLines.append("RejectAfterTime = \(rejectAfterTime)")
|
||||
}
|
||||
if let keepaliveTimeout = trimmed(keepaliveTimeout) {
|
||||
settingsLines.append("KeepaliveTimeout = \(keepaliveTimeout)")
|
||||
}
|
||||
if let maxHandshakeAttempts = trimmed(maxHandshakeAttempts) {
|
||||
settingsLines.append("MaxHandshakeAttempts = \(maxHandshakeAttempts)")
|
||||
}
|
||||
|
||||
return settingsLines.joined(separator: "\n")
|
||||
}
|
||||
|
||||
@@ -116,7 +145,7 @@ struct WGConfig: Decodable {
|
||||
\(presharedKey == nil ? "" : "PresharedKey = \(presharedKey!)")
|
||||
AllowedIPs = \(allowedIPs.joined(separator: ", "))
|
||||
Endpoint = \(hostName):\(port)
|
||||
PersistentKeepalive = \(persistentKeepAlive)
|
||||
\(persistentKeepAlive == nil ? "" : "PersistentKeepalive = \(persistentKeepAlive!)")
|
||||
"""
|
||||
}
|
||||
|
||||
@@ -133,7 +162,7 @@ struct WGConfig: Decodable {
|
||||
PresharedKey = ***
|
||||
AllowedIPs = \(allowedIPs.joined(separator: ", "))
|
||||
Endpoint = \(hostName):\(port)
|
||||
PersistentKeepalive = \(persistentKeepAlive)
|
||||
\(persistentKeepAlive == nil ? "" : "PersistentKeepalive = \(persistentKeepAlive!)")
|
||||
|
||||
SplitTunnelType = \(splitTunnelType)
|
||||
SplitTunnelSites = \(splitTunnelSites.joined(separator: ", "))
|
||||
|
||||
@@ -80,6 +80,11 @@ public:
|
||||
|
||||
void requestInetAccess();
|
||||
bool isTestFlight();
|
||||
|
||||
void showUpdateCover();
|
||||
void hideUpdateCover();
|
||||
void showUpdatePrompt(const QString &title, const QString &message, const QString &updateTitle,
|
||||
const QString &skipTitle, const QString &storeUrl);
|
||||
signals:
|
||||
void connectionStateChanged(Vpn::ConnectionState state);
|
||||
void bytesChanged(quint64 receivedBytes, quint64 sentBytes);
|
||||
|
||||
@@ -591,8 +591,6 @@ bool IosController::setupWireGuard()
|
||||
|
||||
if (config.contains(configKey::persistentKeepAlive)) {
|
||||
wgConfig.insert(configKey::persistentKeepAlive, config[configKey::persistentKeepAlive]);
|
||||
} else {
|
||||
wgConfig.insert(configKey::persistentKeepAlive, "25");
|
||||
}
|
||||
|
||||
if (config.contains(configKey::isObfuscationEnabled) && config.value(configKey::isObfuscationEnabled).toBool()) {
|
||||
@@ -609,6 +607,14 @@ bool IosController::setupWireGuard()
|
||||
wgConfig.insert(configKey::junkPacketCount, config[configKey::junkPacketCount]);
|
||||
wgConfig.insert(configKey::junkPacketMinSize, config[configKey::junkPacketMinSize]);
|
||||
wgConfig.insert(configKey::junkPacketMaxSize, config[configKey::junkPacketMaxSize]);
|
||||
|
||||
wgConfig.insert(configKey::headerProtectionKey, config[configKey::headerProtectionKey]);
|
||||
wgConfig.insert(configKey::contentPaddingAddition, config[configKey::contentPaddingAddition]);
|
||||
wgConfig.insert(configKey::rekeyAfterTime, config[configKey::rekeyAfterTime]);
|
||||
wgConfig.insert(configKey::rekeyTimeout, config[configKey::rekeyTimeout]);
|
||||
wgConfig.insert(configKey::rejectAfterTime, config[configKey::rejectAfterTime]);
|
||||
wgConfig.insert(configKey::keepaliveTimeout, config[configKey::keepaliveTimeout]);
|
||||
wgConfig.insert(configKey::maxHandshakeAttempts, config[configKey::maxHandshakeAttempts]);
|
||||
}
|
||||
|
||||
QJsonDocument wgConfigDoc(wgConfig);
|
||||
@@ -697,8 +703,6 @@ bool IosController::setupAwg()
|
||||
|
||||
if (config.contains(configKey::persistentKeepAlive)) {
|
||||
wgConfig.insert(configKey::persistentKeepAlive, config[configKey::persistentKeepAlive]);
|
||||
} else {
|
||||
wgConfig.insert(configKey::persistentKeepAlive, "25");
|
||||
}
|
||||
|
||||
wgConfig.insert(configKey::initPacketMagicHeader, config[configKey::initPacketMagicHeader]);
|
||||
@@ -721,6 +725,14 @@ bool IosController::setupAwg()
|
||||
wgConfig.insert(configKey::specialJunk4, config[configKey::specialJunk4]);
|
||||
wgConfig.insert(configKey::specialJunk5, config[configKey::specialJunk5]);
|
||||
|
||||
wgConfig.insert(configKey::headerProtectionKey, config[configKey::headerProtectionKey]);
|
||||
wgConfig.insert(configKey::contentPaddingAddition, config[configKey::contentPaddingAddition]);
|
||||
wgConfig.insert(configKey::rekeyAfterTime, config[configKey::rekeyAfterTime]);
|
||||
wgConfig.insert(configKey::rekeyTimeout, config[configKey::rekeyTimeout]);
|
||||
wgConfig.insert(configKey::rejectAfterTime, config[configKey::rejectAfterTime]);
|
||||
wgConfig.insert(configKey::keepaliveTimeout, config[configKey::keepaliveTimeout]);
|
||||
wgConfig.insert(configKey::maxHandshakeAttempts, config[configKey::maxHandshakeAttempts]);
|
||||
|
||||
QJsonDocument wgConfigDoc(wgConfig);
|
||||
QString wgConfigDocStr(wgConfigDoc.toJson(QJsonDocument::Compact));
|
||||
|
||||
@@ -1200,3 +1212,138 @@ bool IosController::isTestFlight() {
|
||||
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
|
||||
return receiptURL && [[receiptURL lastPathComponent] isEqualToString:@"sandboxReceipt"];
|
||||
}
|
||||
|
||||
#if !MACOS_NE
|
||||
static UIWindow *s_updateCoverWindow = nil;
|
||||
|
||||
static UIWindowScene *activeWindowScene() {
|
||||
UIWindowScene *fallback = nil;
|
||||
for (UIScene *scene in [UIApplication sharedApplication].connectedScenes) {
|
||||
if (![scene isKindOfClass:[UIWindowScene class]]) {
|
||||
continue;
|
||||
}
|
||||
fallback = (UIWindowScene *)scene;
|
||||
if (scene.activationState == UISceneActivationStateForegroundActive) {
|
||||
return (UIWindowScene *)scene;
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
#endif
|
||||
|
||||
void IosController::showUpdateCover() {
|
||||
#if !MACOS_NE
|
||||
void (^build)(void) = ^{
|
||||
if (s_updateCoverWindow) {
|
||||
return;
|
||||
}
|
||||
UIWindowScene *scene = activeWindowScene();
|
||||
if (!scene) {
|
||||
return;
|
||||
}
|
||||
UIWindow *win = [[UIWindow alloc] initWithWindowScene:scene];
|
||||
win.windowLevel = UIWindowLevelAlert + 1;
|
||||
UIViewController *vc = [[[UIViewController alloc] init] autorelease];
|
||||
vc.view.backgroundColor = [UIColor colorWithRed:0.055 green:0.055 blue:0.063 alpha:1.0];
|
||||
win.rootViewController = vc;
|
||||
[win makeKeyAndVisible];
|
||||
s_updateCoverWindow = win;
|
||||
};
|
||||
|
||||
if ([NSThread isMainThread]) {
|
||||
build();
|
||||
} else {
|
||||
dispatch_sync(dispatch_get_main_queue(), build);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void IosController::hideUpdateCover() {
|
||||
#if !MACOS_NE
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (!s_updateCoverWindow) {
|
||||
return;
|
||||
}
|
||||
s_updateCoverWindow.hidden = YES;
|
||||
[s_updateCoverWindow release];
|
||||
s_updateCoverWindow = nil;
|
||||
});
|
||||
#endif
|
||||
}
|
||||
|
||||
void IosController::showUpdatePrompt(const QString &title, const QString &message, const QString &updateTitle,
|
||||
const QString &skipTitle, const QString &storeUrl) {
|
||||
#if !MACOS_NE
|
||||
NSString *nsTitle = title.toNSString();
|
||||
NSString *nsMessage = message.toNSString();
|
||||
NSString *nsUpdate = updateTitle.toNSString();
|
||||
NSString *nsSkip = skipTitle.toNSString();
|
||||
NSString *nsUrl = storeUrl.toNSString();
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (!s_updateCoverWindow) {
|
||||
return;
|
||||
}
|
||||
UIViewController *vc = s_updateCoverWindow.rootViewController;
|
||||
|
||||
void (^dismissCover)(void) = ^{
|
||||
s_updateCoverWindow.hidden = YES;
|
||||
[s_updateCoverWindow release];
|
||||
s_updateCoverWindow = nil;
|
||||
};
|
||||
|
||||
UILabel *titleLabel = [[[UILabel alloc] init] autorelease];
|
||||
titleLabel.text = nsTitle;
|
||||
titleLabel.font = [UIFont boldSystemFontOfSize:22];
|
||||
titleLabel.textColor = [UIColor whiteColor];
|
||||
titleLabel.textAlignment = NSTextAlignmentCenter;
|
||||
titleLabel.numberOfLines = 0;
|
||||
|
||||
UILabel *messageLabel = [[[UILabel alloc] init] autorelease];
|
||||
messageLabel.text = nsMessage;
|
||||
messageLabel.font = [UIFont systemFontOfSize:16];
|
||||
messageLabel.textColor = [UIColor colorWithWhite:0.78 alpha:1.0];
|
||||
messageLabel.textAlignment = NSTextAlignmentCenter;
|
||||
messageLabel.numberOfLines = 0;
|
||||
|
||||
UIButton *updateButton = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
[updateButton setTitle:nsUpdate forState:UIControlStateNormal];
|
||||
[updateButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
|
||||
updateButton.backgroundColor = [UIColor colorWithRed:1.0 green:0.6 blue:0.0 alpha:1.0];
|
||||
updateButton.titleLabel.font = [UIFont systemFontOfSize:17 weight:UIFontWeightSemibold];
|
||||
updateButton.layer.cornerRadius = 12;
|
||||
[updateButton.heightAnchor constraintEqualToConstant:52].active = YES;
|
||||
[updateButton addAction:[UIAction actionWithHandler:^(__kindof UIAction *action) {
|
||||
NSURL *url = [NSURL URLWithString:nsUrl];
|
||||
if (url) {
|
||||
[[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil];
|
||||
}
|
||||
dismissCover();
|
||||
}] forControlEvents:UIControlEventTouchUpInside];
|
||||
|
||||
UIButton *skipButton = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
[skipButton setTitle:nsSkip forState:UIControlStateNormal];
|
||||
[skipButton setTitleColor:[UIColor colorWithWhite:0.7 alpha:1.0] forState:UIControlStateNormal];
|
||||
skipButton.titleLabel.font = [UIFont systemFontOfSize:17];
|
||||
[skipButton.heightAnchor constraintEqualToConstant:44].active = YES;
|
||||
[skipButton addAction:[UIAction actionWithHandler:^(__kindof UIAction *action) {
|
||||
dismissCover();
|
||||
}] forControlEvents:UIControlEventTouchUpInside];
|
||||
|
||||
UIStackView *stack = [[[UIStackView alloc] initWithArrangedSubviews:@[titleLabel, messageLabel, updateButton, skipButton]] autorelease];
|
||||
stack.axis = UILayoutConstraintAxisVertical;
|
||||
stack.spacing = 16;
|
||||
stack.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[stack setCustomSpacing:28 afterView:messageLabel];
|
||||
[vc.view addSubview:stack];
|
||||
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
[stack.centerYAnchor constraintEqualToAnchor:vc.view.centerYAnchor],
|
||||
[stack.leadingAnchor constraintEqualToAnchor:vc.view.leadingAnchor constant:32],
|
||||
[stack.trailingAnchor constraintEqualToAnchor:vc.view.trailingAnchor constant:-32]
|
||||
]];
|
||||
});
|
||||
#else
|
||||
Q_UNUSED(title) Q_UNUSED(message) Q_UNUSED(updateTitle) Q_UNUSED(skipTitle) Q_UNUSED(storeUrl)
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -144,6 +144,30 @@ bool WireguardUtilsLinux::addInterface(const InterfaceConfig& config) {
|
||||
out << key.toLower() << "=" << config.m_specialJunk.value(key) << "\n";
|
||||
}
|
||||
|
||||
if (!config.m_headerProtectionKey.isEmpty()) {
|
||||
QByteArray headerProtectionKey =
|
||||
QByteArray::fromBase64(config.m_headerProtectionKey.toUtf8());
|
||||
out << "header_protection_key=" << QString(headerProtectionKey.toHex()) << "\n";
|
||||
}
|
||||
if (!config.m_contentPaddingAddition.isEmpty()) {
|
||||
out << "content_padding_addition=" << config.m_contentPaddingAddition << "\n";
|
||||
}
|
||||
if (!config.m_rekeyAfterTime.isEmpty()) {
|
||||
out << "rekey_after_time=" << config.m_rekeyAfterTime << "\n";
|
||||
}
|
||||
if (!config.m_rekeyTimeout.isEmpty()) {
|
||||
out << "rekey_timeout=" << config.m_rekeyTimeout << "\n";
|
||||
}
|
||||
if (!config.m_rejectAfterTime.isEmpty()) {
|
||||
out << "reject_after_time=" << config.m_rejectAfterTime << "\n";
|
||||
}
|
||||
if (!config.m_keepaliveTimeout.isEmpty()) {
|
||||
out << "keepalive_timeout=" << config.m_keepaliveTimeout << "\n";
|
||||
}
|
||||
if (!config.m_maxHandshakeAttempts.isEmpty()) {
|
||||
out << "max_handshake_attempts=" << config.m_maxHandshakeAttempts << "\n";
|
||||
}
|
||||
|
||||
int err = uapiErrno(uapiCommand(message));
|
||||
if (err != 0) {
|
||||
logger.error() << "Interface configuration failed:" << strerror(err);
|
||||
@@ -229,7 +253,9 @@ bool WireguardUtilsLinux::updatePeer(const InterfaceConfig& config) {
|
||||
out << config.m_serverPort << "\n";
|
||||
|
||||
out << "replace_allowed_ips=true\n";
|
||||
out << "persistent_keepalive_interval=" << WG_KEEPALIVE_PERIOD << "\n";
|
||||
if (!config.m_persistentKeepalive.isEmpty()) {
|
||||
out << "persistent_keepalive_interval=" << config.m_persistentKeepalive << "\n";
|
||||
}
|
||||
for (const IPAddress& ip : config.m_allowedIPAddressRanges) {
|
||||
out << "allowed_ip=" << ip.toString() << "\n";
|
||||
}
|
||||
|
||||
@@ -142,6 +142,30 @@ bool WireguardUtilsMacos::addInterface(const InterfaceConfig& config) {
|
||||
out << key.toLower() << "=" << config.m_specialJunk.value(key) << "\n";
|
||||
}
|
||||
|
||||
if (!config.m_headerProtectionKey.isEmpty()) {
|
||||
QByteArray headerProtectionKey =
|
||||
QByteArray::fromBase64(config.m_headerProtectionKey.toUtf8());
|
||||
out << "header_protection_key=" << QString(headerProtectionKey.toHex()) << "\n";
|
||||
}
|
||||
if (!config.m_contentPaddingAddition.isEmpty()) {
|
||||
out << "content_padding_addition=" << config.m_contentPaddingAddition << "\n";
|
||||
}
|
||||
if (!config.m_rekeyAfterTime.isEmpty()) {
|
||||
out << "rekey_after_time=" << config.m_rekeyAfterTime << "\n";
|
||||
}
|
||||
if (!config.m_rekeyTimeout.isEmpty()) {
|
||||
out << "rekey_timeout=" << config.m_rekeyTimeout << "\n";
|
||||
}
|
||||
if (!config.m_rejectAfterTime.isEmpty()) {
|
||||
out << "reject_after_time=" << config.m_rejectAfterTime << "\n";
|
||||
}
|
||||
if (!config.m_keepaliveTimeout.isEmpty()) {
|
||||
out << "keepalive_timeout=" << config.m_keepaliveTimeout << "\n";
|
||||
}
|
||||
if (!config.m_maxHandshakeAttempts.isEmpty()) {
|
||||
out << "max_handshake_attempts=" << config.m_maxHandshakeAttempts << "\n";
|
||||
}
|
||||
|
||||
int err = uapiErrno(uapiCommand(message));
|
||||
if (err != 0) {
|
||||
logger.error() << "Interface configuration failed:" << strerror(err);
|
||||
@@ -229,7 +253,9 @@ bool WireguardUtilsMacos::updatePeer(const InterfaceConfig& config) {
|
||||
out << config.m_serverPort << "\n";
|
||||
|
||||
out << "replace_allowed_ips=true\n";
|
||||
out << "persistent_keepalive_interval=" << WG_KEEPALIVE_PERIOD << "\n";
|
||||
if (!config.m_persistentKeepalive.isEmpty()) {
|
||||
out << "persistent_keepalive_interval=" << config.m_persistentKeepalive << "\n";
|
||||
}
|
||||
for (const IPAddress& ip : config.m_allowedIPAddressRanges) {
|
||||
out << "allowed_ip=" << ip.toString() << "\n";
|
||||
}
|
||||
|
||||
@@ -62,12 +62,29 @@ void WindowsDaemon::prepareActivation(const InterfaceConfig& config, int inetAda
|
||||
}
|
||||
|
||||
void WindowsDaemon::activateSplitTunnel(const InterfaceConfig& config, int vpnAdapterIndex) {
|
||||
if (m_splitTunnelManager == nullptr)
|
||||
if (m_splitTunnelManager == nullptr) {
|
||||
if (config.m_vpnDisabledApps.length() > 0) {
|
||||
logger.error() << "Split tunnel manager is not initialized";
|
||||
emit backendFailure(DaemonError::ERROR_SPLIT_TUNNEL_INIT_FAILURE);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (config.m_vpnDisabledApps.length() > 0) {
|
||||
m_splitTunnelManager->start(m_inetAdapterIndex, vpnAdapterIndex);
|
||||
m_splitTunnelManager->excludeApps(config.m_vpnDisabledApps);
|
||||
if (!m_splitTunnelManager->start(m_inetAdapterIndex, vpnAdapterIndex)) {
|
||||
logger.error() << "Failed to start split tunnel";
|
||||
emit backendFailure(DaemonError::ERROR_SPLIT_TUNNEL_START_FAILURE);
|
||||
return;
|
||||
}
|
||||
if (!m_splitTunnelManager->excludeApps(config.m_vpnDisabledApps)) {
|
||||
logger.error() << "Failed to apply split tunnel app exclusions";
|
||||
emit backendFailure(DaemonError::ERROR_SPLIT_TUNNEL_EXCLUDE_FAILURE);
|
||||
return;
|
||||
}
|
||||
if (!m_splitTunnelManager->isRunning()) {
|
||||
logger.error() << "Split tunnel did not reach running state";
|
||||
emit backendFailure(DaemonError::ERROR_SPLIT_TUNNEL_START_FAILURE);
|
||||
}
|
||||
} else {
|
||||
m_splitTunnelManager->stop();
|
||||
}
|
||||
@@ -79,7 +96,9 @@ bool WindowsDaemon::run(Op op, const InterfaceConfig& config) {
|
||||
// The Client has sent us a list of disabled apps, but we failed
|
||||
// to init the the split tunnel driver.
|
||||
// So let the client know this was not possible
|
||||
logger.error() << "Split tunnel manager is not initialized";
|
||||
emit backendFailure(DaemonError::ERROR_SPLIT_TUNNEL_INIT_FAILURE);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -90,14 +109,20 @@ bool WindowsDaemon::run(Op op, const InterfaceConfig& config) {
|
||||
}
|
||||
if (config.m_vpnDisabledApps.length() > 0) {
|
||||
if (!m_splitTunnelManager->start(m_inetAdapterIndex)) {
|
||||
logger.error() << "Split tunnel start failed";
|
||||
emit backendFailure(DaemonError::ERROR_SPLIT_TUNNEL_START_FAILURE);
|
||||
return false;
|
||||
};
|
||||
if (!m_splitTunnelManager->excludeApps(config.m_vpnDisabledApps)) {
|
||||
logger.error() << "Split tunnel app exclusion failed";
|
||||
emit backendFailure(DaemonError::ERROR_SPLIT_TUNNEL_EXCLUDE_FAILURE);
|
||||
return false;
|
||||
};
|
||||
// Now the driver should be running (State == 4)
|
||||
if (!m_splitTunnelManager->isRunning()) {
|
||||
logger.error() << "Split tunnel did not reach running state";
|
||||
emit backendFailure(DaemonError::ERROR_SPLIT_TUNNEL_START_FAILURE);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -36,6 +36,8 @@
|
||||
// ID for the Firewall Sublayer
|
||||
DEFINE_GUID(ST_FW_WINFW_BASELINE_SUBLAYER_KEY, 0xc78056ff, 0x2bc1, 0x4211, 0xaa,
|
||||
0xdd, 0x7f, 0x35, 0x8d, 0xef, 0x20, 0x2d);
|
||||
DEFINE_GUID(ST_FW_WINFW_DNS_SUBLAYER_KEY, 0x60090787, 0xcca1, 0x4937, 0xaa,
|
||||
0xce, 0x51, 0x25, 0x6e, 0xf4, 0x81, 0xf3);
|
||||
// ID for the Mullvad Split-Tunnel Sublayer Provider
|
||||
DEFINE_GUID(ST_FW_PROVIDER_KEY, 0xe2c114ee, 0xf32a, 0x4264, 0xa6, 0xcb, 0x3f,
|
||||
0xa7, 0x99, 0x63, 0x56, 0xd9);
|
||||
@@ -49,6 +51,53 @@ constexpr uint8_t LOW_WEIGHT = 0;
|
||||
constexpr uint8_t MED_WEIGHT = 7;
|
||||
constexpr uint8_t HIGH_WEIGHT = 13;
|
||||
constexpr uint8_t MAX_WEIGHT = 15;
|
||||
|
||||
bool ensureSublayer(HANDLE wfp, const GUID& key, const wchar_t* name,
|
||||
const wchar_t* description) {
|
||||
FWPM_SUBLAYER0* maybeLayer = nullptr;
|
||||
DWORD result = FwpmSubLayerGetByKey0(wfp, &key, &maybeLayer);
|
||||
if (result == ERROR_SUCCESS) {
|
||||
logger.debug() << "The Sublayer Already Exists:"
|
||||
<< QString::fromWCharArray(name);
|
||||
FwpmFreeMemory0(reinterpret_cast<void**>(&maybeLayer));
|
||||
return true;
|
||||
}
|
||||
if (result != FWP_E_SUBLAYER_NOT_FOUND) {
|
||||
logger.error() << "FwpmSubLayerGetByKey0 failed. Return value:.\n"
|
||||
<< result;
|
||||
return false;
|
||||
}
|
||||
|
||||
result = FwpmTransactionBegin(wfp, NULL);
|
||||
if (result != ERROR_SUCCESS) {
|
||||
logger.error() << "FwpmTransactionBegin0 failed. Return value:.\n"
|
||||
<< result;
|
||||
return false;
|
||||
}
|
||||
|
||||
FWPM_SUBLAYER0 subLayer;
|
||||
memset(&subLayer, 0, sizeof(subLayer));
|
||||
subLayer.subLayerKey = key;
|
||||
subLayer.displayData.name = const_cast<PWSTR>(name);
|
||||
subLayer.displayData.description = const_cast<PWSTR>(description);
|
||||
subLayer.weight = 0xFFFF;
|
||||
|
||||
result = FwpmSubLayerAdd0(wfp, &subLayer, NULL);
|
||||
if (result != ERROR_SUCCESS) {
|
||||
FwpmTransactionAbort0(wfp);
|
||||
logger.error() << "FwpmSubLayerAdd0 failed. Return value:.\n" << result;
|
||||
return false;
|
||||
}
|
||||
|
||||
result = FwpmTransactionCommit0(wfp);
|
||||
if (result != ERROR_SUCCESS) {
|
||||
logger.error() << "FwpmTransactionCommit0 failed. Return value:.\n"
|
||||
<< result;
|
||||
return false;
|
||||
}
|
||||
logger.debug() << "Initialised Sublayer:" << QString::fromWCharArray(name);
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
WindowsFirewall* WindowsFirewall::create(QObject* parent) {
|
||||
@@ -116,47 +165,12 @@ bool WindowsFirewall::initSublayer() {
|
||||
}
|
||||
auto cleanup = qScopeGuard([&] { FwpmEngineClose0(wfp); });
|
||||
|
||||
// Check if the Layer Already Exists
|
||||
FWPM_SUBLAYER0* maybeLayer;
|
||||
result = FwpmSubLayerGetByKey0(wfp, &ST_FW_WINFW_BASELINE_SUBLAYER_KEY,
|
||||
&maybeLayer);
|
||||
if (result == ERROR_SUCCESS) {
|
||||
logger.debug() << "The Sublayer Already Exists!";
|
||||
FwpmFreeMemory0((void**)&maybeLayer);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Step 1: Start Transaction
|
||||
result = FwpmTransactionBegin(wfp, NULL);
|
||||
if (result != ERROR_SUCCESS) {
|
||||
logger.error() << "FwpmTransactionBegin0 failed. Return value:.\n"
|
||||
<< result;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Step 3: Add Sublayer
|
||||
FWPM_SUBLAYER0 subLayer;
|
||||
memset(&subLayer, 0, sizeof(subLayer));
|
||||
subLayer.subLayerKey = ST_FW_WINFW_BASELINE_SUBLAYER_KEY;
|
||||
subLayer.displayData.name = (PWSTR)L"Amnezia-SplitTunnel-Sublayer";
|
||||
subLayer.displayData.description =
|
||||
(PWSTR)L"Filters that enforce a good baseline";
|
||||
subLayer.weight = 0xFFFF;
|
||||
|
||||
result = FwpmSubLayerAdd0(wfp, &subLayer, NULL);
|
||||
if (result != ERROR_SUCCESS) {
|
||||
logger.error() << "FwpmSubLayerAdd0 failed. Return value:.\n" << result;
|
||||
return false;
|
||||
}
|
||||
// Step 4: Commit!
|
||||
result = FwpmTransactionCommit0(wfp);
|
||||
if (result != ERROR_SUCCESS) {
|
||||
logger.error() << "FwpmTransactionCommit0 failed. Return value:.\n"
|
||||
<< result;
|
||||
return false;
|
||||
}
|
||||
logger.debug() << "Initialised Sublayer";
|
||||
return true;
|
||||
return ensureSublayer(wfp, ST_FW_WINFW_BASELINE_SUBLAYER_KEY,
|
||||
L"Amnezia-SplitTunnel-Sublayer",
|
||||
L"Filters that enforce a good baseline") &&
|
||||
ensureSublayer(wfp, ST_FW_WINFW_DNS_SUBLAYER_KEY,
|
||||
L"Amnezia-SplitTunnel-DNS-Sublayer",
|
||||
L"DNS filters for split tunneling");
|
||||
}
|
||||
|
||||
bool WindowsFirewall::enableInterface(int vpnAdapterIndex) {
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include <QFileInfo>
|
||||
#include <QNetworkInterface>
|
||||
#include <QScopeGuard>
|
||||
#include <QUrl>
|
||||
|
||||
#pragma region
|
||||
|
||||
@@ -148,6 +149,18 @@ ProcessInfo getProcessInfo(HANDLE process, const PROCESSENTRY32W& processMeta) {
|
||||
return pi;
|
||||
}
|
||||
|
||||
QString normalizeExecutablePath(const QString& path) {
|
||||
QString normalized = path.trimmed();
|
||||
if (normalized.startsWith("file:", Qt::CaseInsensitive)) {
|
||||
const QString localPath = QUrl(normalized).toLocalFile();
|
||||
if (!localPath.isEmpty()) {
|
||||
normalized = localPath;
|
||||
}
|
||||
}
|
||||
normalized.replace('/', '\\');
|
||||
return normalized;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<WindowsSplitTunnel> WindowsSplitTunnel::create(
|
||||
@@ -287,6 +300,10 @@ bool WindowsSplitTunnel::excludeApps(const QStringList& appPaths) {
|
||||
|
||||
logger.debug() << "Pushing new Ruleset for Split-Tunnel " << state;
|
||||
auto config = generateAppConfiguration(appPaths);
|
||||
if (config.empty()) {
|
||||
logger.error() << "No valid split-tunnel application rules generated";
|
||||
return false;
|
||||
}
|
||||
|
||||
DWORD bytesReturned;
|
||||
auto ok = DeviceIoControl(m_driver, IOCTL_SET_CONFIGURATION, &config[0],
|
||||
@@ -314,7 +331,7 @@ bool WindowsSplitTunnel::start(int inetAdapterIndex, int vpnAdapterIndex) {
|
||||
auto ok = DeviceIoControl(m_driver, IOCTL_INITIALIZE, nullptr, 0, nullptr,
|
||||
0, &bytesReturned, nullptr);
|
||||
if (!ok) {
|
||||
logger.error() << "Driver init failed";
|
||||
logger.error() << "Driver init failed. Error:" << GetLastError();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -323,11 +340,16 @@ bool WindowsSplitTunnel::start(int inetAdapterIndex, int vpnAdapterIndex) {
|
||||
if (getState() == STATE_INITIALIZED) {
|
||||
logger.debug() << "State is Init, requires process config";
|
||||
auto config = generateProcessBlob();
|
||||
if (config.empty()) {
|
||||
logger.error() << "Process configuration blob is empty";
|
||||
return false;
|
||||
}
|
||||
auto ok = DeviceIoControl(m_driver, IOCTL_REGISTER_PROCESSES, &config[0],
|
||||
(DWORD)config.size(), nullptr, 0, &bytesReturned,
|
||||
nullptr);
|
||||
if (!ok) {
|
||||
logger.error() << "Failed to set Process Config";
|
||||
logger.error() << "Failed to set Process Config. Error:"
|
||||
<< GetLastError();
|
||||
return false;
|
||||
}
|
||||
logger.debug() << "Set Process Config ok || new State:" << stateString();
|
||||
@@ -340,11 +362,16 @@ bool WindowsSplitTunnel::start(int inetAdapterIndex, int vpnAdapterIndex) {
|
||||
logger.debug() << "Driver is ready || new State:" << stateString();
|
||||
|
||||
auto config = generateIPConfiguration(inetAdapterIndex, vpnAdapterIndex);
|
||||
if (config.empty()) {
|
||||
logger.error() << "Network configuration blob is empty. Internet adapter:"
|
||||
<< inetAdapterIndex << "VPN adapter:" << vpnAdapterIndex;
|
||||
return false;
|
||||
}
|
||||
auto ok = DeviceIoControl(m_driver, IOCTL_REGISTER_IP_ADDRESSES, &config[0],
|
||||
(DWORD)config.size(), nullptr, 0, &bytesReturned,
|
||||
nullptr);
|
||||
if (!ok) {
|
||||
logger.error() << "Failed to set Network Config";
|
||||
logger.error() << "Failed to set Network Config. Error:" << GetLastError();
|
||||
return false;
|
||||
}
|
||||
logger.debug() << "New Network Config Applied || new State:" << stateString();
|
||||
@@ -404,13 +431,22 @@ std::vector<uint8_t> WindowsSplitTunnel::generateAppConfiguration(
|
||||
size_t cummulated_string_size = 0;
|
||||
QStringList dosPaths;
|
||||
for (auto const& path : appPaths) {
|
||||
auto dosPath = convertPath(path);
|
||||
const QString normalizedPath = normalizeExecutablePath(path);
|
||||
auto dosPath = convertPath(normalizedPath);
|
||||
if (dosPath.isEmpty()) {
|
||||
logger.error() << "Rejecting split-tunnel app path with empty device "
|
||||
"conversion:"
|
||||
<< normalizedPath;
|
||||
continue;
|
||||
}
|
||||
dosPaths.append(dosPath);
|
||||
cummulated_string_size += dosPath.toStdWString().size() * sizeof(wchar_t);
|
||||
logger.debug() << dosPath;
|
||||
}
|
||||
if (dosPaths.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
size_t bufferSize = sizeof(CONFIGURATION_HEADER) +
|
||||
(sizeof(CONFIGURATION_ENTRY) * appPaths.size()) +
|
||||
(sizeof(CONFIGURATION_ENTRY) * dosPaths.size()) +
|
||||
cummulated_string_size;
|
||||
std::vector<uint8_t> outBuffer(bufferSize);
|
||||
|
||||
@@ -418,7 +454,7 @@ std::vector<uint8_t> WindowsSplitTunnel::generateAppConfiguration(
|
||||
auto entry = (CONFIGURATION_ENTRY*)(header + 1);
|
||||
|
||||
auto stringDest = &outBuffer[0] + sizeof(CONFIGURATION_HEADER) +
|
||||
(sizeof(CONFIGURATION_ENTRY) * appPaths.size());
|
||||
(sizeof(CONFIGURATION_ENTRY) * dosPaths.size());
|
||||
|
||||
SIZE_T stringOffset = 0;
|
||||
|
||||
@@ -437,7 +473,7 @@ std::vector<uint8_t> WindowsSplitTunnel::generateAppConfiguration(
|
||||
stringOffset += stringLength;
|
||||
}
|
||||
|
||||
header->NumEntries = appPaths.length();
|
||||
header->NumEntries = dosPaths.length();
|
||||
header->TotalLength = bufferSize;
|
||||
|
||||
return outBuffer;
|
||||
@@ -449,9 +485,7 @@ std::vector<std::byte> WindowsSplitTunnel::generateIPConfiguration(
|
||||
|
||||
auto config = reinterpret_cast<IP_ADDRESSES_CONFIG*>(&out[0]);
|
||||
|
||||
auto ifaces = QNetworkInterface::allInterfaces();
|
||||
|
||||
if (vpnAdapterIndex == 0) {
|
||||
if (vpnAdapterIndex == 0) {
|
||||
vpnAdapterIndex = WindowsCommons::VPNAdapterIndex();
|
||||
}
|
||||
// Always the VPN
|
||||
@@ -520,7 +554,7 @@ std::vector<uint8_t> WindowsSplitTunnel::generateProcessBlob() {
|
||||
auto process_handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE,
|
||||
currentProcess.th32ProcessID);
|
||||
|
||||
if (process_handle == INVALID_HANDLE_VALUE) {
|
||||
if (process_handle == nullptr) {
|
||||
continue;
|
||||
}
|
||||
ProcessInfo info = getProcessInfo(process_handle, currentProcess);
|
||||
@@ -640,23 +674,48 @@ bool WindowsSplitTunnel::isInstalled() {
|
||||
}
|
||||
|
||||
QString WindowsSplitTunnel::convertPath(const QString& path) {
|
||||
auto parts = path.split("/");
|
||||
const QString normalizedPath = normalizeExecutablePath(path);
|
||||
if (normalizedPath.isEmpty()) {
|
||||
logger.error() << "Empty executable path for DOS device conversion";
|
||||
return "";
|
||||
}
|
||||
auto parts = normalizedPath.split("\\", Qt::SkipEmptyParts);
|
||||
if (parts.isEmpty()) {
|
||||
logger.error() << "Invalid executable path for DOS device conversion:"
|
||||
<< normalizedPath;
|
||||
return "";
|
||||
}
|
||||
QString driveLetter = parts.takeFirst();
|
||||
if (!driveLetter.contains(":") || parts.size() == 0) {
|
||||
// device should contain : for e.g C:
|
||||
logger.error() << "Invalid executable path for DOS device conversion:"
|
||||
<< normalizedPath;
|
||||
return "";
|
||||
}
|
||||
QByteArray buffer(2048, 0xFFu);
|
||||
auto ok = QueryDosDeviceW(qUtf16Printable(driveLetter),
|
||||
(wchar_t*)buffer.data(), buffer.size() / 2);
|
||||
|
||||
if (ok == ERROR_INSUFFICIENT_BUFFER) {
|
||||
QByteArray buffer(2048 * sizeof(wchar_t), 0);
|
||||
DWORD ok = 0;
|
||||
DWORD err = ERROR_SUCCESS;
|
||||
for (int attempt = 0; attempt < 4; ++attempt) {
|
||||
ok = QueryDosDeviceW(reinterpret_cast<LPCWSTR>(driveLetter.utf16()),
|
||||
reinterpret_cast<LPWSTR>(buffer.data()),
|
||||
buffer.size() / sizeof(wchar_t));
|
||||
if (ok != 0) {
|
||||
break;
|
||||
}
|
||||
err = GetLastError();
|
||||
if (err != ERROR_INSUFFICIENT_BUFFER) {
|
||||
WindowsUtils::windowsLog("Err fetching dos path");
|
||||
logger.error() << "QueryDosDeviceW failed for" << driveLetter
|
||||
<< "error:" << err;
|
||||
return "";
|
||||
}
|
||||
buffer.resize(buffer.size() * 2);
|
||||
ok = QueryDosDeviceW(qUtf16Printable(driveLetter), (wchar_t*)buffer.data(),
|
||||
buffer.size() / 2);
|
||||
buffer.fill(0);
|
||||
}
|
||||
if (ok == 0) {
|
||||
WindowsUtils::windowsLog("Err fetching dos path");
|
||||
logger.error() << "QueryDosDeviceW failed after buffer growth for"
|
||||
<< driveLetter << "error:" << err;
|
||||
return "";
|
||||
}
|
||||
QString deviceName;
|
||||
|
||||
@@ -180,7 +180,9 @@ bool WireguardUtilsWindows::updatePeer(const InterfaceConfig& config) {
|
||||
out << config.m_serverPort << "\n";
|
||||
|
||||
out << "replace_allowed_ips=true\n";
|
||||
out << "persistent_keepalive_interval=" << WG_KEEPALIVE_PERIOD << "\n";
|
||||
if (!config.m_persistentKeepalive.isEmpty()) {
|
||||
out << "persistent_keepalive_interval=" << config.m_persistentKeepalive << "\n";
|
||||
}
|
||||
for (const IPAddress& ip : config.m_allowedIPAddressRanges) {
|
||||
out << "allowed_ip=" << ip.toString() << "\n";
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
#include "secureQSettings.h"
|
||||
|
||||
#include "../client/3rd/QSimpleCrypto/src/include/QAead.h"
|
||||
#include "../client/3rd/QSimpleCrypto/src/include/QBlockCipher.h"
|
||||
#include "core/utils/utilities.h"
|
||||
#include "cryptoUtils.h"
|
||||
#include <QDataStream>
|
||||
#include <QDebug>
|
||||
#include <QEventLoop>
|
||||
@@ -180,11 +179,8 @@ void SecureQSettings::clearSettings()
|
||||
|
||||
QByteArray SecureQSettings::encryptText(const QByteArray &value) const
|
||||
{
|
||||
QSimpleCrypto::QBlockCipher cipher;
|
||||
QByteArray result;
|
||||
try {
|
||||
result = cipher.encryptAesBlockCipher(value, getEncKey(), getEncIv());
|
||||
} catch (...) { // todo change error handling in QSimpleCrypto?
|
||||
QByteArray result = CryptoUtils::encryptAes256Cbc(value, getEncKey(), getEncIv());
|
||||
if (result.isEmpty() && !value.isEmpty()) {
|
||||
qCritical() << "error when encrypting the settings value";
|
||||
}
|
||||
return result;
|
||||
@@ -192,11 +188,8 @@ QByteArray SecureQSettings::encryptText(const QByteArray &value) const
|
||||
|
||||
QByteArray SecureQSettings::decryptText(const QByteArray &ba) const
|
||||
{
|
||||
QSimpleCrypto::QBlockCipher cipher;
|
||||
QByteArray result;
|
||||
try {
|
||||
result = cipher.decryptAesBlockCipher(ba, getEncKey(), getEncIv());
|
||||
} catch (...) { // todo change error handling in QSimpleCrypto?
|
||||
QByteArray result = CryptoUtils::decryptAes256Cbc(ba, getEncKey(), getEncIv());
|
||||
if (result.isEmpty() && !ba.isEmpty()) {
|
||||
qCritical() << "error when decrypting the settings value";
|
||||
}
|
||||
return result;
|
||||
@@ -225,8 +218,7 @@ QByteArray SecureQSettings::getEncKey() const
|
||||
|
||||
if (m_key.isEmpty()) {
|
||||
// Create new key
|
||||
QSimpleCrypto::QBlockCipher cipher;
|
||||
QByteArray key = cipher.generatePrivateSalt(32);
|
||||
QByteArray key = CryptoUtils::generateRandomBytes(32);
|
||||
if (key.isEmpty()) {
|
||||
qCritical() << "SecureQSettings::getEncKey Unable to generate new enc key";
|
||||
}
|
||||
@@ -254,8 +246,7 @@ QByteArray SecureQSettings::getEncIv() const
|
||||
|
||||
if (m_iv.isEmpty()) {
|
||||
// Create new IV
|
||||
QSimpleCrypto::QBlockCipher cipher;
|
||||
QByteArray iv = cipher.generatePrivateSalt(32);
|
||||
QByteArray iv = CryptoUtils::generateRandomBytes(32);
|
||||
if (iv.isEmpty()) {
|
||||
qCritical() << "SecureQSettings::getEncIv Unable to generate new enc IV";
|
||||
}
|
||||
|
||||
10
client/server_scripts/install_conntrack.sh
Normal file
10
client/server_scripts/install_conntrack.sh
Normal file
@@ -0,0 +1,10 @@
|
||||
if command -v conntrack > /dev/null 2>&1; then echo "conntrack already installed"; exit 0; fi;\
|
||||
if which apt-get > /dev/null 2>&1; then pm=$(which apt-get); silent_inst="-yq install --install-recommends"; check_pkgs="-yq update"; conntrack_pkg="conntrack"; dist="debian";\
|
||||
elif which dnf > /dev/null 2>&1; then pm=$(which dnf); silent_inst="-yq install"; check_pkgs="-yq check-update"; conntrack_pkg="conntrack-tools"; dist="fedora";\
|
||||
elif which yum > /dev/null 2>&1; then pm=$(which yum); silent_inst="-y -q install"; check_pkgs="-y -q check-update"; conntrack_pkg="conntrack-tools"; dist="centos";\
|
||||
elif which zypper > /dev/null 2>&1; then pm=$(which zypper); silent_inst="-nq install"; check_pkgs="-nq refresh"; conntrack_pkg="conntrack-tools"; dist="opensuse";\
|
||||
elif which pacman > /dev/null 2>&1; then pm=$(which pacman); silent_inst="-S --noconfirm --noprogressbar --quiet"; check_pkgs="-Sup"; conntrack_pkg="conntrack-tools"; dist="archlinux";\
|
||||
else echo "Packet manager not found"; exit 0; fi;\
|
||||
if [ "$dist" = "debian" ]; then export DEBIAN_FRONTEND=noninteractive; fi;\
|
||||
sudo $pm $check_pkgs; sudo $pm $silent_inst $conntrack_pkg;\
|
||||
command -v conntrack > /dev/null 2>&1 && echo "conntrack installed" || echo "conntrack install failed"
|
||||
@@ -18,6 +18,7 @@
|
||||
<file>dns/Dockerfile</file>
|
||||
<file>dns/run_container.sh</file>
|
||||
<file>install_docker.sh</file>
|
||||
<file>install_conntrack.sh</file>
|
||||
<file>ipsec/configure_container.sh</file>
|
||||
<file>ipsec/Dockerfile</file>
|
||||
<file>ipsec/mobileconfig.plist</file>
|
||||
|
||||
@@ -61,6 +61,12 @@ rm -f /data/config.toml
|
||||
echo ""
|
||||
echo "[access.users]"
|
||||
echo "$TELEMT_USER_NAME = \"$SECRET\""
|
||||
i=1
|
||||
for EXTRA in $(echo "$TELEMT_ADDITIONAL_SECRETS" | tr ',' ' '); do
|
||||
echo "$EXTRA" | grep -qE '^[0-9a-fA-F]{32}$' || continue
|
||||
echo "extra_$i = \"$EXTRA\""
|
||||
i=$((i + 1))
|
||||
done
|
||||
} > /data/config.toml
|
||||
|
||||
echo "$SECRET" > /data/secret
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -303,6 +303,8 @@ bool SubscriptionUiController::importFreeFromGateway()
|
||||
emit installServerFromApiFinished(tr("%1 installed successfully.").arg(m_apiServicesModel->getSelectedServiceName()));
|
||||
return true;
|
||||
} else if (errorCode == ErrorCode::ApiCaptchaRequiredError && captchaInfo.isRequired) {
|
||||
m_captchaState = CaptchaState{};
|
||||
m_captchaState.flow = CaptchaFlow::Import;
|
||||
m_captchaState.userCountryCode = userCountryCode;
|
||||
m_captchaState.serviceType = serviceType;
|
||||
m_captchaState.serviceProtocol = serviceProtocol;
|
||||
@@ -327,6 +329,11 @@ void SubscriptionUiController::onCaptchaSolved(const QString &captchaId, const Q
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_captchaState.flow == CaptchaFlow::Update) {
|
||||
resolveUpdateCaptcha(captchaId, solution);
|
||||
return;
|
||||
}
|
||||
|
||||
SubscriptionController::ProtocolData protocolData;
|
||||
protocolData.certPrivKey = m_captchaState.openvpnPrivKey;
|
||||
protocolData.wireGuardClientPrivKey = m_captchaState.wireguardClientPrivKey;
|
||||
@@ -368,6 +375,33 @@ void SubscriptionUiController::onRefreshCaptchaRequested()
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_captchaState.flow == CaptchaFlow::Update) {
|
||||
SubscriptionController::CaptchaInfo captchaInfo;
|
||||
SubscriptionController::ProtocolData usedProtocolData;
|
||||
ErrorCode errorCode = m_subscriptionController->updateServiceFromGateway(
|
||||
m_captchaState.serverId,
|
||||
m_captchaState.newCountryCode,
|
||||
m_captchaState.isConnectEvent,
|
||||
&captchaInfo,
|
||||
&usedProtocolData);
|
||||
|
||||
if (errorCode == ErrorCode::ApiCaptchaRequiredError && captchaInfo.isRequired) {
|
||||
m_captchaState.updateProtocolData = usedProtocolData;
|
||||
emit captchaRequired(captchaInfo.captchaId, captchaInfo.captchaImageBase64,
|
||||
captchaInfo.hint.isEmpty() ? tr("Enter the digits from the image to continue") : captchaInfo.hint);
|
||||
} else if (errorCode == ErrorCode::NoError) {
|
||||
emitCaptchaUpdateSuccess();
|
||||
} else {
|
||||
m_captchaState.isPending = false;
|
||||
if (errorCode == ErrorCode::ApiSubscriptionExpiredError) {
|
||||
emit subscriptionExpiredOnServer();
|
||||
} else {
|
||||
emit errorOccurred(errorCode);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
SubscriptionController::ProtocolData protocolData;
|
||||
protocolData.certPrivKey = m_captchaState.openvpnPrivKey;
|
||||
protocolData.wireGuardClientPrivKey = m_captchaState.wireguardClientPrivKey;
|
||||
@@ -392,6 +426,39 @@ void SubscriptionUiController::onRefreshCaptchaRequested()
|
||||
}
|
||||
}
|
||||
|
||||
void SubscriptionUiController::resolveUpdateCaptcha(const QString &captchaId, const QString &solution)
|
||||
{
|
||||
SubscriptionController::CaptchaInfo retryCaptcha;
|
||||
ErrorCode errorCode = m_subscriptionController->resolveUpdateServiceCaptcha(
|
||||
m_captchaState.serverId,
|
||||
m_captchaState.newCountryCode,
|
||||
m_captchaState.isConnectEvent,
|
||||
m_captchaState.updateProtocolData,
|
||||
captchaId,
|
||||
solution,
|
||||
&retryCaptcha);
|
||||
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
emitCaptchaUpdateSuccess();
|
||||
return;
|
||||
}
|
||||
|
||||
if ((errorCode == ErrorCode::ApiCaptchaInvalidError || errorCode == ErrorCode::ApiCaptchaRefreshError
|
||||
|| errorCode == ErrorCode::ApiCaptchaRequiredError)
|
||||
&& retryCaptcha.isRequired) {
|
||||
emit captchaRequired(retryCaptcha.captchaId, retryCaptcha.captchaImageBase64,
|
||||
retryCaptcha.hint.isEmpty() ? tr("Enter the digits from the image to continue") : retryCaptcha.hint);
|
||||
return;
|
||||
}
|
||||
|
||||
m_captchaState.isPending = false;
|
||||
if (errorCode == ErrorCode::ApiSubscriptionExpiredError) {
|
||||
emit subscriptionExpiredOnServer();
|
||||
} else {
|
||||
emit errorOccurred(errorCode);
|
||||
}
|
||||
}
|
||||
|
||||
bool SubscriptionUiController::importTrialFromGateway(const QString &email)
|
||||
{
|
||||
emit trialEmailError(QString());
|
||||
@@ -423,20 +490,29 @@ bool SubscriptionUiController::updateServiceFromGateway(const QString &serverId,
|
||||
|| oldApiV2->apiConfig.isSubscriptionExpired();
|
||||
}
|
||||
|
||||
ErrorCode errorCode = m_subscriptionController->updateServiceFromGateway(serverId, newCountryCode, isConnectEvent);
|
||||
SubscriptionController::CaptchaInfo captchaInfo;
|
||||
SubscriptionController::ProtocolData usedProtocolData;
|
||||
ErrorCode errorCode = m_subscriptionController->updateServiceFromGateway(serverId, newCountryCode, isConnectEvent,
|
||||
&captchaInfo, &usedProtocolData);
|
||||
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
if (wasSubscriptionExpired) {
|
||||
emit subscriptionRefreshNeeded();
|
||||
}
|
||||
if (reloadServiceConfig) {
|
||||
emit reloadServerFromApiFinished(tr("API config reloaded"));
|
||||
} else if (newCountryName.isEmpty()) {
|
||||
emit updateServerFromApiFinished();
|
||||
} else {
|
||||
emit changeApiCountryFinished(tr("Successfully changed the country of connection to %1").arg(newCountryName));
|
||||
}
|
||||
emitUpdateSuccess(wasSubscriptionExpired, reloadServiceConfig, newCountryName);
|
||||
return true;
|
||||
} else if (errorCode == ErrorCode::ApiCaptchaRequiredError && captchaInfo.isRequired) {
|
||||
m_captchaState = CaptchaState{};
|
||||
m_captchaState.flow = CaptchaFlow::Update;
|
||||
m_captchaState.serverId = serverId;
|
||||
m_captchaState.newCountryCode = newCountryCode;
|
||||
m_captchaState.newCountryName = newCountryName;
|
||||
m_captchaState.isConnectEvent = isConnectEvent;
|
||||
m_captchaState.reloadServiceConfig = reloadServiceConfig;
|
||||
m_captchaState.wasSubscriptionExpired = wasSubscriptionExpired;
|
||||
m_captchaState.updateProtocolData = usedProtocolData;
|
||||
m_captchaState.isPending = true;
|
||||
|
||||
emit captchaRequired(captchaInfo.captchaId, captchaInfo.captchaImageBase64,
|
||||
captchaInfo.hint.isEmpty() ? tr("Enter the digits from the image to continue") : captchaInfo.hint);
|
||||
return false;
|
||||
} else {
|
||||
if (errorCode == ErrorCode::ApiSubscriptionExpiredError) {
|
||||
emit subscriptionExpiredOnServer();
|
||||
@@ -447,6 +523,37 @@ bool SubscriptionUiController::updateServiceFromGateway(const QString &serverId,
|
||||
}
|
||||
}
|
||||
|
||||
void SubscriptionUiController::emitUpdateSuccess(bool wasSubscriptionExpired, bool reloadServiceConfig, const QString &newCountryName)
|
||||
{
|
||||
if (wasSubscriptionExpired) {
|
||||
emit subscriptionRefreshNeeded();
|
||||
}
|
||||
if (reloadServiceConfig) {
|
||||
emit reloadServerFromApiFinished(tr("API config reloaded"));
|
||||
} else if (newCountryName.isEmpty()) {
|
||||
emit updateServerFromApiFinished();
|
||||
} else {
|
||||
emit changeApiCountryFinished(tr("Successfully changed the country of connection to %1").arg(newCountryName));
|
||||
}
|
||||
}
|
||||
|
||||
void SubscriptionUiController::emitCaptchaUpdateSuccess()
|
||||
{
|
||||
const bool fromValidateConfig = m_captchaState.fromValidateConfig;
|
||||
const bool wasSubscriptionExpired = m_captchaState.wasSubscriptionExpired;
|
||||
const bool reloadServiceConfig = m_captchaState.reloadServiceConfig;
|
||||
const QString newCountryName = m_captchaState.newCountryName;
|
||||
|
||||
m_captchaState.isPending = false;
|
||||
emit captchaFlowDismissRequested();
|
||||
|
||||
if (fromValidateConfig) {
|
||||
emit configValidated(true);
|
||||
return;
|
||||
}
|
||||
emitUpdateSuccess(wasSubscriptionExpired, reloadServiceConfig, newCountryName);
|
||||
}
|
||||
|
||||
|
||||
bool SubscriptionUiController::deactivateDevice(const QString &serverId)
|
||||
{
|
||||
@@ -482,7 +589,25 @@ void SubscriptionUiController::validateConfig()
|
||||
|
||||
bool hasInstalledContainers = m_serversController->hasInstalledContainers(serverId);
|
||||
|
||||
ErrorCode errorCode = m_subscriptionController->validateAndUpdateConfig(serverId, hasInstalledContainers);
|
||||
SubscriptionController::CaptchaInfo captchaInfo;
|
||||
SubscriptionController::ProtocolData usedProtocolData;
|
||||
ErrorCode errorCode = m_subscriptionController->validateAndUpdateConfig(serverId, hasInstalledContainers,
|
||||
&captchaInfo, &usedProtocolData);
|
||||
|
||||
if (errorCode == ErrorCode::ApiCaptchaRequiredError && captchaInfo.isRequired) {
|
||||
m_captchaState = CaptchaState{};
|
||||
m_captchaState.flow = CaptchaFlow::Update;
|
||||
m_captchaState.fromValidateConfig = true;
|
||||
m_captchaState.serverId = serverId;
|
||||
m_captchaState.isConnectEvent = true;
|
||||
m_captchaState.updateProtocolData = usedProtocolData;
|
||||
m_captchaState.isPending = true;
|
||||
|
||||
emit captchaRequired(captchaInfo.captchaId, captchaInfo.captchaImageBase64,
|
||||
captchaInfo.hint.isEmpty() ? tr("Enter the digits from the image to continue") : captchaInfo.hint);
|
||||
emit configValidated(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
if (errorCode == ErrorCode::ApiSubscriptionExpiredError) {
|
||||
@@ -508,6 +633,18 @@ bool SubscriptionUiController::isVlessProtocol(const QString &serverId)
|
||||
}
|
||||
|
||||
|
||||
QString SubscriptionUiController::currentProtocol(const QString &serverId)
|
||||
{
|
||||
return m_subscriptionController->currentProtocol(serverId);
|
||||
}
|
||||
|
||||
|
||||
QStringList SubscriptionUiController::availableProtocols(const QString &serverId)
|
||||
{
|
||||
return m_subscriptionController->availableProtocols(serverId);
|
||||
}
|
||||
|
||||
|
||||
void SubscriptionUiController::removeApiConfig(const QString &serverId)
|
||||
{
|
||||
m_subscriptionController->removeApiConfig(serverId);
|
||||
|
||||
@@ -57,6 +57,8 @@ public slots:
|
||||
|
||||
void setCurrentProtocol(const QString &serverId, const QString &protocolName);
|
||||
bool isVlessProtocol(const QString &serverId);
|
||||
QString currentProtocol(const QString &serverId);
|
||||
QStringList availableProtocols(const QString &serverId);
|
||||
|
||||
bool isCaptchaAwaitingUser() const;
|
||||
void onCaptchaSolved(const QString &captchaId, const QString &solution);
|
||||
@@ -95,7 +97,15 @@ signals:
|
||||
void unsupportedConnectDrawerRequested();
|
||||
|
||||
private:
|
||||
enum class CaptchaFlow {
|
||||
Import,
|
||||
Update
|
||||
};
|
||||
|
||||
struct CaptchaState {
|
||||
CaptchaFlow flow = CaptchaFlow::Import;
|
||||
|
||||
// Import flow
|
||||
QString userCountryCode;
|
||||
QString serviceType;
|
||||
QString serviceProtocol;
|
||||
@@ -103,10 +113,25 @@ private:
|
||||
QString wireguardClientPrivKey;
|
||||
QString wireguardClientPubKey;
|
||||
QString xrayUuid;
|
||||
|
||||
// Update flow
|
||||
QString serverId;
|
||||
QString newCountryCode;
|
||||
QString newCountryName;
|
||||
bool isConnectEvent = false;
|
||||
bool reloadServiceConfig = false;
|
||||
bool wasSubscriptionExpired = false;
|
||||
bool fromValidateConfig = false;
|
||||
SubscriptionController::ProtocolData updateProtocolData;
|
||||
|
||||
bool isPending = false;
|
||||
} m_captchaState;
|
||||
|
||||
private:
|
||||
void emitUpdateSuccess(bool wasSubscriptionExpired, bool reloadServiceConfig, const QString &newCountryName);
|
||||
void emitCaptchaUpdateSuccess();
|
||||
void resolveUpdateCaptcha(const QString &captchaId, const QString &solution);
|
||||
|
||||
QList<QString> getQrCodes();
|
||||
int getQrCodesCount();
|
||||
QString getVpnKey();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user