Compare commits

...

39 Commits

Author SHA1 Message Date
NickVs2015
a12fb51214 chore: add Android armv7/arm64-v8a build on Qt 6.10.1 2026-07-28 20:41:13 +03:00
NickVs2015
9af9e1fb4f chore: bump version to 2138 2026-07-28 20:41:13 +03:00
NickVs2015
f7d7f6d9eb fix: amnezia-libxray apply patch to all releases 2026-07-28 20:41:13 +03:00
NickVs2015
38fd316865 fix: pin go/1.23.12 for openvpn-pt-android to avoid android/arm crash 2026-07-28 20:39:27 +03:00
vkamn
4d28f81965 fix: set timeout for MarketplaceUpdateController 2026-07-26 15:20:53 +08:00
vkamn
5c035bb6cf chore: bump version 2026-07-25 23:38:25 +08:00
NickVs2015
26c22327af fix: netlink message lookup and buffer size 2026-07-25 19:31:37 +08:00
vkamn
5e9def4184 feat: add awg3 support 2026-07-25 19:29:54 +08:00
yp
a98c60d967 feat: add update dialog on mobile platforms (#2867)
* add force update

* add native UI & remove func force update
2026-07-25 19:29:24 +08:00
vkamn
0faf3e6d2f fix: pass keepalive from config to uapi 2026-07-25 00:34:38 +08:00
NickVs2015
a32a26bb67 fix: add 16kb page size support to amnezia-libxray android build (#2865) 2026-07-23 20:02:52 +08:00
vkamn
431ece7fff chore: update links in readme (#2860) 2026-07-23 19:24:15 +08:00
vkamn
5e82c6a558 fix: set macos keychain path (#2858) 2026-07-22 20:46:52 +08:00
vkamn
84a2d13b68 fix: various fixes (#2856)
* fix: fixed endless preparing

* fix: remove update drawer when open news
2026-07-22 00:25:37 +08:00
vkamn
324b09cca3 fix: various fixes (#2850)
* fix: return macos ne icon

* fix: return default sni to googletagmanager

* fix: context menu did not appear on ios
2026-07-21 21:37:13 +08:00
vkamn
e70563a071 chore: update links in readme (#2855) 2026-07-21 19:12:38 +08:00
Yaroslav Gurov
bc2a0ca649 fix: copy PF files to debug directory (#2849) 2026-07-20 16:37:59 +08:00
Yaroslav Gurov
3c6ac431b4 chore: update awg (#2845)
* chore: update awg

* chore: update xray

* chore: update awg-apple checksum

* chore: update awg-windows checksum
2026-07-20 15:38:02 +08:00
Yaroslav Gurov
e96a2b171c fix: get rid of empty i1-i5 params (#2846) 2026-07-20 10:44:07 +08:00
vkamn
06d219b92b chore: update xcode version in deploy.yml (#2838) 2026-07-16 23:22:22 +08:00
yp
28c77e570b fix: captcha keyboard overlap (#2827) 2026-07-15 14:11:44 +08:00
yp
6691d2e21c feat: add captha to update service (#2817)
* Feat: add Captha update service

* fixed fill captchaInfo from response
2026-07-15 14:11:27 +08:00
vkamn
c7b5ee1867 chore: minor fixes (#2833)
* fix: always reinstall xray after settings changes

* chore: bump version

* fix: return blocking clear profile
2026-07-15 14:09:35 +08:00
vkamn
e4127f47f3 chore: merge with upstream (#2826)
* feat: add cryptoUtils

* feat: move protocol selection to home page

* feat: add connection warmup
2026-07-13 14:26:00 +08:00
NickVs2015
4b4563818e fix: app icon IOS not appearing after installation (#2822) 2026-07-13 12:59:32 +08:00
communism420
dbb43272e6 chore: fix typo in traffic obfuscation protocols list (#2823) 2026-07-13 12:46:07 +08:00
yyy-amnezia
5e70eb20c9 feat: build macOS prebuilts for arm64 and x86_64 (#2601)
* Build macOS prebuilts for arm64 and x86_64

* chore: remove parallel thingy from CI/CD

* feat: rework amnezia-xray-bindings for multiarch

* Fix macOS universal recipe builds

* Sign macOS OpenVPN helper script

* Address macOS build review comments

* Isolate awg-go universal builds

* Disable Xcode signing for non-NE macOS

* Address remaining macOS recipe review comments

* feat(conan): rework go-based recipes to support Apple's multiarch

* fix(conan): amnezia-xray-bindings naming conventions

* chore(ci/cd): specify macos versions explicitly

* fix(conan): use escaped path for make in windows

* fix(ci/cd): checkout prior using of provisioning profiles

---------

Co-authored-by: Yaroslav Gurov <ygurov@proton.me>
2026-07-09 13:37:46 +08:00
NickVs2015
15176cd3ae chore: up GP version code (#2819) 2026-07-09 13:33:52 +08:00
NickVs2015
25df36ad73 fix: filepicker atv dev (#2763)
* fix: support filepicker android 9

* fix: double call filepicker

* fix: remove double connectionUiControllerProtected
2026-07-08 17:34:45 +08:00
MrMirDan
ea8e3fda56 chore: base client test dependency (#2790) 2026-07-08 13:58:57 +08:00
yp
31ae949ba0 fix: UI Telemt|MTProxy QA fixed (#2734)
* fixed UI 'Transport mode' 'Share QR Code'

* fix FakeTLS domain input & port space & workers

* fix Public host|Port|Promoted tag|FakeTLS domain|Internal/External IP

* fixed diagnostic mtproxy/telemt

* fixed SshSession parent

* fixed timeout & add message & fix disabled telemt|mtproxy

* fixed telemt=mtproxy algoritm

* fixed add/delete additional secret

* fix Additional secret

* remove comment

* fixed mtproxy update secret

* checking the parallelism of lines

* remove old code & & ref code

* chore: minor codestyle fixes

* chore: minor codestyle fixes

---------

Co-authored-by: vkamn <vk@amnezia.org>
2026-07-08 13:50:13 +08:00
NickVs2015
a17847a90b fix: add connectionUiControllerProtected accessor for tests (#2809) 2026-07-07 20:14:45 +08:00
cd-amn
2ca497cd06 fix: openvpn and openssl conan install (#2799) 2026-07-07 17:01:07 +08:00
vkamn
bd003b149d chore: updates from proxy-storage-cache branch (#2773)
* chore: updates from proxy-storage-cache branch

* fix: fixed tap controller paths
2026-07-06 14:47:32 +08:00
NickVs2015
35c1014a44 chore: up version code and compatibility with checks (#2806)
* chore: up version code

* fix: add compatibility with checks
2026-07-06 14:42:02 +08:00
NickVs2015
27857cde8a fix: error 3rdparty and compile (#2789)
* fix: add binary dir to add_subdirectory calls in 3rdparty.cmake

* fix: use list(APPEND LIBS) instead of target_link_libraries for gamepad on Android

* fix: add core skippltgormController
2026-07-06 13:25:06 +08:00
ULTRAVIOLENC3
b39f583097 fix: create DNS WFP sublayer for app split tunneling (#2768) 2026-07-02 20:15:30 +08:00
ULTRAVIOLENC3
6c7b65cac6 fix windows app split tunnel path conversion (#2691) 2026-07-02 20:12:52 +08:00
vkamn
0f6847219b chore: bump version (#2772) 2026-06-26 13:04:36 +08:00
121 changed files with 6684 additions and 3364 deletions

View File

@@ -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
@@ -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:
@@ -903,6 +913,127 @@ jobs:
archive: false
retention-days: 7
# ------------------------------------------------------
Build-Android-Qt610:
runs-on: android-runner
needs: Bake-Prebuilts-Android
if: ${{ always() }}
env:
ANDROID_PLATFORM: android-28
NDK_VERSION: 27.0.11718014
QT_VERSION: 6.10.1
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 }}
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
FREE_V2_ENDPOINT: ${{ secrets.FREE_V2_ENDPOINT }}
PREM_V1_ENDPOINT: ${{ secrets.PREM_V1_ENDPOINT }}
steps:
- name: 'Get sources'
uses: actions/checkout@v4
with:
submodules: 'true'
- name: 'Install desktop Qt'
uses: jurplel/install-qt-action@v4
with:
version: ${{ env.QT_VERSION }}
host: 'linux'
target: 'desktop'
arch: 'linux_gcc_64'
modules: ${{ env.QT_MODULES }}
dir: ${{ runner.temp }}
py7zrversion: '==0.22.*'
extra: '--base ${{ env.QT_MIRROR }}'
- name: 'Install android_armv7 Qt'
uses: jurplel/install-qt-action@v4
with:
version: ${{ env.QT_VERSION }}
host: 'linux'
target: 'android'
arch: 'android_armv7'
modules: ${{ env.QT_MODULES }}
dir: ${{ runner.temp }}
py7zrversion: '==0.22.*'
extra: '--base ${{ env.QT_MIRROR }}'
- name: 'Install android_arm64_v8a Qt'
uses: jurplel/install-qt-action@v4
with:
version: ${{ env.QT_VERSION }}
host: 'linux'
target: 'android'
arch: 'android_arm64_v8a'
modules: ${{ env.QT_MODULES }}
dir: ${{ runner.temp }}
py7zrversion: '==0.22.*'
extra: '--base ${{ env.QT_MIRROR }}'
- name: 'Setup Java'
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: 'Setup Android SDK'
uses: android-actions/setup-android@v4
with:
packages: "platforms;${{ env.ANDROID_PLATFORM }} ndk;${{ env.NDK_VERSION }}"
- name: 'Setup python'
uses: actions/setup-python@v6
with:
python-version: 3.14
- name: 'Install conan'
run: pip install "conan==2.28.0"
- name: 'Decode keystore secret to file'
env:
KEYSTORE_BASE64: ${{ secrets.ANDROID_RELEASE_KEYSTORE_BASE64 }}
shell: bash
run: |
echo $KEYSTORE_BASE64 | base64 --decode > android.keystore
- name: 'Build project'
env:
QT_INSTALL_DIR: ${{ runner.temp }}
QT_ANDROID_KEYSTORE_PATH: ${{ github.workspace }}/android.keystore
QT_ANDROID_KEYSTORE_ALIAS: ${{ secrets.ANDROID_RELEASE_KEYSTORE_KEY_ALIAS }}
QT_ANDROID_KEYSTORE_STORE_PASS: ${{ secrets.ANDROID_RELEASE_KEYSTORE_KEY_PASS }}
shell: bash
run: |
for abi in arm64-v8a armeabi-v7a; do
deploy/build.sh -t android --sign --abi ${abi} --build ./deploy/build/${abi}
VERSION=$(grep CMAKE_PROJECT_VERSION:STATIC deploy/build/${abi}/CMakeCache.txt | cut -d= -f2)
(cd deploy/build/${abi}/client/android-build && mv AmneziaVPN.apk AmneziaVPN_${VERSION}_android9+_qt_6_10_${abi}.apk)
done
- name: 'Upload arm64-v8a APK'
uses: actions/upload-artifact@v7
with:
path: deploy/build/arm64-v8a/client/android-build/*.apk
archive: false
retention-days: 7
- name: 'Upload armeabi-v7a APK'
uses: actions/upload-artifact@v7
with:
path: deploy/build/armeabi-v7a/client/android-build/*.apk
archive: false
retention-days: 7
# ------------------------------------------------------
Extra:
runs-on: ubuntu-latest
steps:

3
.gitmodules vendored
View File

@@ -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

View File

@@ -4,7 +4,7 @@ set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(PROJECT AmneziaVPN)
set(AMNEZIAVPN_VERSION 4.9.0.2)
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 2123)
set(APP_ANDROID_VERSION_CODE 2138)
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
set(MZ_PLATFORM_NAME "linux")

View File

@@ -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

View File

@@ -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)
## Технологии

View File

@@ -103,6 +103,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}
)
@@ -257,6 +258,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")

View File

@@ -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();

View File

@@ -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;

View File

@@ -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))
}
}

View File

@@ -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()
}
}

View File

@@ -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(

View File

@@ -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) }
}

View File

@@ -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
)

View File

@@ -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
)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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;

View File

@@ -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());

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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,6 +216,11 @@ ErrorCode ConnectionController::openConnection(const QString &serverId)
return errorCode;
}
const auto apiV2 = m_serversRepository->apiV2Config(serverId);
if (apiV2.has_value() && !apiV2->sendPayload.isEmpty()) {
PayloadSender::sendAll(apiV2->sendPayload);
}
emit openConnectionRequested(serverId, container, vpnConfiguration);
return ErrorCode::NoError;
}

View File

@@ -67,7 +67,6 @@ 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);
#ifdef Q_OS_ANDROID

View File

@@ -22,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();
@@ -31,8 +32,10 @@ CoreController::CoreController(const QSharedPointer<VpnConnection> &vpnConnectio
initControllers();
initSignalHandlers();
initAndroidController();
initAppleController();
if (!skipPlatformControllerInit) {
initAndroidController();
initAppleController();
}
initLogging();
m_translator = new QTranslator(this);
@@ -282,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)

View File

@@ -90,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();
@@ -114,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; }

View File

@@ -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);

View File

@@ -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);
});
}

View File

@@ -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;
};

View File

@@ -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);

View File

@@ -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);

View File

@@ -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,

View File

@@ -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()) {

View File

@@ -34,7 +34,6 @@ struct ApiConfig
int maxDeviceCount;
int issuedConfigs;
QJsonArray availableCountries;
QJsonArray supportedProtocols;
struct ServiceInfo {
bool isAdVisible = false;

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -280,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();

View File

@@ -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;

View File

@@ -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");

View File

@@ -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");

View File

@@ -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

View File

@@ -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

View 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;
}
}

View 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

View File

@@ -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;

View File

@@ -21,6 +21,7 @@ enum SharedScriptType {
// General scripts
prepare_host,
install_docker,
install_conntrack,
build_container,
remove_container,
remove_all_containers,

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -16,8 +16,6 @@
constexpr const char* WG_INTERFACE = "amn0";
constexpr uint16_t WG_KEEPALIVE_PERIOD = 60;
class WireguardUtils : public QObject {
Q_OBJECT

View 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

View File

@@ -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>

View 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>

View File

@@ -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>

View File

@@ -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);

View File

@@ -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",

View File

@@ -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);

View File

@@ -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: ", "))

View File

@@ -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);

View File

@@ -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
}

View File

@@ -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";
}

View File

@@ -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";
}

View File

@@ -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;
}

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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";
}

View File

@@ -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";
}

View 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"

View File

@@ -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>

View 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

View File

@@ -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);

View File

@@ -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();

View File

@@ -29,6 +29,7 @@ void ConnectionUiController::openConnection()
{
const QString serverId = m_serversController->getDefaultServerId();
if (serverId.isEmpty()) {
m_connectionController->setConnectionState(Vpn::ConnectionState::Disconnected);
return;
}

View File

@@ -0,0 +1,164 @@
#include "marketplaceUpdateController.h"
#include <QDebug>
#include "version.h"
#if defined(Q_OS_IOS) || defined(Q_OS_ANDROID)
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QLocale>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QRegularExpression>
#include <QVersionNumber>
#endif
#if defined(Q_OS_IOS)
#include "platforms/ios/ios_controller.h"
#endif
#if defined(Q_OS_ANDROID)
#include "platforms/android/android_controller.h"
#endif
#if defined(Q_OS_IOS) || defined(Q_OS_ANDROID)
namespace
{
#if defined(Q_OS_IOS)
constexpr auto kIosBundleId = "org.amnezia.AmneziaVPN";
constexpr auto kIosStoreUrlFallback = "itms-apps://itunes.apple.com/app/id1600529900";
#else
constexpr auto kAndroidPackage = "org.amnezia.vpn";
constexpr auto kAndroidStoreUrl = "https://play.google.com/store/apps/details?id=org.amnezia.vpn";
#endif
} // namespace
#endif
MarketplaceUpdateController::MarketplaceUpdateController(QObject *parent) : QObject(parent)
{
}
void MarketplaceUpdateController::start()
{
#if defined(Q_OS_IOS) || defined(Q_OS_ANDROID)
// Cover the app immediately so the store UI is not visible before the check
// resolves (avoids a flash of the main window before the update screen).
showCover();
const QUrl url = versionSourceUrl();
if (!url.isValid()) {
hideCover();
return;
}
QNetworkRequest request(url);
request.setTransferTimeout(1000);
request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork);
request.setHeader(QNetworkRequest::UserAgentHeader, QByteArrayLiteral("AmneziaVPN"));
QNetworkReply *reply = m_nam.get(request);
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
reply->deleteLater();
if (reply->error() != QNetworkReply::NoError) {
qWarning() << "[MarketplaceUpdate] network error:" << reply->errorString();
hideCover();
return;
}
QString version;
QString storeUrl;
if (!parseStoreVersion(reply->readAll(), version, storeUrl) || version.isEmpty()) {
qWarning() << "[MarketplaceUpdate] could not determine store version";
hideCover();
return;
}
const auto current = QVersionNumber::fromString(QString(APP_VERSION)).normalized();
const auto store = QVersionNumber::fromString(version).normalized();
qInfo() << "[MarketplaceUpdate] current:" << current.toString() << "store:" << store.toString();
if (store > current) {
showUpdatePrompt(storeUrl);
} else {
hideCover();
}
});
#endif
}
#if defined(Q_OS_IOS) || defined(Q_OS_ANDROID)
QUrl MarketplaceUpdateController::versionSourceUrl() const
{
#if defined(Q_OS_IOS)
const QString country = QLocale::system().name().section('_', 1, 1).toLower();
QString url = QStringLiteral("https://itunes.apple.com/lookup?bundleId=%1").arg(kIosBundleId);
if (!country.isEmpty()) {
url += QStringLiteral("&country=%1").arg(country);
}
return QUrl(url);
#else
return QUrl(QStringLiteral("https://play.google.com/store/apps/details?id=%1&hl=en&gl=US").arg(kAndroidPackage));
#endif
}
bool MarketplaceUpdateController::parseStoreVersion(const QByteArray &body, QString &version, QString &storeUrl)
{
#if defined(Q_OS_IOS)
const auto results = QJsonDocument::fromJson(body).object().value("results").toArray();
if (results.isEmpty()) {
return false;
}
const auto first = results.first().toObject();
version = first.value("version").toString();
storeUrl = first.value("trackViewUrl").toString();
if (storeUrl.isEmpty()) {
storeUrl = QString::fromLatin1(kIosStoreUrlFallback);
}
return !version.isEmpty();
#else
const QString html = QString::fromUtf8(body);
static const QRegularExpression re(QStringLiteral("\\[\\[\\[\"(\\d+\\.\\d+(?:\\.\\d+){0,2})\"\\]\\]"));
const auto match = re.match(html);
if (match.hasMatch()) {
version = match.captured(1);
}
storeUrl = QString::fromLatin1(kAndroidStoreUrl);
return !version.isEmpty();
#endif
}
void MarketplaceUpdateController::showCover()
{
#if defined(Q_OS_IOS)
IosController::Instance()->showUpdateCover();
#else
AndroidController::instance()->showUpdateCover();
#endif
}
void MarketplaceUpdateController::hideCover()
{
#if defined(Q_OS_IOS)
IosController::Instance()->hideUpdateCover();
#else
AndroidController::instance()->hideUpdateCover();
#endif
}
void MarketplaceUpdateController::showUpdatePrompt(const QString &storeUrl)
{
const QString title = tr("Update available");
const QString message = tr("A new version of AmneziaVPN is available.");
const QString updateTitle = tr("Update");
const QString skipTitle = tr("Skip");
#if defined(Q_OS_IOS)
IosController::Instance()->showUpdatePrompt(title, message, updateTitle, skipTitle, storeUrl);
#else
AndroidController::instance()->showUpdatePrompt(title, message, updateTitle, skipTitle, storeUrl);
#endif
}
#endif

View File

@@ -0,0 +1,30 @@
#ifndef MARKETPLACEUPDATECONTROLLER_H
#define MARKETPLACEUPDATECONTROLLER_H
#include <QNetworkAccessManager>
#include <QObject>
#include <QUrl>
class MarketplaceUpdateController : public QObject
{
Q_OBJECT
public:
explicit MarketplaceUpdateController(QObject *parent = nullptr);
public slots:
void start();
private:
#if defined(Q_OS_IOS) || defined(Q_OS_ANDROID)
QUrl versionSourceUrl() const;
bool parseStoreVersion(const QByteArray &body, QString &version, QString &storeUrl);
void showCover();
void hideCover();
void showUpdatePrompt(const QString &storeUrl);
QNetworkAccessManager m_nam;
#endif
};
#endif // MARKETPLACEUPDATECONTROLLER_H

View File

@@ -9,6 +9,7 @@
#include <QStandardPaths>
#include <QFutureWatcher>
#include <QtConcurrent>
#include <utility>
#include "core/utils/api/apiUtils.h"
#include "core/controllers/selfhosted/installController.h"
@@ -362,17 +363,27 @@ void InstallUiController::setContainerEnabled(const QString &serverId, int conta
}
emit serverIsBusy(true);
const ErrorCode errorCode = m_installController->setDockerContainerEnabledState(serverId, container, enabled);
emit serverIsBusy(false);
if (errorCode == ErrorCode::NoError) {
const ContainerConfig currentConfig = m_serversController->getContainerConfig(serverId, container);
m_protocolModel->updateModel(currentConfig);
emit setContainerEnabledFinished(enabled);
return;
}
InstallController *installController = m_installController;
auto *watcher = new QFutureWatcher<ErrorCode>(this);
QObject::connect(watcher, &QFutureWatcher<ErrorCode>::finished, this,
[this, watcher, serverId, container, enabled]() {
const ErrorCode errorCode = watcher->result();
watcher->deleteLater();
emit serverIsBusy(false);
emit installationErrorOccurred(errorCode);
if (errorCode == ErrorCode::NoError) {
const ContainerConfig currentConfig = m_serversController->getContainerConfig(serverId, container);
m_protocolModel->updateModel(currentConfig);
emit setContainerEnabledFinished(enabled);
return;
}
emit installationErrorOccurred(errorCode);
});
QFuture<ErrorCode> future = QtConcurrent::run([installController, serverId, container, enabled]() -> ErrorCode {
return installController->setDockerContainerEnabledState(serverId, container, enabled);
});
watcher->setFuture(future);
}
void InstallUiController::refreshContainerStatus(const QString &serverId, int containerIndex)
@@ -382,13 +393,23 @@ void InstallUiController::refreshContainerStatus(const QString &serverId, int co
return;
}
int status = 3;
const ErrorCode errorCode = m_installController->queryDockerContainerStatus(serverId, container, status);
if (errorCode != ErrorCode::NoError) {
emit containerStatusRefreshed(3);
return;
}
emit containerStatusRefreshed(status);
using StatusResult = std::pair<int, int>; // {status, errorCode}
InstallController *installController = m_installController;
auto *watcher = new QFutureWatcher<StatusResult>(this);
QObject::connect(watcher, &QFutureWatcher<StatusResult>::finished, this, [this, watcher]() {
const StatusResult result = watcher->result();
watcher->deleteLater();
emit containerStatusRefreshed(result.first, result.second);
});
QFuture<StatusResult> future = QtConcurrent::run([installController, serverId, container]() -> StatusResult {
int status = 3;
const ErrorCode errorCode = installController->queryDockerContainerStatus(serverId, container, status);
if (errorCode != ErrorCode::NoError) {
return { 3, static_cast<int>(errorCode) };
}
return { status, static_cast<int>(ErrorCode::NoError) };
});
watcher->setFuture(future);
}
void InstallUiController::refreshContainerDiagnostics(const QString &serverId, int containerIndex, int port)
@@ -398,14 +419,27 @@ void InstallUiController::refreshContainerDiagnostics(const QString &serverId, i
return;
}
MtProxyContainerDiagnostics diag;
const ErrorCode errorCode = m_installController->queryMtProxyDiagnostics(serverId, container, port, diag);
if (errorCode != ErrorCode::NoError) {
emit containerDiagnosticsRefreshed(false, false, -1, QString(), QString());
return;
}
emit containerDiagnosticsRefreshed(diag.portReachable, diag.upstreamReachable, diag.clientsConnected,
diag.lastConfigRefresh, diag.statsEndpoint);
using DiagResult = std::pair<bool, MtProxyContainerDiagnostics>;
InstallController *installController = m_installController;
auto *watcher = new QFutureWatcher<DiagResult>(this);
QObject::connect(watcher, &QFutureWatcher<DiagResult>::finished, this, [this, watcher]() {
const DiagResult result = watcher->result();
watcher->deleteLater();
if (!result.first) {
emit containerDiagnosticsRefreshed(false, false, -1, QString(), QString());
return;
}
const MtProxyContainerDiagnostics &diag = result.second;
emit containerDiagnosticsRefreshed(diag.portReachable, diag.upstreamReachable, diag.clientsConnected,
diag.lastConfigRefresh, diag.statsEndpoint);
});
QFuture<DiagResult> future =
QtConcurrent::run([installController, serverId, container, port]() -> DiagResult {
MtProxyContainerDiagnostics diag;
const ErrorCode errorCode = installController->queryMtProxyDiagnostics(serverId, container, port, diag);
return { errorCode == ErrorCode::NoError, diag };
});
watcher->setFuture(future);
}
void InstallUiController::fetchContainerSecret(const QString &serverId, int containerIndex)
@@ -415,8 +449,17 @@ void InstallUiController::fetchContainerSecret(const QString &serverId, int cont
return;
}
const QString secret = m_installController->fetchDockerContainerSecret(serverId, container);
emit containerSecretFetched(secret);
InstallController *installController = m_installController;
auto *watcher = new QFutureWatcher<QString>(this);
QObject::connect(watcher, &QFutureWatcher<QString>::finished, this, [this, watcher]() {
const QString secret = watcher->result();
watcher->deleteLater();
emit containerSecretFetched(secret);
});
QFuture<QString> future = QtConcurrent::run([installController, serverId, container]() -> QString {
return installController->fetchDockerContainerSecret(serverId, container);
});
watcher->setFuture(future);
}
void InstallUiController::rebootServer(const QString &serverId)
@@ -592,6 +635,7 @@ void InstallUiController::validateConfig()
{
const QString serverId = m_serversController->getDefaultServerId();
if (serverId.isEmpty()) {
emit configValidated(false);
return;
}
m_installController->validateConfig(serverId);

View File

@@ -114,7 +114,7 @@ signals:
void removeAllContainersFinished(const QString &finishedMessage);
void removeContainerFinished(const QString &finishedMessage);
void setContainerEnabledFinished(bool enabled);
void containerStatusRefreshed(int status);
void containerStatusRefreshed(int status, int errorCode);
void containerDiagnosticsRefreshed(bool portReachable, bool upstreamReachable, int clientsConnected,
const QString &lastConfigRefresh, const QString &statsEndpoint);
void containerSecretFetched(const QString &secret);

View File

@@ -73,12 +73,6 @@ QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const
}
return false;
}
case IsProtocolSelectionSupportedRole: {
if (m_accountInfoData.supportedProtocols.size() > 1) {
return true;
}
return false;
}
case IsSubscriptionExpiredRole: {
if (m_accountInfoData.configType == serverConfigUtils::ConfigType::AmneziaFreeV3) {
return false;
@@ -132,10 +126,6 @@ void ApiAccountInfoModel::updateModel(const QJsonObject &accountInfoObject, cons
accountInfoData.subscriptionDescription = accountInfoObject.value(apiDefs::key::subscriptionDescription).toString();
accountInfoData.isRenewalAvailable = accountInfoObject.value(apiDefs::key::isRenewalAvailable).toBool(false);
for (const auto &protocol : accountInfoObject.value(apiDefs::key::supportedProtocols).toArray()) {
accountInfoData.supportedProtocols.push_back(protocol.toString());
}
m_accountInfoData = accountInfoData;
m_supportInfo = accountInfoObject.value(apiDefs::key::supportInfo).toObject();
@@ -201,7 +191,6 @@ QHash<int, QByteArray> ApiAccountInfoModel::roleNames() const
roles[IsComponentVisibleRole] = "isComponentVisible";
roles[IsSubscriptionRenewalAvailableRole] = "isSubscriptionRenewalAvailable";
roles[HasExpiredWorkerRole] = "hasExpiredWorker";
roles[IsProtocolSelectionSupportedRole] = "isProtocolSelectionSupported";
roles[IsSubscriptionExpiredRole] = "isSubscriptionExpired";
roles[IsSubscriptionExpiringSoonRole] = "isSubscriptionExpiringSoon";
roles[IsInAppPurchaseRole] = "isInAppPurchase";

View File

@@ -22,7 +22,6 @@ public:
IsComponentVisibleRole,
IsSubscriptionRenewalAvailableRole,
HasExpiredWorkerRole,
IsProtocolSelectionSupportedRole,
IsSubscriptionExpiredRole,
IsSubscriptionExpiringSoonRole,
IsInAppPurchaseRole
@@ -58,8 +57,6 @@ private:
serverConfigUtils::ConfigType configType;
QStringList supportedProtocols;
QString subscriptionDescription;
bool isInAppPurchase = false;

View File

@@ -1,6 +1,6 @@
#include "mtProxyConfigModel.h"
#include "ui/models/utils/mtproxy_public_host_input.h"
#include "ui/utils/mtProxyPublicHostInput.h"
#include "core/utils/networkUtilities.h"
#include "core/utils/qrCodeUtils.h"
@@ -8,19 +8,14 @@
#include "core/utils/constants/configKeys.h"
#include "qrcodegen.hpp"
#include <QClipboard>
#include <QGuiApplication>
#include <QHostAddress>
#include <QRegExp>
#include <QRegularExpression>
#include <QtGlobal>
#include <qqml.h>
using namespace amnezia;
MtProxyConfigModel::MtProxyConfigModel(QObject *parent) : QAbstractListModel(parent) {
qmlRegisterType<PublicHostInputValidator>("MtProxyConfig", 1, 0, "PublicHostInputValidator");
}
MtProxyConfigModel::MtProxyConfigModel(QObject *parent) : QAbstractListModel(parent) {}
int MtProxyConfigModel::rowCount(const QModelIndex &parent) const {
Q_UNUSED(parent);
@@ -332,7 +327,7 @@ void MtProxyConfigModel::removeAdditionalSecret(int idx) {
QVariantList MtProxyConfigModel::additionalSecretsList() const {
QVariantList out;
out.reserve(m_protocolConfig.additionalSecrets.size());
for (const auto &s: m_protocolConfig.additionalSecrets) {
for (const auto &s : m_protocolConfig.additionalSecrets) {
if (!s.isEmpty()) {
out.append(s);
}
@@ -398,6 +393,9 @@ bool MtProxyConfigModel::isValidPublicHost(const QString &host) const {
return NetworkUtilities::checkIPv4Format(t);
}
if (a.protocol() == QHostAddress::IPv6Protocol) {
if (a.isNull() || a.isLoopback() || a == QHostAddress(QHostAddress::AnyIPv6)) {
return false;
}
return true;
}
static const QRegularExpression onlyAsciiDigits(QStringLiteral(R"(^\d+$)"));
@@ -504,20 +502,12 @@ bool MtProxyConfigModel::isValidFakeTlsDomain(const QString &domain) const {
if (!re.exactMatch(t)) {
return false;
}
// ee + 32 hex (base secret) + hex(UTF-8 domain); keep headroom under typical client limits.
if (t.toUtf8().size() > 111) {
return false;
}
return true;
}
QString MtProxyConfigModel::clipboardText() const {
if (QClipboard *c = QGuiApplication::clipboard()) {
return c->text();
}
return QString();
}
QString MtProxyConfigModel::sanitizeFakeTlsDomainFieldText(const QString &input) const {
const QString t = normalizeFakeTlsDomainInput(input);
QString out;
@@ -578,7 +568,6 @@ QString MtProxyConfigModel::sanitizeMtProxyTagFieldText(const QString &input) co
if (trimmed.startsWith(QLatin1String("0x"), Qt::CaseInsensitive)) {
trimmed = trimmed.mid(2).trimmed();
}
// Prefer a contiguous 32-hex run (paste from bot message with extra text).
static const QRegularExpression runHex(QStringLiteral(R"(([0-9a-fA-F]{32}))"));
const QRegularExpressionMatch m = runHex.match(trimmed);
if (m.hasMatch()) {
@@ -599,18 +588,6 @@ QString MtProxyConfigModel::sanitizeMtProxyTagFieldText(const QString &input) co
return out;
}
QString MtProxyConfigModel::sanitizeWorkersFieldText(const QString &input) const {
QString out;
out.reserve(qMin(input.size(), 3));
for (const QChar &c: input) {
const ushort u = c.unicode();
if (u >= '0' && u <= '9' && out.size() < 3) {
out.append(c);
}
}
return out;
}
QString MtProxyConfigModel::sanitizeOptionalIpv4FieldText(const QString &input) const {
QString out;
out.reserve(qMin(input.size(), 15));

View File

@@ -128,16 +128,12 @@ public slots:
Q_INVOKABLE bool isFakeTlsDomainInputAllowed(const QString &text) const;
Q_INVOKABLE QString clipboardText() const;
Q_INVOKABLE QString sanitizePublicHostFieldText(const QString &input) const;
Q_INVOKABLE QString sanitizePortFieldText(const QString &input) const;
Q_INVOKABLE QString sanitizeMtProxyTagFieldText(const QString &input) const;
Q_INVOKABLE QString sanitizeWorkersFieldText(const QString &input) const;
Q_INVOKABLE QString sanitizeOptionalIpv4FieldText(const QString &input) const;
Q_INVOKABLE bool isFakeTlsDomainTypingIncomplete(const QString &text) const;

View File

@@ -1,7 +1,12 @@
#include "telemtConfigModel.h"
#include "ui/utils/mtProxyPublicHostInput.h"
#include <QHostAddress>
#include <QRegExp>
#include <QRegularExpression>
#include "core/utils/networkUtilities.h"
#include "core/utils/qrCodeUtils.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/constants/protocolConstants.h"
@@ -49,7 +54,11 @@ bool TelemtConfigModel::setData(const QModelIndex &index, const QVariant &value,
break;
}
case Roles::TagRole: {
m_protocolConfig.tag = value.toString();
const QString tag = sanitizeMtProxyTagFieldText(value.toString());
if (!isValidMtProxyTag(tag)) {
return false;
}
m_protocolConfig.tag = tag;
break;
}
case Roles::IsEnabledRole: {
@@ -57,7 +66,11 @@ bool TelemtConfigModel::setData(const QModelIndex &index, const QVariant &value,
break;
}
case Roles::PublicHostRole: {
m_protocolConfig.publicHost = value.toString();
const QString h = value.toString().trimmed();
if (!isValidPublicHost(h)) {
return false;
}
m_protocolConfig.publicHost = h;
break;
}
case Roles::TransportModeRole: {
@@ -65,7 +78,11 @@ bool TelemtConfigModel::setData(const QModelIndex &index, const QVariant &value,
break;
}
case Roles::TlsDomainRole: {
m_protocolConfig.tlsDomain = value.toString();
const QString d = value.toString().trimmed();
if (!isValidFakeTlsDomain(d)) {
return false;
}
m_protocolConfig.tlsDomain = d;
break;
}
case Roles::AdditionalSecretsRole: {
@@ -85,11 +102,19 @@ bool TelemtConfigModel::setData(const QModelIndex &index, const QVariant &value,
break;
}
case Roles::NatInternalIpRole: {
m_protocolConfig.natInternalIp = value.toString();
const QString ip = value.toString().trimmed();
if (!isValidOptionalIpv4(ip)) {
return false;
}
m_protocolConfig.natInternalIp = ip;
break;
}
case Roles::NatExternalIpRole: {
m_protocolConfig.natExternalIp = value.toString();
const QString ip = value.toString().trimmed();
if (!isValidOptionalIpv4(ip)) {
return false;
}
m_protocolConfig.natExternalIp = ip;
break;
}
case Roles::MaskEnabledRole: {
@@ -238,7 +263,7 @@ void TelemtConfigModel::setSecret(const QString &secret) {
}
bool TelemtConfigModel::validateAndSetSecret(const QString &rawSecret) {
if (!QRegularExpression(QStringLiteral("^[0-9a-fA-F]{32}$")).match(rawSecret).hasMatch()) {
if (!QRegularExpression("^[0-9a-fA-F]{32}$").match(rawSecret).hasMatch()) {
return false;
}
setData(index(0), rawSecret, SecretRole);
@@ -254,7 +279,11 @@ void TelemtConfigModel::setTag(const QString &tag) {
}
void TelemtConfigModel::setPublicHost(const QString &host) {
setData(index(0), host, PublicHostRole);
const QString t = host.trimmed();
if (!isValidPublicHost(t)) {
return;
}
setData(index(0), t, PublicHostRole);
}
void TelemtConfigModel::setTransportMode(const QString &mode) {
@@ -262,13 +291,15 @@ void TelemtConfigModel::setTransportMode(const QString &mode) {
}
QString TelemtConfigModel::getTransportMode() const {
return m_protocolConfig.transportMode.isEmpty() ? QString::fromUtf8(protocols::telemt::transportModeStandard)
: m_protocolConfig.transportMode;
return m_protocolConfig.transportMode.isEmpty()
? QString(protocols::telemt::transportModeStandard)
: m_protocolConfig.transportMode;
}
QString TelemtConfigModel::getTlsDomain() const {
return m_protocolConfig.tlsDomain.isEmpty() ? QString::fromUtf8(protocols::telemt::defaultTlsDomain)
: m_protocolConfig.tlsDomain;
return m_protocolConfig.tlsDomain.isEmpty()
? QString(protocols::telemt::defaultTlsDomain)
: m_protocolConfig.tlsDomain;
}
QString TelemtConfigModel::getPublicHost() const {
@@ -276,7 +307,11 @@ QString TelemtConfigModel::getPublicHost() const {
}
void TelemtConfigModel::setTlsDomain(const QString &domain) {
setData(index(0), domain, TlsDomainRole);
const QString t = domain.trimmed();
if (!isValidFakeTlsDomain(t)) {
return;
}
setData(index(0), t, TlsDomainRole);
}
void TelemtConfigModel::setWorkersMode(const QString &mode) {
@@ -292,11 +327,19 @@ void TelemtConfigModel::setNatEnabled(bool enabled) {
}
void TelemtConfigModel::setNatInternalIp(const QString &ip) {
setData(index(0), ip, NatInternalIpRole);
const QString t = ip.trimmed();
if (!isValidOptionalIpv4(t)) {
return;
}
setData(index(0), t, NatInternalIpRole);
}
void TelemtConfigModel::setNatExternalIp(const QString &ip) {
setData(index(0), ip, NatExternalIpRole);
const QString t = ip.trimmed();
if (!isValidOptionalIpv4(t)) {
return;
}
setData(index(0), t, NatExternalIpRole);
}
void TelemtConfigModel::setMaskEnabled(bool enabled) {
@@ -334,6 +377,17 @@ void TelemtConfigModel::removeAdditionalSecret(int idx) {
emit dataChanged(index(0), index(0), QList<int>{AdditionalSecretsRole});
}
QVariantList TelemtConfigModel::additionalSecretsList() const {
QVariantList out;
out.reserve(m_protocolConfig.additionalSecrets.size());
for (const auto &s : m_protocolConfig.additionalSecrets) {
if (!s.isEmpty()) {
out.append(s);
}
}
return out;
}
void TelemtConfigModel::setEnabled(bool enabled) {
m_protocolConfig.isEnabled = enabled;
emit dataChanged(index(0), index(0), QList<int>{IsEnabledRole});
@@ -348,15 +402,15 @@ QString TelemtConfigModel::generateQrCode(const QString &text) {
}
QString TelemtConfigModel::defaultTlsDomain() const {
return QString::fromUtf8(protocols::telemt::defaultTlsDomain);
return protocols::telemt::defaultTlsDomain;
}
QString TelemtConfigModel::defaultPort() const {
return QString::fromUtf8(protocols::telemt::defaultPort);
return protocols::telemt::defaultPort;
}
QString TelemtConfigModel::defaultWorkers() const {
return QString::fromUtf8(protocols::telemt::defaultWorkers);
return protocols::telemt::defaultWorkers;
}
int TelemtConfigModel::maxWorkers() const {
@@ -364,19 +418,303 @@ int TelemtConfigModel::maxWorkers() const {
}
QString TelemtConfigModel::transportModeStandard() const {
return QString::fromUtf8(protocols::telemt::transportModeStandard);
return protocols::telemt::transportModeStandard;
}
QString TelemtConfigModel::transportModeFakeTLS() const {
return QString::fromUtf8(protocols::telemt::transportModeFakeTLS);
return protocols::telemt::transportModeFakeTLS;
}
QString TelemtConfigModel::workersModeAuto() const {
return QString::fromUtf8(protocols::telemt::workersModeAuto);
return protocols::telemt::workersModeAuto;
}
QString TelemtConfigModel::workersModeManual() const {
return QString::fromUtf8(protocols::telemt::workersModeManual);
return protocols::telemt::workersModeManual;
}
bool TelemtConfigModel::isValidPublicHost(const QString &host) const {
const QString t = host.trimmed();
if (t.isEmpty()) {
return true;
}
if (t.length() > 253) {
return false;
}
QHostAddress a(t);
if (a.protocol() == QHostAddress::IPv4Protocol) {
return NetworkUtilities::checkIPv4Format(t);
}
if (a.protocol() == QHostAddress::IPv6Protocol) {
if (a.isNull() || a.isLoopback() || a == QHostAddress(QHostAddress::AnyIPv6)) {
return false;
}
return true;
}
static const QRegularExpression onlyAsciiDigits(QStringLiteral(R"(^\d+$)"));
if (onlyAsciiDigits.match(t).hasMatch()) {
return false;
}
return NetworkUtilities::domainRegExp().exactMatch(t);
}
bool TelemtConfigModel::isPublicHostInputAllowed(const QString &text) const {
return mtproxyPublicHostInputAllowed(text);
}
bool TelemtConfigModel::isPublicHostTypingIncomplete(const QString &text) const {
const QString t = text.trimmed();
if (isValidPublicHost(t)) {
return false;
}
static const QRegularExpression onlyDigitDot(QStringLiteral(R"(^[0-9.]+$)"));
if (onlyDigitDot.match(t).hasMatch()) {
if (t.endsWith(QLatin1Char('.'))) {
return true;
}
const QStringList parts = t.split(QLatin1Char('.'), Qt::KeepEmptyParts);
if (parts.size() < 4) {
return true;
}
for (const QString &part: parts) {
if (part.isEmpty()) {
return true;
}
}
return false;
}
if (t.contains(QLatin1Char(':'))) {
if (t.contains(QLatin1String(":::"))) {
return false;
}
if (t.endsWith(QLatin1Char(':'))) {
return true;
}
QHostAddress a(t);
if (a.protocol() == QHostAddress::IPv6Protocol) {
return false;
}
if (!t.contains(QLatin1String("::")) && t.count(QLatin1Char(':')) < 7 && !t.contains(QLatin1Char('.'))) {
return true;
}
return false;
}
if (!t.contains(QLatin1Char('.'))) {
return true;
}
return false;
}
bool TelemtConfigModel::isValidMtProxyTag(const QString &tag) const {
if (tag.isEmpty()) {
return true;
}
static const QRegularExpression re(
QStringLiteral("^([0-9a-fA-F]{%1})$").arg(protocols::telemt::botTagHexLength));
return re.match(tag).hasMatch();
}
bool TelemtConfigModel::isMtProxyTagTypingIncomplete(const QString &text) const {
const QString t = text.trimmed();
if (t.isEmpty()) {
return true;
}
static const QRegularExpression hexOnly(QStringLiteral(R"(^[0-9a-fA-F]*$)"));
if (!hexOnly.match(t).hasMatch()) {
return false;
}
return t.size() < protocols::telemt::botTagHexLength;
}
int TelemtConfigModel::mtProxyBotTagHexLength() const {
return protocols::telemt::botTagHexLength;
}
bool TelemtConfigModel::isValidFakeTlsDomain(const QString &domain) const {
const QString t = domain.trimmed();
if (t.isEmpty()) {
return true;
}
if (t.length() > 253) {
return false;
}
QHostAddress addr;
if (addr.setAddress(t)) {
return false;
}
static const QRegularExpression onlyAsciiDigits(QStringLiteral(R"(^\d+$)"));
if (onlyAsciiDigits.match(t).hasMatch()) {
return false;
}
QRegExp re(NetworkUtilities::domainRegExp());
re.setCaseSensitivity(Qt::CaseInsensitive);
if (!re.exactMatch(t)) {
return false;
}
if (t.toUtf8().size() > 111) {
return false;
}
return true;
}
QString TelemtConfigModel::normalizeFakeTlsDomainInput(const QString &input) const {
QString t = input.trimmed();
if (t.startsWith(QLatin1String("https://"), Qt::CaseInsensitive)) {
t = t.mid(8);
} else if (t.startsWith(QLatin1String("http://"), Qt::CaseInsensitive)) {
t = t.mid(7);
}
if (const int slash = t.indexOf(QLatin1Char('/')); slash >= 0) {
t = t.left(slash);
}
if (const int at = t.indexOf(QLatin1Char('@')); at >= 0) {
t = t.mid(at + 1);
}
if (const int colon = t.indexOf(QLatin1Char(':')); colon >= 0) {
t = t.left(colon);
}
if (t.startsWith(QLatin1String("www."), Qt::CaseInsensitive)) {
const QString rest = t.mid(4);
if (rest.contains(QLatin1Char('.'))) {
t = rest;
}
}
return t.trimmed();
}
bool TelemtConfigModel::isFakeTlsDomainTypingIncomplete(const QString &text) const {
const QString t = text.trimmed();
if (t.isEmpty()) {
return true;
}
if (isValidFakeTlsDomain(t)) {
return false;
}
if (t.contains(QLatin1Char('/')) || t.contains(QLatin1Char(':')) || t.contains(QLatin1Char('@'))
|| t.contains(QLatin1Char(' '))) {
return false;
}
if (t.contains(QLatin1String(".."))) {
return false;
}
if (!t.contains(QLatin1Char('.'))) {
return true;
}
if (t.endsWith(QLatin1Char('.'))) {
return true;
}
static const QRegularExpression legalPartial(QStringLiteral(R"(^[a-zA-Z0-9.-]*$)"));
if (!legalPartial.match(t).hasMatch()) {
return false;
}
return true;
}
bool TelemtConfigModel::isFakeTlsDomainInputAllowed(const QString &text) const {
if (text.length() > 253) {
return false;
}
static const QRegularExpression re(QStringLiteral(R"(^[a-zA-Z0-9.-]*$)"));
return re.match(text).hasMatch();
}
QString TelemtConfigModel::sanitizeFakeTlsDomainFieldText(const QString &input) const {
const QString t = normalizeFakeTlsDomainInput(input);
QString out;
out.reserve(t.size());
for (const QChar &c: t) {
const ushort u = c.unicode();
const bool letter = (u >= 'a' && u <= 'z') || (u >= 'A' && u <= 'Z');
const bool digit = (u >= '0' && u <= '9');
if (letter || digit || u == '.' || u == '-') {
out.append(c);
}
}
if (out.size() > 253) {
out.truncate(253);
}
return out;
}
QString TelemtConfigModel::sanitizePublicHostFieldText(const QString &input) const {
QString out;
const int cap = qMin(input.size(), 253);
out.reserve(cap);
for (const QChar &c: input) {
if (out.size() >= 253) {
break;
}
const ushort u = c.unicode();
if ((u >= 'a' && u <= 'z') || (u >= 'A' && u <= 'Z') || (u >= '0' && u <= '9') || u == '.' || u == ':' ||
u == '-') {
out.append(c);
}
}
return out;
}
QString TelemtConfigModel::sanitizePortFieldText(const QString &input) const {
QString out;
out.reserve(qMin(input.size(), 5));
for (const QChar &c: input) {
const ushort u = c.unicode();
if (u >= '0' && u <= '9' && out.size() < 5) {
out.append(c);
}
}
return out;
}
QString TelemtConfigModel::sanitizeMtProxyTagFieldText(const QString &input) const {
QString trimmed = input.trimmed();
if (trimmed.startsWith(QLatin1String("0x"), Qt::CaseInsensitive)) {
trimmed = trimmed.mid(2).trimmed();
}
static const QRegularExpression runHex(QStringLiteral(R"(([0-9a-fA-F]{32}))"));
const QRegularExpressionMatch m = runHex.match(trimmed);
if (m.hasMatch()) {
return m.captured(1);
}
const int cap = protocols::telemt::botTagHexLength;
QString out;
out.reserve(qMin(trimmed.size(), cap));
for (const QChar &c: trimmed) {
if (out.size() >= cap) {
break;
}
const ushort u = c.unicode();
if ((u >= '0' && u <= '9') || (u >= 'a' && u <= 'f') || (u >= 'A' && u <= 'F')) {
out.append(c);
}
}
return out;
}
QString TelemtConfigModel::sanitizeOptionalIpv4FieldText(const QString &input) const {
QString out;
out.reserve(qMin(input.size(), 15));
for (const QChar &c: input) {
if (out.size() >= 15) {
break;
}
const ushort u = c.unicode();
if ((u >= '0' && u <= '9') || u == '.') {
out.append(c);
}
}
return out;
}
bool TelemtConfigModel::isValidOptionalIpv4(const QString &ip) const {
const QString t = ip.trimmed();
if (t.isEmpty()) {
return true;
}
return NetworkUtilities::checkIPv4Format(t);
}
QHash<int, QByteArray> TelemtConfigModel::roleNames() const {

View File

@@ -88,6 +88,8 @@ public slots:
Q_INVOKABLE void removeAdditionalSecret(int idx);
Q_INVOKABLE QVariantList additionalSecretsList() const;
Q_INVOKABLE QString generateQrCode(const QString &text);
Q_INVOKABLE void setEnabled(bool enabled);
@@ -116,12 +118,44 @@ public slots:
Q_INVOKABLE QString workersModeManual() const;
Q_INVOKABLE bool isValidPublicHost(const QString &host) const;
Q_INVOKABLE bool isPublicHostInputAllowed(const QString &text) const;
Q_INVOKABLE bool isPublicHostTypingIncomplete(const QString &text) const;
Q_INVOKABLE bool isValidMtProxyTag(const QString &tag) const;
Q_INVOKABLE bool isMtProxyTagTypingIncomplete(const QString &text) const;
Q_INVOKABLE int mtProxyBotTagHexLength() const;
Q_INVOKABLE bool isValidFakeTlsDomain(const QString &domain) const;
Q_INVOKABLE bool isFakeTlsDomainTypingIncomplete(const QString &text) const;
Q_INVOKABLE bool isFakeTlsDomainInputAllowed(const QString &text) const;
Q_INVOKABLE QString sanitizeFakeTlsDomainFieldText(const QString &input) const;
Q_INVOKABLE QString sanitizePublicHostFieldText(const QString &input) const;
Q_INVOKABLE QString sanitizePortFieldText(const QString &input) const;
Q_INVOKABLE QString sanitizeMtProxyTagFieldText(const QString &input) const;
Q_INVOKABLE QString sanitizeOptionalIpv4FieldText(const QString &input) const;
Q_INVOKABLE bool isValidOptionalIpv4(const QString &ip) const;
protected:
QHash<int, QByteArray> roleNames() const override;
private:
static void applyDefaults(amnezia::TelemtProtocolConfig &c);
QString normalizeFakeTlsDomainInput(const QString &input) const;
amnezia::DockerContainer m_container = amnezia::DockerContainer::None;
QJsonObject m_fullConfig;
amnezia::TelemtProtocolConfig m_protocolConfig;

View File

@@ -20,13 +20,42 @@ Popup {
signal captchaSolved(string captchaId, string solution)
signal refreshCaptchaRequested()
readonly property int keyboardHeight: {
if (PageController.imeHeight > 0) {
return PageController.imeHeight
}
if (Qt.platform.os === "ios" && Qt.inputMethod.visible) {
const r = Qt.inputMethod.keyboardRectangle
if (r.height <= 0) {
return 0
}
return Math.max(0, Math.round(parent.height - r.y))
}
return 0
}
leftMargin: 25
rightMargin: 25
bottomMargin: 70 + SettingsController.safeAreaBottomMargin
width: parent.width - leftMargin - rightMargin
anchors.centerIn: parent
x: Math.round((parent.width - width) / 2)
y: {
const centered = Math.round((parent.height - height) / 2)
if (root.keyboardHeight === 0) {
return centered
}
return Math.max(20, Math.min(centered, parent.height - root.keyboardHeight - height - 16))
}
Behavior on y {
NumberAnimation {
duration: 150
easing.type: Easing.OutCubic
}
}
modal: true
closePolicy: Popup.NoAutoClose
@@ -37,7 +66,7 @@ Popup {
onOpened: {
timer.start()
solutionField.textField.text = ""
solutionField.textField.focus = true
solutionField.textField.forceActiveFocus()
}
onCaptchaIdChanged: {

View File

@@ -93,7 +93,9 @@ Rectangle {
wrapMode: Text.Wrap
ContextMenu.menu: ContextMenuType {
ContextMenu.menu: contextMenu
ContextMenuType {
id: contextMenu
textObj: textArea
}

View File

@@ -79,7 +79,9 @@ Rectangle {
wrapMode: Text.Wrap
ContextMenu.menu: ContextMenuType {
ContextMenu.menu: contextMenu
ContextMenuType {
id: contextMenu
textObj: textArea
}

View File

@@ -144,7 +144,9 @@ Item {
}
}
ContextMenu.menu: ContextMenuType {
ContextMenu.menu: contextMenu
ContextMenuType {
id: contextMenu
textObj: textField
}

View File

@@ -21,6 +21,41 @@ PageType {
property var containersDropDownRef: null
property var apiAvailableProtocols: []
property string apiCurrentProtocol: ""
readonly property bool isApiProtocolSelectionVisible: ServersUiController.isDefaultServerFromApi && root.apiAvailableProtocols.length > 0
function updateApiProtocolState() {
if (ServersUiController.isDefaultServerFromApi) {
root.apiAvailableProtocols = SubscriptionUiController.availableProtocols(ServersUiController.defaultServerId)
root.apiCurrentProtocol = SubscriptionUiController.currentProtocol(ServersUiController.defaultServerId)
} else {
root.apiAvailableProtocols = []
root.apiCurrentProtocol = ""
}
}
function protocolDisplayName(protocol) {
switch (protocol) {
case "awg": return "AmneziaWG"
case "vless": return "VLESS"
default: return protocol
}
}
Component.onCompleted: {
root.updateApiProtocolState()
}
Connections {
target: ServersUiController
function onDefaultServerIdChanged() {
root.updateApiProtocolState()
}
}
Connections {
target: Qt.application
@@ -210,6 +245,10 @@ PageType {
drawer.collapsedHeight = collapsed.implicitHeight
}
onImplicitHeightChanged: {
drawer.collapsedHeight = collapsed.implicitHeight
}
DividerType {
Layout.topMargin: 10
Layout.fillWidth: false
@@ -311,7 +350,7 @@ PageType {
objectName: "rowLayoutLabel"
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
Layout.topMargin: 8
Layout.bottomMargin: drawer.isCollapsedStateActive ? 44 : ServersUiController.isDefaultServerFromApi ? 61 : 16
Layout.bottomMargin: root.isApiProtocolSelectionVisible ? 8 : (drawer.isCollapsedStateActive ? 44 : ServersUiController.isDefaultServerFromApi ? 61 : 16)
spacing: 0
BasicButtonType {
@@ -365,6 +404,61 @@ PageType {
}
}
}
RowLayout {
objectName: "protocolRowLayout"
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
Layout.bottomMargin: drawer.isCollapsedStateActive ? 44 : 61
spacing: 0
visible: root.isApiProtocolSelectionVisible
BasicButtonType {
id: protocolButton
objectName: "protocolButton"
enabled: root.apiAvailableProtocols.length > 1
hoverEnabled: enabled
implicitHeight: 36
leftPadding: 16
rightPadding: 16
defaultColor: AmneziaStyle.color.transparent
hoveredColor: AmneziaStyle.color.translucentWhite
pressedColor: AmneziaStyle.color.sheerWhite
disabledColor: AmneziaStyle.color.transparent
textColor: AmneziaStyle.color.mutedGray
buttonTextLabel.lineHeight: 16
buttonTextLabel.font.pixelSize: 13
buttonTextLabel.font.weight: 400
text: root.apiAvailableProtocols.length > 1
? root.protocolDisplayName(root.apiCurrentProtocol)
: root.protocolDisplayName(root.apiAvailableProtocols[0])
leftImageSource: "qrc:/images/controls/arrow-left-right.svg"
leftImageColor: AmneziaStyle.color.mutedGray
rightImageSource: enabled ? "qrc:/images/controls/chevron-down.svg" : ""
Keys.onEnterPressed: this.clicked()
Keys.onReturnPressed: this.clicked()
onClicked: {
if (ConnectionController.isConnectionInProgress) {
PageController.showNotificationMessage(qsTr("Unable change protocol while trying to make an active connection"))
return
}
if (ConnectionController.isConnected) {
PageController.showNotificationMessage(qsTr("Cannot change protocol during active connection"))
return
}
protocolSelectionDrawer.openTriggered()
}
}
}
}
ColumnLayout {
@@ -479,4 +573,119 @@ PageType {
}
}
}
DrawerType2 {
id: protocolSelectionDrawer
objectName: "protocolSelectionDrawer"
anchors.fill: parent
expandedStateContent: Item {
id: protocolDrawerContainer
implicitHeight: root.height * 0.5
Component.onCompleted: {
protocolSelectionDrawer.expandedHeight = protocolDrawerContainer.implicitHeight
}
ColumnLayout {
id: protocolDrawerHeader
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 16
BackButtonType {
id: protocolDrawerBackButton
Layout.fillWidth: true
backButtonImage: "qrc:/images/controls/arrow-left.svg"
backButtonFunction: function() { protocolSelectionDrawer.closeTriggered() }
}
Header2Type {
Layout.fillWidth: true
Layout.topMargin: 16
Layout.leftMargin: 16
Layout.rightMargin: 16
headerText: qsTr("VPN protocol")
}
}
ListViewType {
id: protocolDrawerListView
anchors.top: protocolDrawerHeader.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.topMargin: 16
model: root.apiAvailableProtocols
ButtonGroup {
id: protocolDrawerButtonGroup
}
delegate: Item {
implicitWidth: protocolDrawerListView.width
implicitHeight: protocolDrawerDelegate.implicitHeight
ColumnLayout {
id: protocolDrawerDelegate
anchors.fill: parent
anchors.leftMargin: 16
anchors.rightMargin: 16
VerticalRadioButton {
id: protocolDrawerRadioButton
Layout.fillWidth: true
text: root.protocolDisplayName(modelData)
ButtonGroup.group: protocolDrawerButtonGroup
checkable: !ConnectionController.isConnected
checked: modelData === root.apiCurrentProtocol
onClicked: {
protocolSelectionDrawer.closeTriggered()
if (modelData === root.apiCurrentProtocol) {
return
}
if (ConnectionController.isConnected) {
PageController.showNotificationMessage(qsTr("Cannot change protocol during active connection"))
return
}
PageController.showBusyIndicator(true)
ServersUiController.setProcessedServerId(ServersUiController.defaultServerId)
SubscriptionUiController.setCurrentProtocol(ServersUiController.defaultServerId, modelData)
if (!SubscriptionUiController.updateServiceFromGateway(ServersUiController.defaultServerId, "", "", true)) {
SubscriptionUiController.setCurrentProtocol(ServersUiController.defaultServerId, root.apiCurrentProtocol)
}
root.updateApiProtocolState()
PageController.showBusyIndicator(false)
}
Keys.onEnterPressed: protocolDrawerRadioButton.clicked()
Keys.onReturnPressed: protocolDrawerRadioButton.clicked()
}
DividerType {
Layout.fillWidth: true
}
}
}
}
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -259,41 +259,9 @@ PageType {
}
DividerType {
visible: !root.isSubscriptionExpired && !root.isSubscriptionExpiringSoon
&& root.isSubscriptionRenewalAvailable && !root.isInAppPurchase
}
SwitcherType {
id: switcher
readonly property bool isVlessProtocol: SubscriptionUiController.isVlessProtocol(ServersUiController.processedServerId)
readonly property bool isProtocolSwitchBlocked: ServersUiController.isDefaultServerCurrentlyProcessed() && ConnectionController.isConnected
Layout.fillWidth: true
Layout.topMargin: 24
Layout.rightMargin: 16
Layout.leftMargin: 16
Layout.bottomMargin: 24
visible: ApiAccountInfoModel.data("isProtocolSelectionSupported")
enabled: !switcher.isProtocolSwitchBlocked
text: qsTr("Use VLESS protocol")
checked: switcher.isVlessProtocol
onToggled: function() {
if (ServersUiController.isDefaultServerCurrentlyProcessed() && ConnectionController.isConnected) {
PageController.showNotificationMessage(qsTr("Cannot change protocol during active connection"))
} else {
PageController.showBusyIndicator(true)
SubscriptionUiController.setCurrentProtocol(ServersUiController.processedServerId, switcher.isVlessProtocol ? "awg" : "vless")
SubscriptionUiController.updateServiceFromGateway(ServersUiController.processedServerId, "", "", true)
PageController.showBusyIndicator(false)
}
}
}
DividerType {
visible: footer.isVisibleForAmneziaFree
visible: (!root.isSubscriptionExpired && !root.isSubscriptionExpiringSoon
&& root.isSubscriptionRenewalAvailable && !root.isInAppPurchase)
|| footer.isVisibleForAmneziaFree
}
WarningType {

View File

@@ -17,6 +17,8 @@ import "../Controls2/TextTypes"
PageType {
id: root
property bool isRestoringBackup: false
Connections {
target: SettingsController
@@ -134,9 +136,14 @@ PageType {
textColor: AmneziaStyle.color.paleGray
borderWidth: 1
enabled: !root.isRestoringBackup
text: qsTr("Restore from backup")
clickedFunc: function() {
if (root.isRestoringBackup) {
return
}
var filePath = SystemController.getFileName(qsTr("Open backup file"),
qsTr("Backup files (*.backup)"))
if (filePath !== "") {
@@ -148,6 +155,10 @@ PageType {
}
function restoreBackup(filePath) {
if (root.isRestoringBackup) {
return
}
var headerText = qsTr("Import settings from a backup file?")
var descriptionText = qsTr("All current settings will be reset");
var yesButtonText = qsTr("Continue")
@@ -157,9 +168,13 @@ PageType {
if (ConnectionController.isConnected) {
PageController.showNotificationMessage(qsTr("Cannot restore backup settings during active connection"))
} else {
root.isRestoringBackup = true
PageController.showBusyIndicator(true)
SettingsController.restoreAppConfig(filePath)
PageController.showBusyIndicator(false)
Qt.callLater(function() {
SettingsController.restoreAppConfig(filePath)
PageController.showBusyIndicator(false)
root.isRestoringBackup = false
})
}
}
var noButtonFunction = function() {

View File

@@ -69,9 +69,7 @@ PageType {
rightImageSource: "qrc:/images/controls/chevron-right.svg"
clickedFunction: function() {
if (!isUpdate) {
NewsModel.markAsRead(index)
}
NewsModel.markAsRead(index)
NewsModel.processedIndex = index
PageController.goToPage(PageEnum.PageSettingsNewsDetail)
}

View File

@@ -16,6 +16,8 @@ import "../Config"
PageType {
id: root
property bool isRestoringBackup: false
Connections {
target: ImportController
@@ -228,6 +230,8 @@ PageType {
rightImageSource: "qrc:/images/controls/chevron-right.svg"
leftImageSource: imageSource
enabled: !root.isRestoringBackup
onClicked: { handler() }
Keys.onEnterPressed: this.clicked()
@@ -314,12 +318,19 @@ PageType {
property string imageSource: "qrc:/images/controls/archive-restore.svg"
property bool isVisible: PageController.isStartPageVisible()
property var handler: function() {
if (root.isRestoringBackup) {
return
}
var filePath = SystemController.getFileName(qsTr("Open backup file"),
qsTr("Backup files (*.backup)"))
if (filePath !== "") {
root.isRestoringBackup = true
PageController.showBusyIndicator(true)
SettingsController.restoreAppConfig(filePath)
PageController.showBusyIndicator(false)
Qt.callLater(function() {
SettingsController.restoreAppConfig(filePath)
PageController.showBusyIndicator(false)
root.isRestoringBackup = false
})
}
}
}

View File

@@ -320,7 +320,7 @@ PageType {
Layout.rightMargin: 16
visible: isQrCodeVisible
horizontalAlignment: Text.AlignHCenter
text: qsTr("To read the QR code in the Amnezia app, select \"Add server\" → \"I have data to connect\"\"QR code, key or settings file\"")
text: qsTr("To read the QR code in the Amnezia app, tap + in the main menu'QR code'")
}
}
}

View File

@@ -1,4 +1,4 @@
#include "mtproxy_public_host_input.h"
#include "mtProxyPublicHostInput.h"
#include <QRegularExpression>

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